Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- 우테코
- 회고
- fastapi
- Spring
- 해외봉사
- 알고리즘
- LV2
- 코딩테스트
- openAI
- OOM
- 세션로그인
- cors
- llm
- 게시판
- docker
- 프로젝트
- 네팔
- Lv.2
- 서버 꺼짐
- spring boot
- 부트스트랩
- 쿠키로그인
- springboot
- crud
- Java
- 로그인
- 커밋 메시지
- mysql
- 프로그래머스
- Dockerfile
Archives
- Today
- Total
s00jin 님의 블로그
6. [로그인] JWT 인증 인가를 이용한 로그인 구현하기 | Spring Boot / JWT 본문
프로젝트/하고 싶은거 다해보는 내 사이트
6. [로그인] JWT 인증 인가를 이용한 로그인 구현하기 | Spring Boot / JWT
s00jin 2025. 8. 17. 01:06JWT란
인증에 필요한 정보들을 암호화 시킨 JSON 토큰이다.
JWT 구조
JWT는 Header, Payload, Signature가 점으로 구분되어 이루어져 있다.
AAAAA.BBBBB.CCCCC
A ← Header
B ← Payload
C ← Signature

Header
헤더에는 일반적으로 토큰 유형과 서명(해싱) 알고리즘이 들어간다.
- typ : 토큰 타입 지정
- alg : 서명 알고리즘 지정 (보통, HS256, RSA 사용 + Signature에서 사용)
{
"typ": "JWT",
"alg" : "HS256"
}
Payload
페이로드에는 클레임으로 구성되어 있는데, 등록된 클래임과 개인 클래임이 있다.
클레임(claim) 이란?
- 토큰에 담을 정보의 한 조각
- key-value 형태
- 여러 개의 클레임을 담을 수 있음
클레임 종류
- 등록된 클레임
- 이미 정해진 클래임
- 필수가 아닌 선택
- 공개된 클레임
- 비공개 클레임
- 정보 공유를 위해 사용자가 지정한 정보를 담는다
{
// 등록된 클레임
"exp": "1234032343",
// 비공개 클레임
"userName": "One",
"isAdmin": false
}
Signature
헤더에서 정의한 알고리즘 방식을 사용해서 헤더 + 페이로드 + 비밀키를 합친 것을 암호화 한 것이다.
Base64Url(Header) + . + Base64Url(PayLoad) + SecretKey 를 알고리즘을 사용하여 암호화
JWT 인증 방식
구현할 때 지정해준 비밀키를 사용해서 서버가 받아온 토큰을 디코딩한다.
이때, Header와 Payload는 비밀키가 없어도 누구나 정보를 알아낼 수 있다. 하지만 Signature은 비밀키를 알아야만 구할 수 있다.

