스프링 시큐리티로 outh2 인증 서버를 구현한다.
outh2의 작동 원리에 대한 설명이 많이 생략되어 있기 때문에 이전 글을 이해해야 코드를 이해할 수 있다.
종속성(maven기준)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
http.exceptionHandling(exception ->
exception.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")
)
);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults());
http.authorizeHttpRequests(
authorizeRequests -> authorizeRequests.anyRequest().authenticated()
);
return http.build();
}
applyDefaultSecurity로 OAuth2인증서버의 기본 필터 구성을 한다.
oidc로 openId connect 프로토콜을 활성화 시킨다.
openId connect는 oauth2를 기반으로한 식별 계층이다.
따로 idtoken을 추가해 복호화를 통해 바로 사용자 정보를 얻을 수 있다.
exceptionHandling으로 인증이 안된 상태의 엔드포인트 접근은 “/login”엔드포인트로 리디렉션 시킨다.
로그인폼을 등록하고 모든 엔드포인트를 인증이 필요한 엔드포인트로 등록한다.
예제이기 때문에 간단한 UserDetails와 NoOpPasswordEncoder를 사용했다.
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
UserDetails u = User.withUsername("john")
.password("12345")
.authorities("read")
.build();
manager.createUser(u);
return manager;
}
클라이언트도 마찬가지로 유저처럼 등록을 해줘야한다.
UserDetails, UserDetailsService와 매우 유사한 구조를 가지고 있다.

@Configuration
public class ClientRegistrationConfig {
private static final String CLIENT_SECRET = "secret";
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
Set<String> redirectUris = getRedirectUris();
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client")
.clientSecret(passwordEncoder.encode(CLIENT_SECRET))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUris(uris->uris.addAll(redirectUris))
.scope(OidcScopes.OPENID)
.build();
//클라이언트가 추가로 필요하면 RegisteredClient 만든 후 리포지토리에 저장
return new InMemoryRegisteredClientRepository(client);
}
private static Set<String> getRedirectUris() {
Set<String> redirectUris = new HashSet<>();
redirectUris.add("http://localhost:8080/");
return redirectUris;
}
}
withId는 앱 내에서 client를 식별하는데 사용하는 id값을 지정한다.
clientAuthenticationMethod값으로 CLIENT_SECRET_BASIC을 사용했는데 이는 outh2 클라이언트 자격증명 인증에서 http basic을 사용한다는 의미이다.
authorizationGrantType으로는 AUTHORIZATION_CODE만 등록했기 때문에 승인 코드 그랜트 유형만 그랜트타입으로 허용된다.
redirectUris를 통해 리디렉션할 uri들을 등록한다, 여기 등록되지 않은 uri로 요청이 올 경우 거부한다.
scope는 요청 정보 범위를 설정하는데 OIDC를 활성화 했기 때문에 openid에 대한 스코프 까지만 허용하도록 설정하였다.
클라이언트를 builder로 만들었으면 클라이언트 리포지토리에 등록한다. 리포지토리로는 User와 마찬가지로 jpa리포지토리로 바꿀수도 있다.
권한 부여 서버가 jwt를 만들때 사용할 수 있는 키쌍이 필요하기 때문에 JWKSource를 등록한다.
권한부여 서버 구현 예제이기 때문에 프로그래밍 방식으로 키쌍을 만들어 등록한다.
@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
AuthorizationServerSettings을 통해 권한 부여 서버의 기본 엔드포인트를 등록한다.
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
유저가 클라이언트에 로그인 요청을 보내고 클라이언트가 권한 부여 서버로 리디렉션 시켜주는 과정을 시뮬레이션
클라이언트가 없기 때문에 클라이언트 처럼 행동해야한다.
PKCE가 기본적으로 활성화되어 있기 때문에
RegisteredClient에서 다음 코드를 추가하여 비활성화 시킨다.
.clientSettings(ClientSettings.builder()
.requireProofKey(false)
.build())
실제 클라이언트는 유저를 권한 부여 서버로 리디렉션할때 아래와 같은 쿼리를 추가한다

authorization code를 얻기 위한 기본 endpoint는 /oauth2/authorize이다.
권한 부여 서버의 로그인 폼으로 리디렉션 시켜주는 것을 확인할 수 있다.

로그인을 하면

클라이언트가 요청한 uri로 code쿼리를 포함해서 리디렉트 시켜준다.
code를 이용해서 클라이언트 대신 클라이언트의 자격증명을 사용해서 아래와 같은 POST요청을 하면
성공적으로 access_token과 oidc의 id_token을 얻을 수 있다.

RegisteredClient 빌더에 아래 메서드를 추가한다.
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
.build())
access_token이 jwt가 아니고 정보가 안 들어가 있기 때문에 훨씬 짧아졌다

access_token을 통해 정보를 얻기 위해서는 따라서 권한 부여 서버에 정보에 대한 요청을 해야된다.
이를 introspection 이라고 한다.
http basic으로 client:secret 을 설정하고 아래와 같은 엔드포인트로 token과 함께 POST요청을 보낸다.

토큰이 도난당했다는 것을 알게된다면 토큰 취소도 가능한데
위 요청에서 introspect대신 revoke를 엔드포인트로 설정한다.
추가로 다시 introspect를 확인하면 activate가 false가 된것을 확인할 수 있다.