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}

위의 설정은 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
File Upload에 필요한 Request Annotation(@RequestParam, @RequestPart, @ModelAttribute, @RequestBody)
2024-11-27