> Hello World !!!

     

@syaku

Spring Security OAuth - Resource Server

https://github.com/syakuis/spring-security-oauth

코드를 함께 보면서 작업하시면 도움이 됩니다.

목차


라이브러리 의존성

ext.springBootVersion = "2.4.5"

implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server"
implementation "com.nimbusds:oauth2-oidc-sdk:7.1.1"
implementation "io.jsonwebtoken:jjwt:0.9.1"
testImplementation "org.springframework.security:spring-security-test"

설정 구성

@Slf4j
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    private final Environment environment;

    @Bean
    @Primary
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public SecurityExpressionHandler<FilterInvocation> expressionHandler() {
        return new DefaultWebSecurityExpressionHandler();
    }

    @Bean
    public AccessDecisionManager accessDecisionManager() {
        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(expressionHandler());
        List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(
            new RoleVoter(),
            new AuthenticatedVoter(),
            webExpressionVoter);
        return new AffirmativeBased(decisionVoters);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .httpBasic(AbstractHttpConfigurer::disable)
            .csrf(AbstractHttpConfigurer::disable)
            .formLogin(AbstractHttpConfigurer::disable)
            .sessionManagement(
                sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeRequests(
                authorize -> authorize
                    .accessDecisionManager(accessDecisionManager())
                    .antMatchers(HttpMethod.GET, accountApiPathPrefix + "/*/users/duplicate-username").permitAll()
                    .antMatchers(HttpMethod.POST, accountApiPathPrefix + "/*/users/signup").permitAll()
                    .antMatchers(HttpMethod.POST, accountApiPathPrefix + "/*/users/signin").permitAll()
//                    .antMatchers(accountApiPathPrefix + "/*/manager/**").hasAnyRole("MANAGER")
                    .anyRequest().authenticated())
            ;
    }
}

위 설정은 일반적인 스프링 시큐리티에서 사용하는 설정이다. 여기서 추가되어야 하는 것이 액세스 토큰 검증이다.

액세스 토큰 검증

액세스 토큰은 두가지 검증 방법이 있다. 하나는 Remote Opaque Token 검증이며 또 하나는 Local JWT 검증이다.

Remote Opaque Token 검증은 저장된 정보를 기반으로 검증하는 방식이고 Local JWT는 토큰 자체로만 검증하는 방식이다.

Local JWT 검증은 클라이언트 정보가 없고 다시 인증을 받기 전까지 비활성화된 토큰인지 파괴된 토큰인지 판단할 수 없는 단점이 있다.

민감한 데이터에 액세스할 경우 Remote Opaque Token 검증을 사용하고 그렇지 않을 경우 Local JWT 검증을 사용하면된다. 아무래도 Local JWT 검증이 인증 서버의 부하를 줄일 수 있다.

Remote Opaque Token 검증하기

application.yml

spring:
    security:
        oauth2:
          resourceserver:
            opaquetoken:
              client-id: 382a9f75fac6a50393f41a5620211b8b7c9b532165c7cec8be331be3f383035858fa62bf0638d2fa
              client-secret: KmNxazpPZnxTUy53W2Ap
              introspection-uri: http://localhost:5000/oauth/check_token

WebSecurityConfiguration.configure 설정 구성에서 추가로 설정하면 된다.

@Value("${spring.security.oauth2.resourceserver.opaquetoken.introspection-uri}")
private String introspectionUri;

@Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}")
private String clientId;

@Value("${spring.security.oauth2.resourceserver.opaquetoken.client-secret}")
private String clientSecret;

---

http.oauth2ResourceServer(oauth2ResourceServer -> {
    oauth2ResourceServer.opaqueToken(token -> token.introspectionUri(introspectionUri).introspectionClientCredentials(clientId, clientSecret));
});

Remote Opaque Token - 인증 정보 변경하기

인증 서버에서 검증된 액세스 토큰으로 인증된 정보를 변경해야할 경우가 있다.

자원 서버만의 권한 정책을 가지고 있어 ROLE을 재정의하거나 추가적인 사용자 정보가 있을 수 있다. OpaqueTokenIntrospector 인터페이스로 구현하면 된다.

public final class AuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {

    private final OpaqueTokenIntrospector delegate;
    private final UserDetailsService userDetailsService;

    public AuthoritiesOpaqueTokenIntrospector(UserDetailsService userDetailsService, String introspectionUri,
        String clientId,
        String clientSecret) {
        this.userDetailsService = userDetailsService;
        this.delegate = new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
    }

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        return new DefaultOAuth2AuthenticatedPrincipal(
            principal.getName(), principal.getAttributes(), extractAuthorities(principal));
    }

    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        Assert.notNull(principal, "principal cannot be null");

        List<GrantedAuthority> authorities = new ArrayList<>();

        getScopes(principal).ifPresent(authorities::addAll);

        getAuthorities(principal).ifPresent(authorities::addAll);

        return authorities;
    }

    private Optional<Collection<? extends GrantedAuthority>> getScopes(OAuth2AuthenticatedPrincipal principal) {
        List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);

        if (scopes == null) {
            return Optional.empty();
        }

        return Optional.of(scopes.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList()));
    }

    private Optional<Collection<? extends GrantedAuthority>> getAuthorities(OAuth2AuthenticatedPrincipal principal) {
        String username = principal.getAttribute("user_name");

        if (!StringUtils.hasText(username)) {
            return Optional.empty();
        }

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        return Optional.of(userDetails.getAuthorities());
    }
}

그리고 WebSecurityConfiguration.configure 설정을 재구성한다.

http.oauth2ResourceServer(oauth2ResourceServer -> {
    oauth2ResourceServer
        .opaqueToken(token -> {
            token.introspector(
                new AuthoritiesOpaqueTokenIntrospector(memberDetailsService, introspectionUri,
                    clientId, clientSecret));
        });
});

로컬 액세스 토큰 검증하기

application.yml

spring:
    security:
        oauth2:
          resourceserver:
            jwt:
              jwk-set-uri: http://localhost:5000/oauth2/v1/keys

WebSecurityConfiguration.configure 설정 구성에서 추가로 설정하면 된다.

http.oauth2ResourceServer(oauth2ResourceServer -> {
    oauth2ResourceServer.jwt(jwt -> jwt.decoder(jwtDecoder)
                        .jwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()));
});

JwtBearerTokenAuthenticationConverter 구현체를 참고하여 인증 정보를 변경할 수 있다.