- 사용자가 로그인을 요청함
- 요청한 로그인 정보를 가지고 사용자를 확인함
- 사용자가 맞다면 토큰을 발급함
- 발급한 토큰을 서버에 보내줌
- 사용자가 서버에 요청 시, 헤더에 발급 받은 토큰을 붙여 요청을 보냄
- 토큰 검증
- 문제 없다면 응답 반환
구현 코드
라이브러리 설치
// JWT Token 사용 라이브러리
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0..11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
JwtTokenUtil
JWT 사용시 필용한 기능들을 정리해 둔 클래스
package org.mySite.user.jwt;
import java.util.Date;
public class JwtTokenUtil {
// JWT token 발급 (Claim에 loginId 넣기)
public static String createToken(String loginId, String key, long expireTimeMs) {
// Claim = Jwt Token에 들어갈 정보
// Claim에 loginId를 넣어줌으로써 나중에 loginId를 꺼낼 수 있음
Claims claims = Jwts.claims();
claims.put("loginId", loginId);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expireTimeMs))
.signWith(SignatureAlgorithm.HS256, key)
.compact();
}
// Claims에서 loginId 꺼내기
public static String getLoginId(String token, String secretKey) {
return extractClaims(token, secretKey).get("loginId").toString();
}
// 발급된 Token이 만료 시간이 지났는지 체크
public static boolean isExpired(String token, String secretKey){
Date expiredDate = extractClaims(token, secretKey).getExpiration();
// 만료 날짜가 지금보다 이전인지 체크
return expiredDate.before(new Date());
}
// SecretKey를 사용해 Token Parsing
private static Claims extractClaims(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
}
}
SecurityConfig
package org.mySite.user.SpringSecurity;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig{
//spring Security 로그인에서 사용 + JWT
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//spring security 사용
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// 인증, 인가 필요한 url 지정
.requestMatchers("/security-login/info").authenticated() //authenticated 해당 url에 진입하기 위해 authentication(인증, 로그인)이 필요함
.requestMatchers("/security-login/admin/**").hasAuthority(UserRole.ADMIN.name()) // 해당 url 집입하기 위해서 Authorization(인가, ex)권한이 ADMIN인 유저만 집입가능)이 필요함
.anyRequest().permitAll() // 그외의 모든 url / authentication, authorization 필요 없이 통과
)
.formLogin(form -> form
// form login 방식 적용
.usernameParameter("loginId") // 로그인할 때 사용되는 id를 적어줌(여기서는 loginId로 로그인 하기 때문에 따로 적어줌. userName으로 로그인 한다면 적어주지 않아도 됨)
.passwordParameter("password") // 로그인할 때 사용되는 password를 적어줌
.loginPage("/security-login/login") // 로그인 페이지 url
.defaultSuccessUrl("/security-login", true) // 로그인 성공 시 이동할 url
.failureUrl("/security-login/login") // 로그인 실패 시 이동할 url
)
.logout(logout -> logout
// 로그아웃에 대한 정보
.logoutUrl("/security-login/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
);
return http.build();
}
//---------------------------
// JWT 로그인에서 사용
private final @Lazy JwtTokenFilter jwtTokenFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic(Customizer.withDefaults()) // Http Basic 인증 비활성화
.csrf(csrf -> csrf.disable()) // csrf 보호 비활성화(jwt는 상태가 없기 때문)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안함 + 필수 설정
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) // JWT 필터 추가
.authorizeHttpRequests(auth -> auth
.requestMatchers("/jwt-login/info").authenticated() // 인증 필요
.requestMatchers("jwt-login/admin/**").hasAuthority(UserRole.ADMIN.name()) // ADMIN 권한 필요
.anyRequest().permitAll() // 그 외 요청은 모두 허용
);
return httpSecurity.build();
}
}
JwtTokenFilter
package org.mySite.user.jwt;
@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter { // OncePerRequestFilter : 매번 들어갈 때 마다 체크해주는 필터
private final UserDetailsService userDetailsService;
@Value("${jwt.secret}")
private String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
// Header의 Authorization 값이 비어있으면 => Jwt Token을 전송하지 않음 => 로그인 하지 않음
if(authorizationHeader == null) {
filterChain.doFilter(request, response);
return;
}
// Header의 Authorization 값이 'Bearer '로 시작하지 않으면 => 잘못된 토큰
if(!authorizationHeader.startsWith("Bearer ")){
filterChain.doFilter(request, response);
return;
}
// 전송받은 값에서 'Bearer ' 뒷부분(Jwt Token) 추출
String token = authorizationHeader.split(" ")[1];
// 전송받은 Jwt Token이 만료되었으면 => 다음 필터 진행(인증 X)
if(JwtTokenUtil.isExpired(token, secretKey)) {
filterChain.doFilter(request, response);
return;
}
// Jwt Token에서 loginId 추출
String loginId = JwtTokenUtil.getLoginId(token, secretKey);
// 추출한 loginId로 User 찾아오기
UserDetails userDetails = userDetailsService.loadUserByUsername(loginId);
// loginUser 정보로 UsernamePasswordAuthenticationToken 발급
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails.getUsername(), null, userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 권한 부여
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
JwtLoginApiController
package org.mySite.user.jwt;
@RestController
@RequiredArgsConstructor
@RequestMapping("/jwt-login")
public class JwtLoginApiController {
private final UserService userService;
@PostMapping("/join")
public String join(@RequestBody JoinRequest joinRequest) {
// loginId 중복 체크
if(userService.checkLoginId(joinRequest.getLoginId())) {
return "로그인 아이디가 중복됩니다.";
}
// 닉네임 중복 체크
if(userService.checkNickname(joinRequest.getNickname())) {
return "닉네임이 중복됩니다.";
}
// password + passwordCheck 확임
if(!joinRequest.getPassword().equals(joinRequest.getPasswordCheck())) {
return "비밀번호가 일치하지 않습니다.";
}
//userService.join2(joinRequest);
userService.join(joinRequest);
return "회원가입 성공";
}
@PostMapping("/login")
public String login(@RequestBody LoginRequest loginRequest) {
User user = userService.login(loginRequest);
// 로그인 실패 시 에러 반환
if(user == null){
return "로그인 아이디 또는 비밀번호가 틀렸습니다.";
}
// 로그인 성공 시 Jwt Token 발급
String secretKey = "dGhpcy1pcy1hLXZlcnktc2VjdXJlLXNlY3JldC1rZXktMTIzNDU2Nzg5MCE=";
long expireTimeMs = 1000 * 60 * 60; // 60분
String jwtToken = JwtTokenUtil.createToken(user.getLoginId(), secretKey, expireTimeMs);
return jwtToken;
}
@GetMapping("/info")
public String userInfo(Authentication auth) {
User loginUser = userService.getLoginUserByLoginId(auth.getName());
return String.format("loginId: %s\\n nickname: %s\\n role: %s", loginUser.getLoginId(), loginUser.getNickname(), loginUser.getRole().name());
}
@GetMapping("/admin")
public String adminPage() {
return "관리자 페이지 접근 성공";
}
}
결과 확인
POSTMAN을 사용하여 회원가입 → 로그인 → 토큰 발급됨 → 헤더에 토큰 추가 후 info 등 확인
'프로젝트 > 하고 싶은거 다해보는 내 사이트' 카테고리의 다른 글
| 8. [게시판] Spring Boot/Mysql을 사용한 CRUD 게시판 만들기 | Repository 생성 및 JUnit 테스트 (0) | 2025.08.21 |
|---|---|
| 7. [게시판] Spring Boot/Mysql을 사용한 CRUD 게시판 만들기 | 게시판 엔티티 설계 및 구현 (0) | 2025.08.21 |
| 5. [로그인] SpringSecurity 로그인 구현하기 (5) | 2025.07.08 |
| 4. [로그인] 세션 로그인 구현하기 - Spring/SpringBoot (1) | 2025.07.07 |
| 3-1. [로그인/에러] 쿠키 로그인 시 500 에러 (whitelabel error page / 500) (0) | 2025.07.01 |