반응형

들어가며

  • OAuth2 Authorization Server란 OAuth2 인증 기능을 제공하는 서버를 말한다.
  • 아래에서는 client_credentials 방식으로 인증을 위해 Basic Authentication을 사용하고, 응답으로 내려온 Jwt 토큰을 사용하여 제한된 리소스에 접근하는 예제를 다룬다.

Grant Type

  • Authorization Code Grant
    • 이 방식은 웹 애플리케이션에서 주로 사용됩니다. 클라이언트 애플리케이션은 사용자를 인증서버로 리디렉션하고, 사용자가 로그인한 후에 받은 인가 코드를 교환하여 액세스 토큰과 리프레시 토큰을 얻습니다. 이 방식은 보안성이 높고, 장기적인 액세스를 위해 리프레시 토큰을 사용할 수 있습니다.
  • Implicit Grant
    • 이 방식은 주로 웹 애플리케이션에서 사용되며, 클라이언트가 인가 코드 없이 바로 액세스 토큰을 받는 방식입니다. 사용자의 브라우저를 통해 인증을 수행하며, 리프레시 토큰은 사용되지 않습니다. 보안상 취약점이 있을 수 있어서 최근에는 권장되지 않는 방식입니다.
  • Resource Owner Password Credentials (ROPC) Grant
    • 이 방식은 클라이언트 애플리케이션이 사용자의 아이디와 패스워드를 직접 수집하여 액세스 토큰을 얻는 방식입니다. 이 방식은 보안상 위험이 있으므로, 가능한 사용을 피하는 것이 좋습니다.
  • Client Credentials Grant
    • 이 방식은 클라이언트가 자신의 인증 정보를 사용하여 액세스 토큰을 요청하는 방식입니다. 사용자와 관련 없이 리소스 서버에 접근하는 것이 주 목적입니다. 예를 들어, 백엔드 서비스에서 다른 API를 호출할 때 사용될 수 있습니다
  • Refresh Token Grant
    • 이 방식은 기존에 얻은 리프레시 토큰을 사용하여 새로운 액세스 토큰을 얻는 방식입니다. 주로 장기적인 인증을 위해 사용됩니다.

예제

의존성 추가

implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")

SecurityConfig

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> {
                authorize.anyRequest().permitAll();
            })
            .oauth2ResourceServer((oauth2) -> {
                oauth2.jwt(Customizer.withDefaults());
            })
            .csrf(AbstractHttpConfigurer::disable)
            .apply(new OAuth2AuthorizationServerConfigurer());

        return http.build();
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
            .tokenEndpoint("/api/v1/oauth/token") // default : /oauth2/token
            .build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("tyler")
            .clientSecret(passwordEncoder.encode("1234"))
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // Basic Authentication
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // client_credentials
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofSeconds(60)).build())
            .build();

        return new InMemoryRegisteredClientRepository(client);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
        jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(List.of(
            new JwtTimestampValidator(Duration.ofSeconds(0)) // 디폴트 세팅으로는 expire 체크시 60초의 유예시간을 두고 체크하고 있는데, 유예시간을 제거하기 위한 설정
        )));
        return jwtDecoder;
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
        return new ImmutableJWKSet<>(new JWKSet(rsaKey));
    }

    @Bean
    public KeyPair keyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed("test-secure-seed-v1".getBytes(StandardCharsets.UTF_8)); // 서버 재실행할 경우 이전에 발급했던 Jwt 토큰을 계속 사용할 수 있도록 고정된 시드값 설정.
        keyPairGenerator.initialize(2048, secureRandom);
        return keyPairGenerator.generateKeyPair();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

인증 토큰 발급

  • 아래 Basic 뒤의 "dHlsZXI6MTIzNA==" 값은 "tyler:1234"를 Base64로 인코딩한 값
    • echo -n "tyler:1234" | base64
curl -i -X POST "http://localhost:8080/api/v1/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic dHlsZXI6MTIzNA==" \
-d "grant_type=client_credentials"
{
  "access_token": "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0eWxlciIsImF1ZCI6InR5bGVyIiwibmJmIjoxNjk4NTYxNTYyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE2OTg1NjE2MjIsImlhdCI6MTY5ODU2MTU2Mn0.qVZS8Fd8-UGdSbqc-mdhxF93CfhcHfuOhWov6pdrmy7H1RXrJjh2SJaB0bgsh0ytwG3wCoxfmtwIlPdi_OETgxORiNMwcm2k0P0WaqAVOuriSprnW6iw_x7VoXCtAiwm9n_45Hoxr1RexeDTS7Hql5Mt1xpAkwBt8NiSgH1yiv5RtluBrRkgxcg_l8VczB4OW28MScM9zq6EwReUAFS0DWJ1jf7TtMeWqwVhIqMxH2xk83qMwU9zw9e2sjstx6dUiz_-CTcLmTbJPMzkMny1_QPOLzsqkXCCw31NTqblUQ3chuwCFCVgj901RJVBuvZFhI943K9rbTG_Z1akuoSWIw",
  "token_type": "Bearer",
  "expires_in": 59
}

리소스 요청

curl -i "http://localhost:8080/api/v1/demo" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0eWxlciIsImF1ZCI6InR5bGVyIiwibmJmIjoxNjk4NTYxNTYyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE2OTg1NjE2MjIsImlhdCI6MTY5ODU2MTU2Mn0.qVZS8Fd8-UGdSbqc-mdhxF93CfhcHfuOhWov6pdrmy7H1RXrJjh2SJaB0bgsh0ytwG3wCoxfmtwIlPdi_OETgxORiNMwcm2k0P0WaqAVOuriSprnW6iw_x7VoXCtAiwm9n_45Hoxr1RexeDTS7Hql5Mt1xpAkwBt8NiSgH1yiv5RtluBrRkgxcg_l8VczB4OW28MScM9zq6EwReUAFS0DWJ1jf7TtMeWqwVhIqMxH2xk83qMwU9zw9e2sjstx6dUiz_-CTcLmTbJPMzkMny1_QPOLzsqkXCCw31NTqblUQ3chuwCFCVgj901RJVBuvZFhI943K9rbTG_Z1akuoSWIw"

 

반응형

+ Recent posts