반응형
들어가며
- 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"
반응형
'Development > Spring Security' 카테고리의 다른 글
[Spring Security] Test (0) | 2023.10.29 |
---|---|
[Spring Security] WebSocketSecurity (0) | 2023.10.29 |
[Spring Security] Custom Authentication (Jwt) (0) | 2023.10.29 |
[Spring Security] OAuth2 Authentication (0) | 2023.10.29 |
[Spring Security] Basic Authentication (0) | 2023.10.29 |