우리는 지난시간에 KeyClock 의 연동과 간단한 용어 정리 그리고 개념에 대해서 공부를 해보았다 용어는 앞으로 반복 그리고 새로운것이 계속 나올것이기에 그때마다 정리를 하도록 하겠습니다 오늘부터는 어떻게 Spring - Security 가 KeyClocak 을 연동하고 최종적으로 사용자 자원을 가져오는지에 대해서 알아볼려고 합니다
ClientRegitsrtaion
시작하기전에 새로운 용어 ClientRegitsrtaion 에 대해서 알아보도록 하겠습니다 클라이언트에 Oauth2 정보 또는 클라이언트 정보를 저장소입니다 이곳에 우리는 우리 클라이언트 정보와 연동할려고 하는 KeyClocak 의 정보를 심어두고 시큐리티는 이 ClientRegitsrtaion 정보를 바탕으로 필요한 요청정보를 만들어 인가서버와 통신을 하게 됩니다
SecuirtyConfig 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity.authorizeRequests().anyRequest().authenticated();
httpSecurity.oauth2Login();
return httpSecurity.build();
}
}
OAuth2ClientRegistrationRepositoryConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
class OAuth2ClientRegistrationRepositoryConfiguration {
@Bean
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
OAuth2ClientRegistrationRepositoryConfiguration 는 우리가 앞에서 application.properties 에서 정의한 keyClocak 연동값을 시큐리티에 저장을 하는 클래스 입니다 이때 returnType InMemoryClientRegistrationRepository 있으며 즉 연동 결과를 메모리에 저장하는 로직입니다 그리고 파라미터로 넘어오는 OAuth2ClientProperties properties 값들은 아래와 같은데
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
result = {OAuth2ClientProperties}
provider = {HashMap} size = 1
"keycloak" -> {OAuth2ClientProperties$Provider}
key = "keycloak"
value = {OAuth2ClientProperties$Provider}
authorizationUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/auth"
tokenUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/token"
userInfoUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/userinfo"
userInfoAuthenticationMethod = null
userNameAttribute = "preferred_username"
jwkSetUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/certs"
issuerUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project"
registration = {HashMap} size = 1
"keycloak" -> {OAuth2ClientProperties$Registration}
key = "keycloak"
value = {OAuth2ClientProperties$Registration}
provider = null
clientId = "Spring-Oauth2-Authorizaion-client"
clientSecret = "NIe2qftuPcclGWFiBFicEWoK5SfYs7ql"
clientAuthenticationMethod = "client_secret_post"
authorizationGrantType = "authorization_code"
redirectUri = "http://localhost:8081/login/oauth2/code/keycloak"
scope = {LinkedHashSet@5107} size = 2
clientName = "Spring-Oauth2-Authorizaion-client"
우리가 앞에서 선언한 값들이 의존에 의해서 넘어오는것을 확인할 수 있습니다
OAuth2ClientPropertiesRegistrationAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class OAuth2ClientPropertiesRegistrationAdapter {
...
private static ClientRegistration getClientRegistration(String registrationId,
OAuth2ClientProperties.Registration properties, Map<String, Provider> providers) {
Builder builder = getBuilderFromIssuerIfPossible(registrationId, properties.getProvider(), providers);
if (builder == null) {
builder = getBuilder(registrationId, properties.getProvider(), providers);
}
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties::getClientId).to(builder::clientId);
map.from(properties::getClientSecret).to(builder::clientSecret);
map.from(properties::getClientAuthenticationMethod).as(ClientAuthenticationMethod::new)
.to(builder::clientAuthenticationMethod);
map.from(properties::getAuthorizationGrantType).as(AuthorizationGrantType::new)
.to(builder::authorizationGrantType);
map.from(properties::getRedirectUri).to(builder::redirectUri);
map.from(properties::getScope).as(StringUtils::toStringArray).to(builder::scope);
map.from(properties::getClientName).to(builder::clientName);
return builder.build();
}
}
여기서 보면 OAuth2ClientPropertiesRegistrationAdapter 클래스는 앞에서 넘어오는 properties 값들을 넣어주는데 사실
Builder builder = getBuilderFromIssuerIfPossible(registrationId, properties.getProvider(), providers); 만 호출이 되더라도
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
result = {ClientRegistration$Builder}
registrationId = "keycloak"
clientId = null
clientSecret = null
clientAuthenticationMethod = {ClientAuthenticationMethod}
authorizationGrantType = {AuthorizationGrantType}
redirectUri = "{baseUrl}/{action}/oauth2/code/{registrationId}"
scopes = null
authorizationUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/auth"
tokenUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/token"
userInfoUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/userinfo"
userInfoAuthenticationMethod = {AuthenticationMethod}
userNameAttributeName = "preferred_username"
jwkSetUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/certs"
issuerUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project"
configurationMetadata = {LinkedHashMap@5396} size = 52
clientName = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project"
이미 필요한 값들은 다 들어가 있는것을 알 수 있다 현재 이곳은 Client 그리고 Provider 에 대한 데이터는 이미 다들어가 있는것을 확인할 수 있다 이런 원리는 사실 우리가 앞에서 본 Properties 값 중에
spring.security.oauth2.client.provider.keycloak.issuerUri=http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project
우리 지난시간에 issuer 는 이는 ID 토큰 액세스 토큰을 발급하는 주체를 뜻하며 현재 클라이언트가 사용하는 인증서버의 엔드포인트입니다 이렇게 설명을 했습니다 즉 이 하나의 uri 만 가지고 있어도 시큐리티는 우리가 다른 설정 authorizationUri , tokenUri 등 설정을 해줄 필요가 없습니다 시큐리티는 이 주소만 읽고도 Provier 가 필요한 모든 값들을 읽어 올 수 있습니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Builder getBuilderFromIssuerIfPossible(String registrationId, String configuredProviderId,
Map<String, Provider> providers) {
String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId;
if (providers.containsKey(providerId)) {
Provider provider = providers.get(providerId);
String issuer = provider.getIssuerUri();
if (issuer != null) {
Builder builder = ClientRegistrations.fromIssuerLocation(issuer).registrationId(registrationId);
return getBuilder(builder, provider);
}
}
return null;
}
이 부분에서 provider 값과 issuer 값을 분리해내서 fromIssuerLocation 을 호출하게 됩니다
1
2
3
4
5
6
public static ClientRegistration.Builder fromIssuerLocation(String issuer) {
Assert.hasText(issuer, "issuer cannot be empty");
URI uri = URI.create(issuer);
return getBuilder(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri));
}
여기서 url http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project 을 가리키는데 이는 계속해서 앞에서 말하는 issurUri 를 가져오게 되는것이고
getBuilder 호출시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static Supplier<ClientRegistration.Builder> oidc(URI issuer) {
URI uri = UriComponentsBuilder.fromUri(issuer)
.replacePath(issuer.getPath() + OIDC_METADATA_PATH)
.build(Collections.emptyMap());
return () -> {
RequestEntity<Void> request = RequestEntity.get(uri).build();
Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
if (metadata.getUserInfoEndpointURI() != null) {
builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
}
return builder;
};
}
이 부분으로 넘어오게 되는데 이곳에서 RequestEntity<Void> request = RequestEntity.get(uri).build();
요청정보를 만들고 Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
요청이 들어가고 그것을 body 를 추출해서 Map 타입의 설정정보를 만들어내게 되는데
configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
configuration = {LinkedHashMap} size = 53
"issuer" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project"
"authorization_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/auth"
"token_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/token"
"introspection_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/token/introspect"
"userinfo_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/userinfo"
"end_session_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/logout"
"jwks_uri" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/certs"
"check_session_iframe" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/login-status-iframe.html"
"registration_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/clients-registrations/openid-connect"
"revocation_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/revoke"
"device_authorization_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/auth/device"
"backchannel_authentication_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/ext/ciba/auth"
"pushed_authorization_request_endpoint" -> "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/ext/par/request"
이런 configuration 정보를 만들어내개 됩니다
1
2
3
4
5
6
7
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
if (metadata.getUserInfoEndpointURI() != null) {
builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
}
withProviderConfiguration 부분을 통해서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static ClientRegistration.Builder withProviderConfiguration(AuthorizationServerMetadata metadata,
String issuer) {
String metadataIssuer = metadata.getIssuer().getValue();
Assert.state(issuer.equals(metadataIssuer),
() -> "The Issuer \"" + metadataIssuer + "\" provided in the configuration metadata did "
+ "not match the requested issuer \"" + issuer + "\"");
String name = URI.create(issuer).getHost();
ClientAuthenticationMethod method = getClientAuthenticationMethod(metadata.getTokenEndpointAuthMethods());
Map<String, Object> configurationMetadata = new LinkedHashMap<>(metadata.toJSONObject());
return ClientRegistration.withRegistrationId(name)
.userNameAttributeName(IdTokenClaimNames.SUB)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(method)
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
.authorizationUri((metadata.getAuthorizationEndpointURI() != null) ? metadata.getAuthorizationEndpointURI().toASCIIString() : null)
.providerConfigurationMetadata(configurationMetadata)
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.issuerUri(issuer)
.clientName(issuer);
}
이곳에서 나머지 필요한 모든 값들을 설정을 하게 됩니다 이렇게 해서 ClientRegitsrtaion 이 하나 만들어지게 됩니다 그럼 사실 우리는 앞에서
1
2
3
4
5
6
7
8
9
10
11
12
13
server.port=8081
spring.security.oauth2.client.registration.keycloak.clientId=Spring-Oauth2-Authorizaion-client
spring.security.oauth2.client.registration.keycloak.clientSecret=NIe2qftuPcclGWFiBFicEWoK5SfYs7ql
spring.security.oauth2.client.registration.keycloak.redirectUri=http://localhost:8081/login/oauth2/code/keycloak
spring.security.oauth2.client.registration.keycloak.scope=email,profile
spring.security.oauth2.client.registration.keycloak.clientName=Spring-Oauth2-Authorizaion-client
spring.security.oauth2.client.registration.keycloak.authorizationGrantType=authorization_code
spring.security.oauth2.client.registration.keycloak.clientAuthenticationMethod=client_secret_post
spring.security.oauth2.client.provider.keycloak.issuerUri=http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project
이렇게 client 부분은 client 설정이기때문에 고정 Provider 부분은 사실상 issuerUri 있으면 연동이 되는것이다 그래서 주석잡고나 또는 지우고 진행을 해도 알아서 연동을 하게 됩니다 실제 모든 정보는 http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project 와 통신해서 전부 가져올 수 있기 때문입니다
오늘은 기본적으로 properties 에 등록된 정보를 어떻게 시큐리티가 가져와서 연동을 하게 되는지에 대해서 알아보았습니다 앞으로는 이런식으로 계속 글이 쓰여질 예정입니다