제목: 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에서 권한을 부여하는 방법을 통해 문제를 해결할 수 있었습니다.
- SecurityContext 설정 확인: OAuth2 로그인 후 SecurityContext에 인증 정보가 제대로 설정되고 유지되는지 확인했습니다.
- JWT 필터 설정: JWT 토큰을 검증하고 SecurityContext에 권한을 부여하는 필터를 구성했습니다.
- Spring Security 설정 조정: Spring Security의 필터 체인에서 JWT 필터가 적절하게 작동하도록 설정을 조정했습니다.
이러한 해결 과정을 통해 애플리케이션의 인증 및 권한 부여를 보다 견고하게 구축할 수 있었습니다. 여러분의 프로젝트에서도 비슷한 문제가 발생했을 때, 이 글이 도움이 되기를 바랍니다.
'웹개발 > 오류해결' 카테고리의 다른 글
Spring Boot에서 Circular view path [favicon] 오류 해결하기 (0) | 2024.06.21 |
---|---|
docker-compose : Connection refused (0) | 2024.06.17 |
failed to solve: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount277479487/Dockerfile: no such file or directory 해결 (0) | 2024.06.17 |
스프링부트 404에러 해결 (0) | 2024.03.22 |
could not prepare statement [Table ""] (0) | 2024.03.22 |