Spring Security OAuth2를 이용한 로그인 구현 - Spring boot OAuth2 인증 살펴보기

목차

참고

OAuth2LoginAuthenticationFilter

OAuth2 인증을 진행하기 위한 Filter

OAuth2 인증 방식도 UsernamePassword 인증과 같이 인증을 진행하기 위한 Filter 가 존재한다. OAuth2LoginAuthenticationFilter 에서는 Authentication Server 로부터 Authorization Code 를 받은 후 Acess TokenReflesh Token 을 받기 위한 과정을 진행한다.

  • ClientRegistrationRepository
    • ClientRegistration 정보를 저장 및 가져오기 위한 Class
  • OAuth2AuthorizedClient
    • 인증된 Client 정보를 관리하기 위한 Class
    • Access Token, Reflesh Token, ClientRegistration 정보를 관리하는 Class
  • OAuth2AuthorizedClientRepository
    • OAuth2AuthorizedClient 정보를 저장 및 가져오기 위한 Class
    • 인증이 진행중인 Client 정보를 가져온다.
    • 기본 구현체는 HttpSessionOAuth2AuthorizedClientRepository
  • OAuth2AuthorizedClientService
    • Application Level 에서 OAuth2AuthorizedClient 정보를 가져오기 위한 Class
    • 인증이 완료된 Client 정보를 저장소 에서 가져올 때 사용한다.
    • 메모리에 OAuth2AuthorizedClient 객체를 저장하는 InMemoryOAuth2AuthorizedClientService
    • 데이터 베이스에 OAuth2AuthorizedClient 객체를 저장하는 JdbcOAuth2AuthorizedClientService
  • AuthorizationRequestRepository
    • 인증 요청에서 인증 응답을 받을때 까지 OAuth2AuthorizationRequest 의 지속성을 보장하기 위한 Class
    • 기본 구현체는 Session 에 저장하는 HttpSessionOAuth2AuthorizationRequestRepository
  • OAuth2AuthorizationRequestResolver
    • registrationId 와 HttpServletRequest 를 OAuth2AuthorizationRequest 객체를 생성하기 위한 Class
    • 기본 구현체는 DefaultOAuth2AuthorizationRequestResolver
  • OAuth2AccessTokenResponseClient
    • Authorization Code 를 Access Token 으로 교환하는데 사용하는 Class
    • 기본 구현체는 DefaultAuthorizationCodeTokenResponseClient

인증 과정

  1. AuthorizationRequestRepository 객체에서 OAuth2AuthorizationRequest 정보를 가져온 후 해당 객체를 지운다.
  2. ClientRegistration, OAuth2AuthorizationRequest, OAuth2AuthorizationResponse 객체를 이용해 OAuth2LoginAuthenticationToken 객체를 생성한다.
  3. AuthenticationProvider 객체를 이용해 인증을 진행한 후 새로운 OAuth2LoginAuthenticationToken 객체를 반환 받는다.
  4. Access Token 정보와 Reflesh Token 정보 ClientRegistration 정보를 담는 OAuth2AuthorizedClient 객체를 생성
  5. OAuth2AuthorizedClientRepository 에 성성된 OAuth2AuthorizedClient 객체를 저장한다.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {

// Request 객체로부터 Parameter 를 가져온다.
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils
.toMultiMap(request.getParameterMap());

// Request Parameter 에 code, state 가 있는지 확인한다.
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}

// AuthorizationRequestRepository 객체에서 state 정보에 해당하는 OAuth2AuthorizationRequest 객체를 가져온후 해당 값을 AuthorizationRequestRepository 에서 지운다.
// AuthorizationRequestRepository 객체 기본 구현체는 HttpSessionOAuth2AuthorizationRequestRepository
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
.removeAuthorizationRequest(request, response);

if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}

// Registration 정보를 가져온다. ex) google, github, facebook, ...
String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);

// Registration 에 맞는 client_id 와 client_secret 정보를 가져와 ClientRegistration 객체를 생성한다.
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
"Client Registration not found with Id: " + registrationId, null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}

// @formatter:off
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
// @formatter:on

// Request Parameter 로부터 code, state, redirectUri 을 가져와 OAuth2AuthorizationResponse 객체를 생성한다.
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params,
redirectUri);
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);

// ClientRegistration 객체와 OAuth2AuthorizationRequest 객체와 OAuth2AuthorizationResponse 객체를 이용해 OAuth2LoginAuthenticationToken 객체를 만든다.
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);

// AuthenticationProvider 객체를 이용해 인증을 진행한다. 인증시 ClientRegistration 정보와 Authorization_code 정보를 사용한다.
// 인증을 진행한 후 Acess Token, Refresh Token 정보가 담긴 Authentication(Token) 객체를 가져온다.
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager()
.authenticate(authenticationRequest);

// OAuth2LoginAuthenticationToken 객체를 OAuth2AuthenticationToken 객체로 변경한다.
OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
.convert(authenticationResult);

Assert.notNull(oauth2Authentication, "authentication result cannot be null");
oauth2Authentication.setDetails(authenticationDetails);

// Access Token 정보와 Reflesh Token 정보 ClientRegistration 정보를 담는 OAuth2AuthorizedClient 객체를 생성한다.
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());

// OAuth2AuthorizedClientRepository 에 성성된 OAuth2AuthorizedClient 객체를 저장한다.
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
}

OAuth2LoginAuthenticationProvider

