> Hello World !!!

     

@syaku

Spring Security OAuth - Authorization Server

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

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

목차


설치

빌더는 Gradle 플랫폼을 사용했고 전체 설정은 코드를 확인하고 아래 설정은 Authorization Server 에서 부가적으로 필요했던 의존성만 정리했다.

dependencies {
    testImplementation "org.springframework.boot:spring-boot-starter-webflux"

    testImplementation "org.springframework.cloud:spring-cloud-starter-contract-stub-runner"

    implementation "io.jsonwebtoken:jjwt:0.9.1"
		implementation "com.nimbusds:oauth2-oidc-sdk:7.1.1"

    // authorization-server
    implementation "org.springframework.security:spring-security-oauth2-core"
    implementation "org.springframework.security:spring-security-oauth2-jose"
    implementation "org.springframework.security:spring-security-oauth2-client"
    implementation "org.springframework.security.oauth:spring-security-oauth2:${springSecurityOAuth2Version}"
    implementation "org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:${springSecurityOAuth2Version}"
    implementation "org.springframework.security:spring-security-jwt:1.0.11.RELEASE"
    implementation "org.springframework.boot:spring-boot-starter-data-redis"
    
}
  • spring-boot-starter-webflux - 테스트시 http 요청이 필요할때 WebClientTest 를 사용한다.
  • spring-cloud-starter-contract-stub-runner - Mock 서버로 테스트하기 위해 사용한다.

보안 설정 구성

@Slf4j
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;

    @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;

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

    @Bean("authenticationManager")
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider());
    }

    @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
                    .requestMatchers(
                        new AntPathRequestMatcher("/oauth2/v1/token/keys", HttpMethod.GET.name()),
                        new AntPathRequestMatcher("/oauth/authorize", HttpMethod.POST.name())
                    )
                    .permitAll()
                    .anyRequest().authenticated())
            .exceptionHandling()
         ;
    }
}

위 설정은 자원 서버나 일반적인 스프링 시큐리티에서 사용하는 설정이다. 인증 서버에 계정 서비스와 클라이언트 등록 서비스도 포함되어 있기 때문에 필요한 설정이다.

인증 서비스에 계정을 등록하거나 관리하려면 권한이 필요하다. 클라이언트 등록도 마찬가지 이다. 만약 계정과 클라이언트 등록 서비스를 분리한다면 위 설정은 하지 않아도 된다.

위 설정은 해당 서버에 대한 설정이고 아래 설정이 액세스 토큰을 검증하기 위한 Remote Opaque Token 설정이다. 다른 페이지에서 Local 검증을 위한 설정 방법에 대해 알아본다.

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

JWT Converter 를 위한 비대칭키 만들기

Java KeyStore (JKS) 비대칭키는 java에서 제공하는 keytool로 생성할 수 있다.

$ keytool -genkeypair -alias syaku -keyalg RSA -keypass syaku@1234 -keystore authorization.jks -storepass syaku@pass1234 -dname "CN=syaku, OU=preson, O=authorization, L=seoul, ST=seoul, C=KR"
  • storepass - 키 저장소에 액세스하는 데 사용됩니다.
  • keypass - 특정 키 쌍의 개인 키에 액세스하는 데 사용됩니다.

인증 서버 설정 구성

@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;
    private final ClientDetailsService clientDetailsService;
    private final TokenStore tokenStore;
    private final TokenEnhancer tokenEnhancer;
    private final JwtAccessTokenConverter jwtAccessTokenConverter;

    @Bean
    @Primary
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore);
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setTokenEnhancer(tokenEnhancer);
        return defaultTokenServices;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer, jwtAccessTokenConverter));

        endpoints
            .tokenStore(tokenStore)
            .reuseRefreshTokens(true)
            .tokenEnhancer(tokenEnhancerChain)
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService)
        ;
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()")
        ;
    }
}

기본 설정

  • @EnableAuthorizationServer - 기본 인증 서버 설정을 자동으로 구성한다.
  • AuthorizationServerConfigurerAdapter - 클래스를 상속하여 설정을 재 정의한다.

