0. 요구사항
- Filter와 ArgumentResolver 기반의 JWT 인증 구조를 Spring Security로 변경하기
1. Filter와 ArgumentResolver 기반의 JWT 인증 구조
- `JwtFilter` : 직접 request에 저장(setAttribute)
- `AuthUserArgumentResolver` : setAttribute 된 값을 꺼내서 @Auth, AuthUser에 주입
1. 클라이언트의 인증 요청 ; 헤더에 jwt가 담겨서 요청됨
2. `FilterConfig.java`
- FilterConfig 파일은 웹 어플리케이션에서 제일 먼저 실행되는 서블릿 필터 체인
- JwtFilter를 커스텀 필터로 등록
- Servlet 기반의 일반 필터를 FilterRegistrationBean으로 등록하여 모든 요청을 수동 처리
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilter() {
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtFilter(jwtUtil)); // 커스텀 JwtFilter 등록
registrationBean.addUrlPatterns("/*"); // 모든 url 요청에 대하여 필터 매핑
3. `JwtFilter.java` : JWT 인증 처리하는 커스텀 필터
- JWT 헤더에서 토큰 추출
String bearerJwt = httpRequest.getHeader("Authorization"); // 헤더에서 토큰 추출
String jwt = jwtUtil.substringToken(bearerJwt); // Bearer 접두사 제거
- JWT 유효성 검사와 클레임 추출
Claims claims = jwtUtil.extractClaims(jwt);
- 클레임에서 인증된 유저 정보 꺼내서 request에 저장 ( setAttribute() )
httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
httpRequest.setAttribute("email", claims.get("email"));
httpRequest.setAttribute("userRole", claims.get("userRole"));
httpRequest.setAttribute("nickname", nickname);
4. `AuthUserArgumentResolver.java` : 사용자 정보 주입
- `supportsParameter()`
- @Auth가 붙어있고, 파라미터 타입이 AuthUser일 때만 지원. 그렇지 않으면 예외 발생
- @Auth : 커스텀 인증 유저를 의미함
public boolean supportsParameter(MethodParameter parameter) {
boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null;
boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class);
if (hasAuthAnnotation != isAuthUserType) {
throw new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다.");
}
return hasAuthAnnotation;
}
- `resolveArgument()`
- request에서 꺼낸 값으로 AuthUser 객체를 생성 >> 이후에 컨트롤러의 파라미터에 주입할거임!!!!!
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
// JwtFilter 에서 설정한 값들을 꺼내서 AuthUser 객체 생성
Long userId = (Long) request.getAttribute("userId");
String email = (String) request.getAttribute("email");
UserRole userRole = UserRole.of((String) request.getAttribute("userRole"));
String nickname = (String) request.getAttribute("nickname");
return new AuthUser(userId, email, userRole, nickname);
5. `WebConfig.java` : ArgumentResolver 등록
- Spring MVC에 Resolver 등록한 덕분에, 컨트롤러의 파라미터에 유저 정보가 전달됨!
resolvers.add(new AuthUserArgumentResolver());
6. Controller 로직 실행
- Spring MVC는 컨트롤러의 각 파라미터를 하나씩 확인하면서, `supportsParameter()`가 true이면 그 Resolver의 `resolveArgument()`를 실행하여 파라미터 값을 만들어 주입함!
- authUser는 Resolver가 생성한 객체이며, 내부에 userId, email, userRole, nickname 와 같은 인증 정보를 포함하고 있음
[ UserController.java ]
@PutMapping("/users")
public void changePassword(@Auth AuthUser authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) {
userService.changePassword(authUser.getId(), userChangePasswordRequest);
}
2. Spring Security 기반의 JWT 인증 구조로 리팩토링
- Spring Security 기반의 인증 필터
- `SecurityContextHolder`에 인증 객체 저장
- 컨트롤러에서 `@AuthenticationPrincipal` 사용
1. 클라이언트의 인증 요청 ; 헤더에 jwt가 담겨서 요청됨
2. Spring Security의 SecurityFilterChain에서 모든 요청을 필터링함!
🐣 SecurityFilterChain의 기본 구조 및 순서
- SecurityContextPersistenceFilter
- LogoutFilter
- JwtAuthenticationFilter (내가 만든 커스텀 jwt 인증 필터)
- UsernamePasswordAuthenticationFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
[ application.properties ]
logging.level.org.springframework.security=DEBUG
// SecurityFilterChain 순서 디버깅 코드
2-2. `JwtAuthenticationFilter` 실행
- 내가 만든 커스텀 jwt 인증 필터 (기존의 JwtFilter와 비슷)
- 기존의 JwtFilter는 DispatcherServlet 이전의 서블릿 필터 체인이였다면, 이젠 SecurityFilterChain에 통합됨
- `SecurityConfig`에서 `UsernamePasswordAuthenticationFilter`보다 해당 필터가 먼저 실행되도록 설정함
http
.addFilterBefore(jwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
- JWT 헤더에서 토큰 추출
String bearerToken = request.getHeader("Authorization");
- JwtUtil로 Claims 파싱 및 검증
String jwt = jwtUtil.substringToken(bearerToken);
- Claims에서 사용자 정보 추출
Long userId = Long.parseLong(claims.getSubject());
String email = claims.get("email", String.class);
String nickname = claims.get("nickname", String.class);
String role = claims.get("userRole", String.class);
- CustomUserDetails 객체 생성 & UsernamePasswordAuthenticationToken 생성
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
new CustomUserDetails(userId, email, nickname, UserRole.valueOf(role)),
null,
Collections.singleton(() -> "ROLE_" + role)
);
- SecurityContextHolder에 인증 정보 저장🌟 >> 앞으로 요걸 기준으로 인증/인가 처리 될거야!
SecurityContextHolder.getContext().setAuthentication(authentication);
3. SecurityConfig 파일 생성 ; JWT 인증/인가 설정 파일
4. CustomUserDetails 파일 생성 ; 인증된 사용자 정보 파일
5. Controller 로직 실행
- SecurityContext를 기준으로 인증이 완료되면, 해당 요청은 컨트롤러로 넘어감
- `@AuthenticationPrincipal CustomUserDetails userDetails`를 통해 현재 로그인된 사용자 정보 자동 주입
@PostMapping("/todos")
public ResponseEntity<TodoSaveResponse> saveTodo(
@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @RequestBody TodoSaveRequest todoSaveRequest
) {
return ResponseEntity.ok(todoService.saveTodo(userDetails, todoSaveRequest));
}
6. Service 로직 실행
- `userDetails`를 통해 사용자 정보 접근 가능하므로, JPA 연관 관계 설정에 필요한 `User` 엔티티 만들어서 사용
public CommentSaveResponse saveComment(CustomUserDetails userDetails, long todoId, CommentSaveRequest commentSaveRequest) {
User user = User.from(userDetails);
... }
📮 POSTMAN을 통한 리팩토링 확인
1. 로그인 성공 (토큰 발행)

