우리는 지난시간에 로그인을 성공해서 승인코드를 발급받고 그 코드를 이용해서 인가사버와 통신해서 access_token 과 refresh_token 을 발급을 받았습니다 우리는 이 access_token 을 통해서 user 정보를 가져오는 코드를 같이 살펴볼 예정입니다
OAuth2LoginAuthenticationProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;
...
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
}
우리는 지난시간까지 이 윗부분을 활용해서 통신결과 access_token 을 가져오는 것까지 살펴보았습니다 그 아래 부터 살펴보겠습니다
1
2
3
4
5
6
7
8
9
10
11
12
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
1
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
이때 이 loadUser 를 통과하게 되는데 우리가 앞에 시큐리티 form 로그인에서 loadByUsername 하고 동일한 역활을 하게 됩니다
DefaultOAuth2UserService
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 class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
...
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
...
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
Map<String, Object> userAttributes = response.getBody();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
}
여기서 다시 인가서버와 통신할 준비를 하게 됩니다 이때는 userInfo 엔드포인트에 요청을 하게 됩니다
1
2
3
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
이때 RequestEntity 로 http 통신을 준비하게 됩니다 이때 userRequest 정보를 살펴보면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
userRequest = {OAuth2UserRequest}
clientName='{Spring-Oauth2-Authorizaion-client'}"
registrationId = "keycloak"
clientId = "Spring-Oauth2-Authorizaion-client"
clientSecret = "NIe2qftuPcclGWFiBFicEWoK5SfYs7ql"
clientAuthenticationMethod = {ClientAuthenticationMethod}
authorizationGrantType = {AuthorizationGrantType}
redirectUri = "http://localhost:8081/login/oauth2/code/keycloak"
scopes = {Collections$UnmodifiableSet} size = 2
providerDetails = {ClientRegistration$ProviderDetails}
clientName = "Spring-Oauth2-Authorizaion-client"
accessToken = {OAuth2AccessToken}
tokenType = {OAuth2AccessToken$TokenType}
tokenValue = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkcDdscEZZUFktZG84aTlVNlZwM3NxYjRhdHl1dHN3MURVUXRaWml3SV9zIn0.eyJleHAiOjE2OTY5NDQ1MDQsImlhdCI6MTY5Njk0NDIwNCwiYXV0aF90aW1lIjoxNjk2OTQzOTQ5LCJqdGkiOiJkYTUxNzNjMy0wMjBhLTQ1ZGQtYmMyZi1iNjgxZGE3NTczNjciLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL1NycGluZy1PYXV0aDItQXV0aG9yaXphaW9uLVByb2plY3QiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMzAyODMyMjgtZmEzNi00ZDg4LTgyYzMtYzk0OTRkYzQ0YmZkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiU3ByaW5nLU9hdXRoMi1BdXRob3JpemFpb24tY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjkzMjRmZGUzLWU1ODAtNDM2NS1hMjRhLWRjY2MyODhkMTgwNyIsImFjciI6IjAiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImRlZmF1bHQtcm9sZXMtc3JwaW5nLW9hdXRoMi1hdXRob3JpemFpb24tcHJvamVjdCJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjkzMjRmZGUzLWU1ODAtNDM2NS1hMjRhLWRjY2MyODhkMTgwNyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFt"
issuedAt = {Instant@7986} "2023-10-10T13:23:24.453337500Z"
expiresAt = {Instant@7987} "2023-10-10T13:28:24.453337500Z"
우리가 앞에서 인증이 완료된 ClientRegistraion 이 들어있고 access_token 으로 http 통신할 메세지를 만들게 됩니다
그 결과는 이렇게 http 메세지가 만들어지게 됩니다 주소는 http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/userinfo method 는 get 요청이 들어가게 되고 토큰은 헤더에 Bearer 방식으로 들어가게 됩니다
1
2
3
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
Map<String, Object> userAttributes = response.getBody();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
그리고 요청을 보내게 되면 아래와 같이 getResponse 를 통해서 아래와 같은 결과가 나오게 되는데
1
2
3
4
5
6
7
8
userAttributes = {LinkedHashMap} size = 7
"sub" -> "30283228-fa36-4d88-82c3-c9494dc44bfd"
"email_verified" -> {Boolean} false
"name" -> "time user"
"preferred_username" -> "user1"
"given_name" -> "time"
"family_name" -> "user"
"email" -> "user1@gmail.com"
boy 에서 이런 정보를 빼올 수 있습니다 이떄 sub 는 이 user 를 유일하게 식별할 수 있는 key 값이 되고 위에서 이 key 값으로 user 를 분리하게 됩니다
1
2
3
4
5
6
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
기본적으로는 authorities.add(new OAuth2UserAuthority(userAttributes));
통과시 ROLE_USER 가 만들어지는데 기 이유는 아래 OAuth2UserAuthority 에서 기본적으로 생성자 호출시 만들어지게 됩니다
OAuth2UserAuthority
1
2
3
4
5
6
7
8
public class OAuth2UserAuthority implements GrantedAuthority {
public OAuth2UserAuthority(Map<String, Object> attributes) {
this("ROLE_USER", attributes);
}
}
그리고 우리가 보낸 access_token 에서 scopes 를 분리해서 SCOPE_ 해서 권한을 더만들게 됩니다 우리는 앞에서 email , profile 를 했음으로 이에 대한 권한이 들어가게 됩니다
1
2
3
4
5
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
그래서 다시 return 을 받게 되는 이 부분에서는
1
2
3
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
아래와 같이 user 정보가 만들어지고 생겨나게 됩니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
oauth2User = {DefaultOAuth2User@8094} "Name: [30283228-fa36-4d88-82c3-c9494dc44bfd], Granted Authorities: [[ROLE_USER, SCOPE_email, SCOPE_profile]], User Attributes: [{sub=30283228-fa36-4d88-82c3-c9494dc44bfd, email_verified=false, name=time user, preferred_username=user1, given_name=time, family_name=user, email=user1@gmail.com}]"
authorities = {Collections$UnmodifiableSet@8100} size = 3
0 = {OAuth2UserAuthority@8060} "ROLE_USER"
1 = {SimpleGrantedAuthority@8103} "SCOPE_email"
2 = {SimpleGrantedAuthority@8104} "SCOPE_profile"
attributes = {Collections$UnmodifiableMap@8101} size = 7
"sub" -> "30283228-fa36-4d88-82c3-c9494dc44bfd"
"email_verified" -> {Boolean@8039} false
"name" -> "time user"
"preferred_username" -> "user1"
"given_name" -> "time"
"family_name" -> "user"
"email" -> "user1@gmail.com"
nameAttributeKey = "sub"
value = {byte[3]@8116} [115, 117, 98]
coder = 0
hash = 114240
1
2
3
4
5
6
7
8
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
1
2
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
이 결과를 통해서 mappedAuthorities 로 권한을 포함하는 객체를 만들게 되고 그 하단 OAuth2LoginAuthenticationToken 객체는 이제까지 모두 모인 값 ClientRegistrtaion , 요청때 사용한 정보 , 인증결과 user , 권한 그리고 access_token , refresh_token 을 포함한 거대한 객체 하나를 만들게 됩니다
OAuth2LoginAuthenticationFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager().authenticate(authenticationRequest);
OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
.convert(authenticationResult);
Assert.notNull(oauth2Authentication, "authentication result cannot be null");
oauth2Authentication.setDetails(authenticationDetails);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
이제 마지막으로
1
2
3
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager().authenticate(authenticationRequest);
부분이 넘어오게 됩니다 위레서 만든 거대한 객체의 결과는 authenticationResult 에 담기게 됩니다 그리고 시큐티티는 넘어오는 객체를 좀더 쉽게 사용하기 위해 아래의 메서드를 거치게 됩니다
1
OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter.convert(authenticationResult);
그럼 객체는 아래와 같이 변하게 되며 이 결과를 최종적으로 시큐리티 context 에 담아주어야 합니다
1
2
3
4
5
6
7
8
9
10
11
oauth2Authentication = {OAuth2AuthenticationToken@8500} "OAuth2AuthenticationToken [Principal=Name: [30283228-fa36-4d88-82c3-c9494dc44bfd], Granted Authorities: [[ROLE_USER, SCOPE_email, SCOPE_profile]], User Attributes: [{sub=30283228-fa36-4d88-82c3-c9494dc44bfd, email_verified=false, name=time user, preferred_username=user1, given_name=time, family_name=user, email=user1@gmail.com}], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER, SCOPE_email, SCOPE_profile]]"
principal = {DefaultOAuth2User@8378} "Name: [30283228-fa36-4d88-82c3-c9494dc44bfd], Granted Authorities: [[ROLE_USER, SCOPE_email, SCOPE_profile]], User Attributes: [{sub=30283228-fa36-4d88-82c3-c9494dc44bfd, email_verified=false, name=time user, preferred_username=user1, given_name=time, family_name=user, email=user1@gmail.com}]"
authorizedClientRegistrationId = "keycloak"
authorities = {Collections$UnmodifiableRandomAccessList@8505} size = 3
0 = {OAuth2UserAuthority@8388} "ROLE_USER"
1 = {SimpleGrantedAuthority@8389} "SCOPE_email"
2 = {SimpleGrantedAuthority@8390} "SCOPE_profile"
details = null
authenticated = true
AbstractAuthenticationProcessingFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
}
인증이 성공되면 인증결과를 AbstractAuthenticationProcessingFilter 곳에서 인증컨텍스트에 심게 됩니다
1
2
3
4
5
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
인증컨텍스트에 앞에서 넘어오는 결과를 심어두게 됩니다
우리는 정말로 길고긴 소스를 보았습니다 그 첫번째인 Authorization_code_grant 방식에서 로그인 성공시 코드를 발급받고 , 그 코드를 통해서 access_token 을 발급받고
이 access_token 을 통해서 userInfo 엔드포인트로 토큰을 전달해서 유저 정보를 전달하고 그 유저정보를 시큐리티 컨텍스트에 심는것으로 끝이나게 됩니다
여기 까지 우리는 인가서버 KeyClock 를 활용해서 제 3의 서버에서 user 정보를 받아오는 것을 처음부터 끝까지 쭈욱 공부를 했습니다 다음장에서는 여기서 넘어오는 user 를 우리가 어떻게 활용하는지에 대해서 알아보도록 하겠습니다