참조 클래스

  • AuthenticationManager - Spring security 인증 처리를 한다.
  • UserDetailsService - 사용자 정보를 조회한다.
  • ClientDetailsService - 클라이언트 정보를 조회한다.
  • TokenStore - 액세스 토큰이 저장되는 저장소이다. 자주 데이터를 읽기때문에 Redis 같은 메모리 저장소를 주로 사용한다. 기본적으로 아래의 저장소를 제공한다.
    • InMemory - 메모리에 저장하며 테스트 작업시 사용해야 한다.
    • Jdbc - 데이터베이스를 저장소로 사용한다.
    • Redis - Redis를 저장소로 사용한다.
  • TokenEnhancer - JWT의 Payload를 커스텀할 수 있다. 아래 "JWT Payload 변경하기" 참고한다.
  • JwtAccessTokenConverter - JWT의 액세스 토큰으로 변경하는 컨버터이다.
  • AuthorizationServerTokenServices - 액세스 토큰을 발급하기 위한 서비스이다. OAuth 2.0 workflow가 구현된 서비스라고 보면 된다. 여러 인증 방식을 각각 구현한 것이 아니고 비슷한 것끼리 섞어 구현해서 세부적인 커스텀에 어려움이 있다.
  • AuthorizationServerEndpointsConfigurer - 액세스 토큰을 발급하기 위한 설정 구성이다.
  • ClientDetailsServiceConfigurer - 클라이언트 등록을 위한 설정 구성이다. 아래 두가지 방식을 지원한다.
    • InMemory - 메모리에 저장하며 테스트 작업시 사용해야 한다.
    • Jdbc - 데이터베이스를 저장소로 사용한다.
    • CRUD만 작업하면 되므로 직접 개발도 어려지 않다.
  • AuthorizationServerSecurityConfigurer - 인증 서버에 대한 보안 설정 구성이다.

Client Registation

  • ClientSecret 값은 임의의 값이며 암호화되어 저장되므로 잃어버리면 다시 찾을 수 없고 재발급 받아야 한다.
  • ClientSecret 값은 기밀성을 보장해야한다.꼭 안정하게 보관할 수 있는 서버 프로그램과 같은 곳에 사용해야 한다. 코드가 유출되어 난독화로 오픈되지 않도록 주의하고 유출될 경우 재발급 받아야 한다.

AuthorizationServerEndpointsConfigurer

TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer, jwtAccessTokenConverter));

TokenEnhancerChain 클래스에 토큰 체인 작업을 순서대로 정의하면 된다.

AuthorizationServerSecurityConfigurer

security
      .tokenKeyAccess("permitAll()")
    .checkTokenAccess("isAuthenticated()")
;

보안 설정에서 tokenKeyAccess 는 액세스 토큰을 발급 받기 위한 API에 대한 접근 권한 설정이다. 모두에게 오픈하였다.

checkTokenAccess 는 발급 받은 액세스 토큰을 검증하기 위한 API에 대한 접근 권한 설정이다. API 응답에 액세스 토큰 정보가 포함되어있어 접근 권한은 인증된 사용자만으로 설정했다.

JWT Payload 변경하기

JWT의 Payload에 추가적인 정보를 담거나 구조를 변경하기 위해 다음과 같이 할 수 있다.

@Slf4j
@Service
public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        if (authentication.getPrincipal() instanceof OAuth2UserDetails) {
            OAuth2UserDetails oAuth2UserDetails = (OAuth2UserDetails) authentication.getPrincipal();

            Map<String, Object> additionalInfo = new HashMap<>();
            additionalInfo.put("name", oAuth2UserDetails.getName());
            additionalInfo.put("uid", oAuth2UserDetails.getUid());
            additionalInfo.put("additionalInformation", oAuth2UserDetails.getAdditionalInformation());

            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);

            return accessToken;
        }
        return accessToken;
    }
}

TokenEnhancer 인터페이스로 구현 클래스를 작성한다. 원하는 구조로 변경할 수 있다. 너무 많은 데이터를 담지 않는 것이 좋고 일반화된 정보를 담는 것이 중요하다.

RedisTokenStore 사용 설정

redis 사용에 따르 프로퍼티 설정이 필요하다.

spring:
	redis:
    host: localhost
    port: 6379
    password: 1234

Redis 설정 구성

@RequiredArgsConstructor
@Configuration
class RedisConfiguration {

    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisProperties.getHost(),
            redisProperties.getPort());
        configuration.setPassword(RedisPassword.of(redisProperties.getPassword()));
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setDefaultSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }
}

RedisConnectionFactory Redis에 연결하기 위한 설정이다. RedisTemplate 은 redis를 사용하기 위한 빈 설정이고 필요하지 않을 경우 설정하지 않아도 된다. spring security와 관련은 없다.

Local JWT 검증을 위한 jwtSetUri 구현

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/oauth2/v1")
class OAuthTokenRestController {
    private final JWKSet jwkSet;

    @GetMapping(value = "/keys", produces = MediaType.APPLICATION_JSON_VALUE)
    public String keys() {
        return this.jwkSet.toString();
    }
}

누구나 접속할 수 있게 권한을 오픈한다. (permitAll)