2. 로그인 안 한 상태로 "/todos" 접근 (JWT 없이 보호된 API에 접근) : 401 에러

- 해당 로그 확인 : SecurityContextHolder에 인증 정보 없이 접근하여서 anonymouns(익명)으로 처리됨

💫 신경 썼던 리팩토링 포인트
- JWT 기반의 인증 방식은 유지하며
- 인증 정보를 저장하고 전달하는 방식에 대해서만 리팩토링 진행하였음.
| 기존 구조 (커스텀 Filter + Resolver) | Spring Security 기반의 구조 | |
| 인증 처리 위치 | JwtFilter에서 직접 JWT 검증 | Security 필터 체인에 JwtAuthenticationFilter 등록 |
| 유저 정보 저장 | `request.setAttribute()`에 저장 | SecurityContextHolder에 저장 |
| 인증 객체 생성 | AuthUser 객체 생성 | CustomUserDetails 구현체 사용 |
| 컨트롤러에 주입 | @Auth + ArgumentResolver | @AuthenticationPrincipal |
🌕 회고
커스텀 필터와 Resolver를 활용한 기존의 인증 방식을 Spring Security로 리팩토링 하였다. 두 구조에서 느낀 차이점은, 인증 로직을 직접 구현하는 방식과 Spring Security라는 보안 프레임워크에 맡긴 방식의 차이였다. 직접 구현한 방식은 내가 로직의 흐름을 완벽히 통제할 수 있다는 장점이 있지만, 한편으로는 구조가 여기저기 흩어져있고 코드마다 중복된다는 단점도 있었다. 반면 Spring Security는 구조를 이해하고 나니 오히려 간편하고 통합적인 인증 로직이라는 것이 느껴졌다. 기존에는 필터에서 setAttribute()를 통해 request로 유저 정보를 넘기고, 컨트롤러에서는 Resolver로 유저 정보를 꺼내 쓰는 mvc 방식이었지만, 리팩토링 이후에는 JwtAuthenticationFilter에서 인증 객체를 SecurityContext에 저장하고, 컨트롤러에서는 @AuthenticationPrincipal로 간단히 사용자 정보를 주입받을 수 있게 되었다. 인증/인가/주입의 흐름이 하나의 통합된 체인으로 연결되었다는 점이 만족스러웠다. 덕분에 기능을 직접 구현하는 것보다 잘 설계된 프레임워크에 역할을 위임하고 잘 활용하는 것이 안정성이나 유지보수 측면에서 훨씬 효율적이라는 것을 체감할 수 있었다. 앞으로 인증/인가가 필요할때면 Spring Security를 각 프로젝트의 구조에 알맞게 잘 활용해야겠다는 생각이 든다.
'👩🏻💻Project' 카테고리의 다른 글
| 🔄️Spring Boot에서 재시도 로직 간단하게 구현하기 - Spring Retry 사용법 (0) | 2025.06.24 |
|---|---|
| 🚈실시간 기차 예매 서비스 프로젝트의 Docker 관련 트러블슈팅 (0) | 2025.05.26 |
| [JPA] JPQL을 QueryDSL로 리팩토링하기! (0) | 2025.05.10 |
| [JAVA/Spring] DeliSpring 프로젝트 정리본 및 트러블슈팅 (0) | 2025.04.29 |
| [JPA] ⏰타이머 과제 (1) | 2025.04.21 |