JWT의 이론에 대해서는 이미 학습을 했고, Spring Boot에서 JWT를 활용하는 방법을 학습하겠다 🤓
[Spring] 인증/인가와 Session 방식과 JWT 방식의 차이
인증 / 인가인증 Authentication특정 인증 요청(로그인, 이메일 인증 등)사용자 식별을 하기 위해서 필요ex) 뉴스피드 프로젝트: 로그인인가 Authorization이미 인증된 사용자에게 특정 자원에 대한 접
mannakingdom.tistory.com
Spring 심화 개인 과제를 진행하기 위해 클론한 프로젝트가 JWT가 구현되어있어서 이 코드를 분석해보려고 한다🍴
GitHub - f-api/spring-advanced
Contribute to f-api/spring-advanced development by creating an account on GitHub.
github.com
⚠️ Spring Security를 사용하지 않는 JWT 활용 방법에 대해 다룬다‼️
1. Spring Boot에서 JWT 인증 구현 코드 분석
1️⃣ JWT 유틸 클래스 (JwtUtil.java)
JWT를 생성하고 검증하는 역할을 하는 클래스이다.
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil { ... }
- `@Component` ➡️ Bean 자동 등록
- `Slf4j(topic = "JwtUtil")` ➡️ 로그 남기기
📌 JWT 키 생성 및 Init
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
- `@Value("${jwt.secret.key}")`
➡️ `application.yml`(또는 `application.properties`)에서 설정한 JWT 키를 가져온다. - `Base64.getDecoder().decode(secretKey)`
➡️ 가져온 키 값(Base64 인코딩된 문자열)을 Base64 디코딩 - `Keys.hmacShaKeyFor(bytes)`
➡️ JWT 서명(Signatur)에 사용할 Key 생성
✨ 참고 사항
📍 SignatureAlgorithm.HS256
HS256: HMAC-SHA256 (Hash-based Message Authentication Code with SHA-256)
SHA-256의 출력 크기가 256비트(32바이트) 이므로, HMAC-SHA256을 사용할 때는 키 길이가 최소 256비트(32바이트) 이상이어야 함.
https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
📍 Keys.hmacShaKeyFor()
Keys.hmacShaKeyFor(byte[]) 메서드는 HMAC-SHA 계열 알고리즘(HMAC-SHA256, HMAC-SHA384, HMAC-SHA512 등)에서 사용할 키를 안전하게 생성하는 기능을 제공
키 길이가 충분하지 않으면 WeakKeyException을 발생시킴.
https://javadoc.io/doc/io.jsonwebtoken/jjwt-api/latest/io/jsonwebtoken/security/Keys.html#hmacShaKeyFor-byte:A-
➡️ JWT 서명을 위해 HS256을 사용할 경우 반드시 256비트(32바이트) 이상의 키를 Base64로 인코딩하여 사용해야 함.
📌 JWT 토큰 생성
public String createToken(Long userId, String email, UserRole userRole) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("userRole", userRole)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
- JWT 생성 시 claim으로 userId, email, userRole을 포함한다.
- `signWith(key, signatureAlgorithm)`
- ➡️ init에서 생성한 key로 HMAC SHA256 알고리즘을 이용해 서명(Signature) 추가
- ➡️ 검증이 필요할 때, 이 값으로 토큰 검증을 한다.
📌 JWT 토큰에서 "Bearer" 제거
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
throw new ServerException("Not Found Token");
}
- 헤더의 토큰에서 "Bearer"을 제거하여 실제 JWT 값만 추출한다.
- 헤더의 토큰 예시
➡️ key: Authorization, value: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiw(생략)
- 헤더의 토큰 예시
📌 JWT 토큰 검증 및 정보 추출
public Claims extractClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
- JWT의 서명을 검증한 후 Claims 객체를 반환
- Claims에는 createToken에서 claim으로 포함한 userId, email, userRole이 있다.
- 만료된 JWT라면 ExpiredJwtException이 발생한다.
2️⃣ JWT 필터 (JwtFilter.java)
JWT를 검사하고 사용자 정보를 설정하는 역할이다.
Filter 인터페이스를 implements하는 클래스
@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {
private final JwtUtil jwtUtil;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
...
}
📌 doFilter 메서드 분석
String url = httpRequest.getRequestURI();
if (url.startsWith("/auth")) {
chain.doFilter(request, response);
return;
}
- /auth로 시작되는 url(`/auth/signin`, `/auth/signup`)은 JWT 검사 없이 리턴
String bearerJwt = httpRequest.getHeader("Authorization");
if (bearerJwt == null) {
// 토큰이 없는 경우 400을 반환합니다.
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
return;
}
String jwt = jwtUtil.substringToken(bearerJwt);
- Authorization 헤더의 value를 `bearerJwt`에 저장
- Authorization 헤더가 없으면 `400 BAD REQUEST` 반환
- 저장한 토큰(`bearerJwt`)을 `substringToken` 메서드로 JWT 값만 추출해서 저장
// JWT 유효성 검사와 claims 추출
Claims claims = jwtUtil.extractClaims(jwt);
if (claims == null) {
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
return;
}
- JWT가 유효하지 않다면 `400 BAD REQUEST` 반환
httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
httpRequest.setAttribute("email", claims.get("email"));
httpRequest.setAttribute("userRole", claims.get("userRole"));
- HttpServletRequest 객체에 JWT에서 추출한 정보 저장
UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));
if (url.startsWith("/admin")) {
// 관리자 권한이 없는 경우 403을 반환합니다.
if (!UserRole.ADMIN.equals(userRole)) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다.");
return;
}
chain.doFilter(request, response);
return;
}
- /admin 경로 접근 시, `userRole`이 `UserRole.ADMIN`이 아니면 `403 FORBIDDEN` 반환
3️⃣ JWT 필터 등록 (FilterConfig.java)
@Configuration
@RequiredArgsConstructor
public class FilterConfig {
private final JwtUtil jwtUtil;
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilter() {
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtFilter(jwtUtil));
registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다.
return registrationBean;
}
}
- JwtFilter를 Spring Boot에 등록해서 모든 요청(`/*`)에 적용
- jwtUtil을 주입해서 JWT 검증이 가능하도록 설정
2. 커스텀 어노테이션으로 사용자 정보 주입 과정 분석
구현한 JWT 필터의 doFilter 메서드를 보면, HttpServletRequest에 사용자 정보를 저장하는 것을 알 수 있다.
이를 활용해서, 컨트롤러에서 `@Auth AuthUser authUser`를 파라미터로 사용자 정보를 가져오는 방식을 분석해보겠다.

