Spring Security - OAuth2.0 사용 시 여러개의 리소스 서버를 지원하기
2024-12-10
학습 배경
각각 다른 OAuth 리소스 서버를 사용하는 서비스들에서 한 서버의 api를 호출할 수 있도록 변경하기 위해 학습하였다.
처음에는 OAuth별로 백엔드 서버를 구축해야되나 했는데, 뭔가 방법이 있을 것 같아서 찾아보게 되었다.
기존 설정
-
application.yaml
-
issuer-uri : jwt를 발급한 인증 서버의 url을 지정. Spring Security는 이 정보를 기반으로 jwt를 검증한다.
-
jwk-set-uri : jwt를 검증하기 위해 필요한 공개키를 조회할 때 사용하는 jwk url은 spring security에서 자동으로 매핑하며, 아래와 같이 직접 지정해서 사용할 수 있다.
1spring: 2 security: 3 oauth2: 4 resourceserver: 5 jwt: 6 issuer-uri: https://sso.server.com 7 jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs -
-
WebSecurityConfig.java
- oauth2ResourceServer method를 사용하여 Oauth2를 활성화 시키고 application.yaml에 설정된
issuer-uri를 통해 jwt를 검증한다. - 검증된 jwt는 CustomUserConverter를 통해 decode 되고 Authentication 객체를 생성한다.
- 이후 BearerTokenAuthenticationFilter에 의해 Authentication 정보가 SecurityContextHolder에 저장된다.(Spring Security가 호출)
1@Configuration 2@EnableWebSecurity 3@RequiredArgsConstructor 4public class WebSecurityConfig { 5 6 private final CustomUserConverter customUserConverter; 7 8 @Bean 9 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 10 http 11 .csrf(AbstractHttpConfigurer::disable) 12 .sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 13 .authorizeHttpRequests(request -> request.anyRequest().permitAll()) 14 .oauth2ResourceServer(oauth2Config -> oauth2Config 15 .jwt(jwtConfig -> jwtConfig.jwtAuthenticationConverter(customUserConverter)) 16 ); 17 18 return http.build(); 19 } 20} - oauth2ResourceServer method를 사용하여 Oauth2를 활성화 시키고 application.yaml에 설정된
위의 설정은 resource server가 1개일 때 사용 가능하다
변경 설정
-
application.yaml
1resourceserver: 2 issuer-uri-main: http://sso.server1.com 3 issuer-uri-sub: http://sso.server2.com위와 같이 issuer-uri를 각각 지정해준다
-
WebSecurityConfig.java
1@Configuration 2@EnableWebSecurity 3@RequiredArgsConstructor 4public class WebSecurityConfig { 5 6 private final CustomUserConverterMain customUserConverterMain; 7 private final CustomUserConverterSub customUserConverterSub; 8 9 @Value("${resourceserver.issuer-uri-main}") 10 private final String issuerUriMain; 11 @Value("${resourceserver.issuer-uri-sub}") 12 private final String issuerUriSub; 13 14 @Bean 15 public SecurityFilterChain filterChain(HttpSecurity http, CustomAuthenticationFilter customAuthenticationFilter) throws Exception { 16 http 17 .csrf(AbstractHttpConfigurer::disable) 18 .sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 19 .authorizeHttpRequests(request -> request.anyRequest().permitAll()) 20 .addFilterBefore(customAuthenticationFilter, BearerTokenAuthenticationFilter.class) 21 .oauth2ResourceServer(oauth2Config -> oauth2Config 22 .authenticationManagerResolver(customAuthenticationManagerResolver()) 23 ); 24 25 return http.build(); 26 } 27 28 @Bean 29 public AuthenticationManagerResolver<HttpServletRequest> customAuthenticationManagerResolver() { 30 return request -> { 31 String authServer = request.getHeader("Auth-Server"); 32 33 if (authServer.equals("main")) { 34 return authenticationManagerMain(issuerUriMain); 35 } else if (authServer.equals("sub")) { 36 String path = request.getRequestURI(); 37 if (path.contains("/product")) { 38 return authenticationManagerSub(issuerUriSub); 39 } 40 } 41 throw new AuthenticationServiceException(ExceptionCode.ISSUER_MISMATCHED.getMessage()); 42 }; 43 } 44 45 private AuthenticationManager authenticationManagerMain(String issuerLocation) { 46 JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerLocation); 47 JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwtDecoder); 48 provider.setJwtAuthenticationConverter(labIDEUserConverterMain); 49 50 return provider::authenticate; 51 } 52 53 private AuthenticationManager authenticationManagerSub(String issuerLocation) { 54 JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerLocation); 55 JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwtDecoder); 56 provider.setJwtAuthenticationConverter(labIDEUserConverterSub); 57 58 return provider::authenticate; 59 } 60}-
.jwt()대신.authenticationManagerResolver를 사용하여 AuthenticationManager를 직접 생성할 수 있다. -
예시는 Header를 사용하여 main, sub issuer를 구분하고, sub에서 사용할 수 있는 api를 path로 제한하였다.
-
customAuthenticationManagerResolver에서 던지는 AuthenticationServiceException 예외를 처리하기 위해.addFilterBefore로 customAuthenticationFilter를 등록했다.- 아래와 같이 사용하지 않는 경우 그대로 500에러를 반환하기 때문에 HttpServletResponse 객체에 직접 작업을 해주었다.
1 @Component 2 public class CusomAuthenticationFilter extends OncePerRequestFilter { 3 private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver; 4 5 public CusomAuthenticationFilter(AuthenticationManagerResolver<HttpServletRequest> resolver) { 6 this.authenticationManagerResolver = resolver; 7 } 8 9 @Override 10 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 11 throws IOException { 12 try { 13 authenticationManagerResolver.resolve(request); 14 15 filterChain.doFilter(request, response); 16 } catch (AuthenticationException ex) { 17 SecurityContextHolder.clearContext(); 18 String errorDescription = ex.getMessage(); 19 ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); 20 21 ErrorMessage errorMessage = ErrorMessage.builder() 22 .statusCode(HttpStatus.UNAUTHORIZED.value()) 23 .message(errorDescription) 24 .timestamp(LocalDateTime.now()) 25 .build(); 26 27 response.getWriter().write(mapper.writeValueAsString(errorMessage)); 28 response.setStatus(HttpStatus.UNAUTHORIZED.value()); 29 } 30 } 31 }
-
/end of Spring Security - OAuth2.0 사용 시 여러개의 리소스 서버를 지원하기
CONTENT LISTMERRI's DEVELOG
2024-11-27
2024-11-19
2024-10-23
2023-09-25