OAuth 방식을 이용해 인증을 진행하고 Authentication(인증) 객체를 생성하는 Provider

  • OAuth2AuthorizationExchange
    • OAuth2AuthorizationRequest 객체와 OAuth2AuthorizationResponse 객체를 관리하는 객체
  • ClientRegistration
    • client_id, client_secret, scope 정보를 관리하는 객체
  • OAuth2LoginAuthenticationToken
    • OAuth2 인증시 사용하는 Authentication 객체
  • OAuth2AuthorizationCodeAuthenticationProvider
    • Authorization Code 를 이용해 인증을 진행하는 Provider
    • Authorization Code 를 이용해 Access Token 을 가져오는 OAuth2AccessTokenResponseClient 객체를 갖고 있다.
    • 인증에 성공하면 Access Token 과 Refresh Token 을 Resource Server 로부터 받게 된다.
  • OAuth2AuthorizationCodeAuthenticationToken
    • Authorization Code 를 이용한 인증 후 생성되는 Authentication 객체
    • OAuth2AccessToken(accessToken)OAuth2RefreshToken(refreshToken) 을 관리한다.
  • OAuth2AccessToken
    • OAuth2 에서 사용하는 Access Token 을 관리하는 객체
  • OAuth2RefreshToken
    • OAuth2 에서 사용하는 Reflesh Token 을 관리하는 객체
  • OAuth2User
    • 사용자 Principal 를 관리하는 객체
    • OAuth2User 인터페이스의 구현 인스턴스는 OAuth2AuthenticatedPrincipal 인터페이스로 표현할 수 있다.
    • Authentication.getPrincipal() 를 이용해 값을 얻어올 수 있다.

인증 절차

  • Authorization Code 를 이용해 Resource Server 로부터 Access Token 과 Refresh Token 을 발급 받는다.
  • Access Token 을 이용해 Resource Server 로부터 사용자 정보를 가져온후 OAuth2User 객체를 반환 받는다.
  • 반환 받은 OAuth2User 객체와 Access Token 과 Refresh Token, Client Registration 정보를 이용해 새로운 AuthenticationToken(OAuth2LoginAuthenticationToken) 객체를 생성 후 반환한다.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

// Oauth2 인증을 통해 생성되는 Authentication 객체
OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;

if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains("openid")) {
return null;
}

OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;

try {
// Authorization Code 를 이용해 인증 진행 후 Acces Token 정보를 가져온다.
authorizationCodeAuthenticationToken
= (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
}
catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}

OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();

// Resource Server 로부터 사용자 정보를 조회한 후 OAuth2User 객체를 생성한다.
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;
}

DefaultOAuth2UserService

DefaultOAuth2UserService 주 역할은 Access Token 을 이용해 Resource Server 로부터 사용자 정보를 가져와 OAuth2User 객체를 생성하는 역할이다.

  • userNameAttributeName 정보가 있는지 확인
    • Resource Server 에서 사용자 정보를 식별하기 위한 Id 역할을 하는 정보
  • Resource Server 로부터 사용자 정보를 가져온다.
  • 가져온 사용자 정보를 이용해 OAuth2User 객체를 생성한다.

현재 코드는 예외 코드를 제외한 로직만 있다.

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

// userNameAttributeName 정보가 있는지 확인
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();

// Resource Server 로부터 사용자 정보를 가져온다.
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));
}

// Resource Server 로부터 가져온 사용자 정보를 이용해 OAuth2User 객체를 만든다.
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}

// RestOperations 을 이용해 Resource Server 로 요청을 보낸다.
private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
}
....
}

OAuth2LoginAuthenticationToken

OAuth 인증시 사용하는 Authentication 객체 Principal, Client Registration, Access Token, Refresh Token 등의 정보를 관리한다.

public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private OAuth2User principal;
private ClientRegistration clientRegistration;
private OAuth2AuthorizationExchange authorizationExchange;
private OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;

public OAuth2LoginAuthenticationToken(
ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange) {
super(Collections.emptyList());
this.clientRegistration = clientRegistration;
this.authorizationExchange = authorizationExchange;
this.setAuthenticated(false);
}

public OAuth2LoginAuthenticationToken(
ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange, OAuth2User principal,
Collection<? extends GrantedAuthority> authorities, OAuth2AccessToken accessToken) {
this(clientRegistration, authorizationExchange, principal, authorities, accessToken, null);
}

public OAuth2LoginAuthenticationToken(
ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange, OAuth2User principal,
Collection<? extends GrantedAuthority> authorities, OAuth2AccessToken accessToken,
@Nullable OAuth2RefreshToken refreshToken) {

super(authorities);

this.clientRegistration = clientRegistration;
this.authorizationExchange = authorizationExchange;
this.principal = principal;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.setAuthenticated(true);
}

...

}

DefaultOAuth2User

OAuth 인증 후 전달 받은 사용자 정보를 관리하는 Principal 객체

public class DefaultOAuth2User implements OAuth2User, Serializable {
private static final long serialVersionUID = 560L;
private final Set<GrantedAuthority> authorities;
private final Map<String, Object> attributes;
private final String nameAttributeKey;

public DefaultOAuth2User(
Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes,
String nameAttributeKey
) {

if (!attributes.containsKey(nameAttributeKey)) {
throw new IllegalArgumentException("Missing attribute '" + nameAttributeKey + "' in attributes");
} else {
this.authorities = authorities != null ? Collections.unmodifiableSet(new LinkedHashSet(this.sortAuthorities(authorities))) : Collections.unmodifiableSet(new LinkedHashSet(AuthorityUtils.NO_AUTHORITIES));
this.attributes = Collections.unmodifiableMap(new LinkedHashMap(attributes));
this.nameAttributeKey = nameAttributeKey;
}
}

....

}
Share