목차
- [이론] 스프링 시큐리티 1
- [OAuth] 스프링 시큐리티 OAuth
- [실습] 스프링 시큐리티 OAuth2 Login 1
- [실습] 스프링 시큐리티 OAuth2 Login 2
- [추가] 스프링 시큐리티 OAuth 로그인 처리 방법 1
- [추가] 스프링 시큐리티 OAuth 로그인 처리 방법 2
앞서 스프링 시큐리티가 어떻게 yml파일만을 가지고 설정을 하는지에 대해 알아보았다
이번에는 로그인 요청이 들어왔을때 어떻게 처리하는지 알아보자
OAuth 로그인 요청 처리
역시 Spring Security 답게 Filter를 사용해서 로그인 처리를 하게 된다
앞서 form Login에서 UsernamePasswordAuthenticationFilter에 대해서 충분히 이해했다면 이 또한 쉽게 이해할 것이다
혹시라도 기억이 안난다면 아래 두 글을 보고오길 바란다
OAuth2AuthorizationRequestRedirectFilter
Authorization Server에 접근하여 로그인 시도하여 인증 코드를 받아오는 역할을 한다
로그인 후 Authorization Server에서 redirect_uri로 code값을 queryParameter로 전송
--> callBackUrl은 google, naver등 app을 설정 시 작성한 값
--> OAuth2LoginAuthenticationFilter에서 처리
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
}
catch (Exception ex) { //생략
}
try {
filterChain.doFilter(request, response);
}
catch (IOException ex) { //생략
}
}
authorizationRequestResolver.resolve(request);
/oauth2/authorization/{registrationId}
로 온 요청에서 registrationId 추출
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = resolveRegistrationId(request);
if (registrationId == null) {
return null;
}
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
찾은 registrationId를 통해 ClientRegistration객체를 찾고 OAuth2AuthorizationRequest를 만듬
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
builder.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scopes(clientRegistration.getScopes())
.state(DEFAULT_STATE_GENERATOR.generateKey());
this.authorizationRequestCustomizer.accept(builder);
return builder.build();
}
OAuth2AuthorizationRequest 주요 값
- authorizationUri: yml 파일에 등록 한 값 (네이버 경우 : https://nid.naver.com/oauth2.0/authorize)
- authorizationRequestUri
- queryParam
- RESPONSE_TYPE
- CLIENT_ID
- SCOPE
- STATE
- REDIRECT_URI
해당 값을 사용해서 authorizationRequestUri = authorizationUri?queryParam
생성this.sendRedirectForAuthorization(request, response, authorizationRequest)
에서 사용
OAuth2LoginAuthenticationFilter
로그인 후 받은 인증 코드를 이용해서 액세스 토큰 발행 및 사용자 정보 리소스 요청
- OAuth2LoginAuthenticationFilter.attenptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// callBackUrl로 부터 온 queryParameter 추출
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
// 정상적으로 api요청이 왔는지 확인작업
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
}
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
}
//registrationId를 통해 Provider 가져오기
String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
// callBackUrl로부터 받은 Parameter 추출 후 OAuth2AuthorizationResponse객체로 변경
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params,
redirectUri);
// 토큰 요청을 위한 OAuth2LoginAuthenticationToken객체 생성
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);
// 중요!! providerManager에서 OAuth2LoginAuthenticationProvider.authenticate() 실행
//
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager().authenticate(authenticationRequest);
// 받아온 token converting
OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
.convert(authenticationResult);
oauth2Authentication.setDetails(authenticationDetails);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
}
토큰 발급 시 필요 내용
- grant_type
- client_id
- client_secret
- code
- state
OAuth2LoginAuthenticationProvider.authenticate() -> OAuth2AuthorizationCodeAuthenticationProvider.authenticate()
- OAuth2LoginAuthenticationProvider.authenticate()
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
authorizationCodeAuthenticationToken =
//토큰 받아오기
(OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
}
catch (OAuth2AuthorizationException ex) {
}
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
//CustomUserService.loadUser() 실행
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;
}
- OAuth2AuthorizationCodeAuthenticationProvider.authenticate()
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthorizationException(authorizationResponse.getError());
}
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest();
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
throw new OAuth2AuthorizationException(oauth2Error);
}
// 토큰 받아오기
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
// Authentication 타입의 객체인 OAuth2AuthorizationCodeAuthenticationToken를 만들어 전달
OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken(), accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
- 토큰 받아오기
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
//토큰 요청 url 가진 request 생성
RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
//내부적으로 restTemplate사용해서 토큰 받아옴
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
return response.getBody();
}
이렇게 토큰을 받아오게 되면 OAuth2LoginAuthenticationProvider.authenticate()
에서 userservice.loaduser()
를 실행하게 된다
앞서 구현한 userservice 코드를 다시 확인해 보면 다음과 같다
public class CustomOauth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private static final String PASSWORD = "password";
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2UserInfo oAuth2UserInfo = OAuthAttributes.of(registrationId, super.loadUser(userRequest).getAttributes());
User user = saveUser(oAuth2UserInfo);
return new CustomUser(user, oAuth2UserInfo);
}
private User saveUser(OAuth2UserInfo oauth2Oauth2UserInfo) {
String email = oauth2Oauth2UserInfo.getEmail();
String name = oauth2Oauth2UserInfo.getName();
String password = passwordEncoder.encode(PASSWORD + UUID.randomUUID().toString().substring(0, 8));
User user = new User(email, name, password);
userRepository.findUserByEmail(email)
.ifPresentOrElse(
entity -> entity.update(user.getEmail(), user.getName()),
() -> userRepository.save(user)
);
return user;
}
}
super.loadUser()
를 통해서 accessToken을 이용해서 유저정보를 얻어오게 된다
그 후 얻어온 정보를 통해 회원가입 및 로그인을 진행해 주면 된다
@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);
}
OAuth 로그인 처리 정리
/oauth2/authorization/{registrationId}
로 요청이 오면OAuth2AuthorizationRedirectFilter
가 로그인 창으로 리다이렉션 시킴- 로그인 후 Authorization Server에서 등록해 둔 redirect_uri로 인가 코드와 함께 승인이 옴
- redirect_uri 즉
/login/oauth2/code/{registrationId}
를OAuth2LoginAuthenticationFilter
에서 처리해서 token을 얻어옴 - 얻어온 token을 가지고
DefaultOAuth2UserService.loadUser()
에서 유저 정보를 받아옴 - 유저 정보를 바탕으로 회원가입 및 로그인 처리 시도
이것으로 길고 길었던 Spring Security에 대해서 마무리를 하도록 한다
진행하는 도중에 큰 부상도 있어서 거의 두달 가까이 하지 못해서 진행이 늦춰진 감이 아쉽기는 하지만 정리를 마무리 할 수 있어서 다행이다
'공부기록 > Spring Security' 카테고리의 다른 글
Controller에서 Security 정보 가져오기 (0) | 2023.11.14 |
---|---|
Spring Security - 동적 권한 처리 (2) | 2023.03.29 |
[추가] 스프링 시큐리티 OAuth 로그인 처리 방법 1 (2) | 2023.02.22 |
[실습] 스프링 시큐리티 OAuth2 Login 2 (2) | 2023.02.21 |
[실습] 스프링 시큐리티 OAuth2 Login 1 (0) | 2023.02.21 |