제목: OAuth2 + JWT 코드에서 /api/admin/** 경로에 접근하지 못했던 이유와 해결 과정


문제 정의

Spring Boot 애플리케이션에서 OAuth2 인증과 JWT 기반의 권한 관리를 설정하면서, ROLE_ADMIN 권한을 가진 사용자가 /api/admin/** 경로에 접근할 수 없다는 문제가 발생했습니다. 이번 포스팅에서는 이 문제의 원인을 찾고, 해결하는 과정을 공유하고자 합니다.


문제 발생: /api/admin/** 경로에 접근하지 못하는 이유

OAuth2 인증 후 사용자가 ROLE_ADMIN 권한을 가지고 있음에도 불구하고, /api/admin/** 경로에 접근할 때 403 Forbidden 응답이 반환되었습니다. 다음은 이 문제를 해결하기 위한 디버깅 과정과 해결 방법에 대한 기록입니다.

1. 문제 증상

로그를 통해 문제를 확인한 결과, 사용자가 ROLE_ADMIN 권한을 가지고 있음에도 불구하고, Spring Security에서 해당 권한을 인식하지 못하고 있었습니다.

 

2024-06-23 19:55:27 [http-nio-8080-exec-10] DEBUG o.s.security.web.FilterChainProxy - Securing POST /api/admin/announcements
2024-06-23 19:55:27 [http-nio-8080-exec-9] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
2024-06-23 19:55:27 [http-nio-8080-exec-9] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8080/login

로그에서 AnonymousAuthenticationFilter가 활성화되었고, SecurityContext가 익명으로 설정된 것을 확인할 수 있었습니다.

2. 문제 원인 분석

다양한 점검을 통해 다음과 같은 문제 원인들을 발견했습니다:

  • SecurityContext가 올바르게 설정되지 않음: 인증된 사용자의 정보가 SecurityContext에 제대로 설정되지 않았기 때문에, 사용자가 익명으로 처리되었습니다.
  • OAuth2 인증 후 JWT 사용: OAuth2 인증 후, 프론트엔드와의 통신은 커스텀 JWT 토큰을 사용하고 있었습니다. 이로 인해 OAuth2 인증 정보가 실제 사용되지 않았습니다.
  • JWT 필터 설정의 불완전: JWT를 기반으로 한 권한 부여 로직이 SecurityContext에 제대로 반영되지 않았습니다.

문제 해결: /api/admin/** 경로 접근 문제

문제를 해결하기 위해 다음과 같은 단계들을 거쳤습니다:

1. SecurityContext 설정 확인

먼저, OAuth2 로그인 후 SecurityContext에 사용자 인증 정보가 올바르게 설정되고 유지되는지 확인했습니다. 이를 위해 AuthSuccessHandler에서 JWT 토큰을 생성하고, 이를 기반으로 SecurityContext를 설정하도록 수정했습니다.

 

//oauth2 로그인 성공 핸들러
@Component
public class AuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final UserService userService;

    public AuthSuccessHandler(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        if (authentication instanceof OAuth2AuthenticationToken) {
            User user = userService.findUserByOAuthToken((OAuth2AuthenticationToken) authentication);

            // 로그인 성공시 사용자 정보를 기반으로 JWT 토큰 생성
            String token = userService.generateTokenForUser(user);

        }
    }
}

2. JWT 필터 설정 및 권한 부여

JWT 토큰을 사용하여 프론트엔드와의 통신을 처리하면서, Spring Security가 이를 인식하고 적절한 권한을 부여하도록 JwtTokenFilter를 수정했습니다.

 

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserService userService;

    public JwtTokenFilter(JwtService jwtService, UserService userService) {
        this.jwtService = jwtService;
        this.userService = userService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        SecurityContext context = SecurityContextHolder.getContext();
		//스프링 시큐리티 콘텍스트에서 인증정보를 가지고 있지 않으면 인증정보 추가
        if (context.getAuthentication() == null) {
            String token = request.getHeader("Authorization");
            if (token != null && token.startsWith("Bearer ")) {
                token = token.substring(7); // "Bearer " 접두어 제거

                try {
                    // 토큰 유효성 검사 및 클레임 추출
                    Claims claims = jwtService.validateTokenAndExtractClaims(token);

                    // 클레임에서 사용자 ID 추출
                    String userId = claims.getSubject();
                    
                    // 사용자 정보 조회
                    User tokenUser = userService.findUserById(userId);

                    // 사용자 엔티티에서 권한 정보 추출
                    List<GrantedAuthority> authorities = tokenUser.getRoles().stream()
                            .map(User.Role::name)
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());

                    // 새로운 Authentication 객체 생성
                    Authentication authentication = new UsernamePasswordAuthenticationToken(
                            userId, null, authorities);

                    // SecurityContext에 Authentication 객체 저장
                    context.setAuthentication(authentication);
                } catch (Exception e) {
                    // 토큰 검증 실패 처리
                    SecurityContextHolder.clearContext();
                    logger.error("JWT 토큰 검증 실패: {}", e.getMessage());
                }
            }
        }

        filterChain.doFilter(request, response);
    }
}

3. Spring Security 설정 조정

JWT 필터가 작동하도록 Spring Security 설정을 수정했습니다. JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가하여, JWT가 유효한 경우 적절한 권한을 부여하도록 했습니다.

 

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(corsCustomizer()) // CORS 설정 적용
        .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
        .authorizeHttpRequests(authz -> authz
            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS 요청 허용
            .requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN") // '/api/admin' 경로는 ROLE_ADMIN 권한을 가진 사용자만 접근 허용
            .anyRequest().permitAll() // 그 외 모든 요청은 로그인 여부에 관계없이 허용
        )
        .oauth2Login(oauth2 -> oauth2
            .successHandler(authSuccessHandler) // 로그인 성공 핸들러
            .failureUrl("/loginFailure") // 로그인 실패 시 이동할 URL
            .userInfoEndpoint(userInfo ->
                userInfo.userService(customOAuth2UserService) // 사용자 정보를 로드하는 서비스
            )
        )
        //JwtTokenFilter가 UsernamePasswordAuthenticationFilter보다 먼저실행돼야함. (addFilterBefore)
        .addFilterBefore(new JwtTokenFilter(jwtService, userService), UsernamePasswordAuthenticationFilter.class);
    
    return http.build();
}

 

결론

이번 포스팅에서는 /api/admin/** 경로에 접근하지 못했던 이유를 분석하고, 해결하는 과정을 다루었습니다. OAuth2 인증 후 JWT 토큰을 사용하여 프론트엔드와 소통하고, Spring Security에서 권한을 부여하는 방법을 통해 문제를 해결할 수 있었습니다.

  1. SecurityContext 설정 확인: OAuth2 로그인 후 SecurityContext에 인증 정보가 제대로 설정되고 유지되는지 확인했습니다.
  2. JWT 필터 설정: JWT 토큰을 검증하고 SecurityContext에 권한을 부여하는 필터를 구성했습니다.
  3. Spring Security 설정 조정: Spring Security의 필터 체인에서 JWT 필터가 적절하게 작동하도록 설정을 조정했습니다.

이러한 해결 과정을 통해 애플리케이션의 인증 및 권한 부여를 보다 견고하게 구축할 수 있었습니다. 여러분의 프로젝트에서도 비슷한 문제가 발생했을 때, 이 글이 도움이 되기를 바랍니다.

+ Recent posts