1️⃣인증된 사용자 정보를 담는 DTO (AuthUset.java)
@Getter
public class AuthUser {
private final Long id;
private final String email;
private final UserRole userRole;
public AuthUser(Long id, String email, UserRole userRole) {
this.id = id;
this.email = email;
this.userRole = userRole;
}
}
- JWT에서 추출한 사용자 정보를 담는 DTO
2️⃣ 커스텀 어노테이션 @Auth (Auth.java)
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
}
- `@Target(ElementType.PARAMETER)` ➡️ 메서드의 매개변수에만 적용할 수 있도록 설정
- `@Retention(RetentionPolicy.RUNTIME)` ➡️ 런타임에도 어노테이션이 유지되도록 설정
- @Auth가 붙은 매개변수는 AuthUserArgumentResolver에서 자동으로 주입된다. (설명은 아래에서)
3️⃣ 사용자 정보 자동 주입 (AuthUserArgumentResolver.java)
AuthUserArgumentResolver는 HandlerMethodArgumentResolver를 implements하여, 컨트롤러의 메서드가 실행될 때 `@Auth AuthUser authUser` 매개변수를 자동으로 채워주는 역할을 한다.
public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { ... }
- HandlerMethodArgumentResolver는 Spring MVC에서 컨트롤러의 메서드 파라미터를 변환하는 인터페이스이다.
- 컨트롤러에서 특정 어노테이션이 붙은 매개변수를 자동으로 해석하고 변환하여 주입할 수 있도록 한다.
📌 supportsParameter()
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null;
boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class);
// @Auth 어노테이션과 AuthUser 타입이 함께 사용되지 않은 경우 예외 발생
if (hasAuthAnnotation != isAuthUserType) {
throw new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다.");
}
return hasAuthAnnotation;
}
- 매개변수가 @Auth와 AuthUser 타입이 함께 사용되는지 확인한다.
- 컨트롤러의 매개변수에 `@Auth`가 있는지 확인
- 매개변수 타입이 `AuthUser`인지 확인
- 둘 다 만족해야 `resolveArgument()` 실행
- 즉, 파라미터가 `@Auth AuthUser authUser`이 아니면 예외처리
📌 resolveArgument()
@Override
public Object resolveArgument(
@Nullable MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory
) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
// JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴
Long userId = (Long) request.getAttribute("userId");
String email = (String) request.getAttribute("email");
UserRole userRole = UserRole.of((String) request.getAttribute("userRole"));
return new AuthUser(userId, email, userRole);
}
- HttpServletRequest에서 JwtFilter가 저장한 userId, email, userRole을 가져옴
- 가져온 정보로 AuthUser 객체를 생성하여 반환
- 컨트롤러의 `@Auth AuthUser authUser`에 AuthUser 객체가 자동으로 주입
4️⃣ Argument Resolver 등록 (WebConfig.java)
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
// ArgumentResolver 등록
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthUserArgumentResolver());
}
}
3. 정리
- 클라이언트가 로그인하면 JWT를 받음 ➡️ `Authorization: Bearer <JWT>`
- 클라이언트가 API 요청 시 `Authorization: Bearer <JWT>` 헤더를 포함하여 보냄
- `JwtFilter`가 JWT를 검증하고 사용자 정보(userId, email, userRole)를 `HttpServletRequest`에 저장
- 컨트롤러에서 `@Auth AuthUser authUser`를 사용하면 자동으로 `AuthUserArgumentResolver`가 실행
- HttpServletRequest에서 사용자 정보를 가져와 AuthUser 객체로 생성 후 반환
- 별도의 인증 없이 `authUser.getId()` 같은 방식으로 사용자 정보 활용 가능
'TIL (Today I Learned)' 카테고리의 다른 글
| Spring Security를 활용한 JWT 인증 적용해보기 🖥️🔑 (0) | 2025.02.27 |
|---|---|
| Interceptor와 AOP 개념 정리 및 API 로깅 예시 ✍️ (0) | 2025.02.26 |
| git clone 후 build.gradle 빨간 줄❓ 해결 방법‼️ (0) | 2025.02.24 |
| [Spring] 인증/인가와 Session 방식과 JWT 방식의 차이 (0) | 2025.02.20 |
| [Spring] JPA 연관관계 매핑 확실하게 알아보기❗ (0) | 2025.02.14 |