OAuth 2.0으로 소셜로그인 구현하기 Version2 ❕
지난 소셜로그인 구현 : V1. OAuth2.0으로 소셜로그인 구현하기
[Spring] V1. OAuth2.0으로 소셜로그인 구현하기
소셜 로그인에서 사용되는 프로토콜인 OAuth에 대해 학습하고 Spring에서 구현해보자 ❕ 1. OAuth (Open Authorization) ?많은 사이트에서 소셜 계정을 기반으로 회원가입 & 로그인을 할 수 있는 기능이
chchaego.tistory.com
소셜 로그인은 크게 1. 인가 요청 2. 토큰 요청 3. 사용자 정보 요청을 하는 과정이 필요하다. 지난번 구현했을 때는 인가 요청의 결과로 받은 인가 코드(Authorization code)를 사용자단에 넘긴 후, 그 값을 Client(내 서버)에게 그대로 전달해 토큰 요청, 사용자 정보조회 요청을 담당하는 방식으로 구현했다. (^인가 요청을 사용자단에서 진행^)
spring-security-oauth2-client
는 기본적으로 인가요청도 Client에서 담당하도록 구현할 수 있다. 그럼 지난번 구현처럼 인가코드를 사용자단에서 Client에게 넘기는 과정을 생략할 수 있고, Client에서 바로 1. 인가 요청 2. 토큰 요청 3. 사용자 정보 요청 을 진행할 수 있다. 모두 oauth2-client
가 자동으로 실행한다.
따라서 이번에는 인가코드를 사용자단이 아닌 Client단에서 처리해보려고 한다.
1. 인증 흐름
V1. 기존 인증 흐름
- 사용자(Resource Owner)가 서비스(Client)의 로그인 페이지에 접근해 소셜 로그인 버튼을 누르면 해당 플랫폼의 인가 서버(Authorization Server)에게 로그인 페이지를 요청한다.
- 인가 서버(Authorization Server)에서 사용자의 화면을 로그인 페이지로 redirect시킨다.
- 사용자가 id, pw를 입력해 로그인한다.
- 로그인 정보가 올바르다면 인가 서버는 인가 코드(Authorization code)를 Callback URL을 통해 사용자에게 전달한다.
- 사용자는 발급된 인가 코드를 그대로 Client에게 보낸다.
- Client는 인가 서버(Authorization Server)에게 인가 코드와 Client정보를 함께 보낸다.
- 인가 서버는 Client가 보낸 정보를 확인하고, 토큰(Access Token)을 발급한다.
- 토큰을 받은 Client는 리소스 서버(Resource Server)를 통해 사용자 정보를 조회하고, 조회된 결과를 이용해 Client 서버에서 회원가입 처리(DB에 저장) 후 로그인이 완료되었다고 응답한다.
(이 때 Client DB에 저장되는 사용자의 정보는 pw를 포함하지 않으며 권한 설정으로 허락한 정보를 저장한다)
V2. 변경된 인증 흐름
(변경 전)
1. 사용자(Resource Owner)가 서비스(Client)의 로그인 페이지에 접근해 소셜 로그인 버튼을 누르면 해당 플랫폼의 인가 서버(Authorization Server)에게 로그인 페이지를 요청한다.
(변경 후)
-> ^사용자(Resource Owner)가 서비스(Client)의 로그인 페이지에 접근해 소셜 로그인 버튼을 누르면 Client를 통해 해당 플랫폼의 인가 서버(Authorization Server)에게 로그인 페이지를 요청한다.^
(변경 전)
4. 로그인 정보가 올바르다면 인가 서버는 인가 코드(Authorization code)를 Callback URL을 통해 사용자에게 전달한다.
5. 사용자는 발급된 인가 코드를 그대로 Client에게 보낸다.
(변경 후)
→ ^로그인 정보가 올바르다면 인가 서버는 인가코드(Authorization code)를 Callback URL을 통해 Client에게 전달한다.^
- 사용자(Resource Owner)가 서비스(Client)의 로그인 페이지에 접근해 소셜 로그인 버튼을 누르면 Client를 통해 해당 플랫폼의 인가 서버(Authorization Server)에게 로그인 페이지를 요청한다.
- 인가 서버(Authorization Server)에서 사용자의 화면을 로그인 페이지로 redirect시킨다.
- 사용자가 id, pw를 입력해 로그인한다.
- 로그인 정보가 올바르다면 인가 서버는 인가코드(Authorization code)를 Callback URL을 통해 Client에게 전달한다.
- Client는 인가 서버(Authorization Server)에게 인가 코드와 Client정보를 함께 보낸다.
- 인가 서버는 Client가 보낸 정보를 확인하고, 토큰(Access Token)을 발급한다.
- 토큰을 받은 Client는 리소스 서버(Resource Server)를 통해 사용자 정보를 조회하고, 조회된 결과를 이용해 Client 서버에서 회원가입 처리(DB에 저장) 후 로그인이 완료되었다고 응답한다.
2. spring-security-oauth2-client 처리과정
`spring-security-oauth2-client`는 Spring Security에서 OAuth2.0 프로토콜을 사용해 인증처리를 하는 Client에게 편리한 기능을 제공한다. 따라서 Spring Security 설정 파일에서 OAuth2.0 클라이언트 구성을 세부적으로 제어할 수 있다. 또, Spring Boot에서 제공하는 starter 종속성인 `spring-boot-starter-oauth2-client`도 있다. 이는 `spring-security-oauth2-client`를 포함해 관련한 의존성을 자동으로 포함한다. 대부분의 설정이 자동화되어 있다는 특징이 있다.
나는 Spring Security에서 소셜 로그인 과정을 처리할거라 `spring-security-oauth2-client`를 사용했다.
oauth2-client의 로그인 과정은 ` OAuth2AuthorizationRequestRedirectFilter`와 `OAuth2LoginAuthenticationFilter`에서 처리된다. `OAuth2AuthorizationRequestRedirectFilter`에서 로그인 페이지를 요청해 인가 요청을 하고, 인가 요청의 결과로 받은 인가 코드를 이용해 `OAuth2LoginAuthenticationFilter`에서 토큰 요청, 사용자 정보 요청을 실행한다.
1. OAuth2AuthorizationRequestRedirectFilter
: 사용자가 Client에게 로그인 페이지를 요청하면, Client는 인가 서버(Authorization Server)에게 로그인 페이지를 요청해 사용자의 페이지를 redirect시킨다. redirect된 페이지에서 사용자가 로그인해 인가 요청을 실행한다.
- 사용자가 Client에게 로그인 페이지를 요청하면 OAuth2AuthorizationRequestRedirectFilter가 실행된다. --- (a)
- OAuth2AuthorizationRequestRedirectFilter의 `doFilterInternal()`이 시작하고 OAuth2AuthorizationRequestResolver의 `resolve()`를 호출한다.
- OAuth2AuthorizationRequestResolver의 `resolve()`가 호출되면, 구현체인 DefaultOAuth2AuthorizationRequestResolver의 `resolve()`가 실행된다.
- DefaultOAuth2AuthorizationRequestResolver의 `resolve()`에서는 로그인 요청 url을 확인하고 registrationId를 추출한 뒤 환경변수에 설정했던 clientId와 redirectUri 같은 정보들을 담아 OAuth2AuthorizationRequest 객체를 생성해 반환한다.
- 이렇게 생성된 OAuth2AuthorizationRequest를 통해 (a) OAuth2AuthorizationRequestRedirectFilter의 `sendRedirectForAuthorization()`가 실행되면 OAuth2AuthorizedClientRepository에 OAuth2AuthorizationRequest가 저장되고 로그인 요청 페이지로 redirect 된다.
+ OAuth2AuthorizationRequest는 어떻게 저장될까?
`sendRedirectForAuthorization()`를 보면 OAuth2AuthorizationRequest는 OAuth2AuthorizedClientRepository에 저장되는데, OAuth2AuthorizedClientRepository의 기본 구현체는 HttpSessionOAuth2AuthorizationRequestRepository이고 세션을 통해 저장되는 방식이다.
그럼 그 다음에 실행될 OAuth2LoginAuthenticationFilter에서 이 세션에 저장된 값으로 OAuth2AuthorizationRequest 객체를 알 수 있게 되는 것이다.
(만약 인가요청의 결과를 받을 때 redirect를 ClientA → ClientB로 이동해야 하거나 세션을 사용하지 않는다면, OAuth2AuthorizedClientRepository를 구현하는 custom한 HttpCookieOAuth2AuthorizationRequestRepository 클래스를 만들어서 세션 대신 쿠키에 저장하면 된다)
2. OAuth2LoginAuthenticationFilter
: 사용자가 로그인을 완료하면 인가 서버(Authorization Server)는 인가 코드를 담아 Client로 redirect시키고, Client는 인가 코드를 이용해 토큰 요청, 토큰 요청의 결과로 사용자 정보 요청을 실행한다.
- 로그인을 실행하면 인가 서버에게 인증을 요청한다.
- 인가 서버는 인가 코드를 담아 Client에게 redirect하고 OAuth2LoginAuthenticationFilter가 실행된다. (redirect uri는 default로 '/login/oauth2/code/*'이다)
- OAuth2LoginAuthenticationFilter의 `doFilter()`가 실행되는데, OAuth2LoginAuthenticationFilter에는 `doFilter()`가 없어 부모 클래스인 AbstractAuthenticationProcessingFilter의 `doFilter()`가 실행된다.
- 여기서 OAuth2LoginAuthenticationFilter의 `attemptAuthentication()`이 실행된다.
- `attemptAuthentication()`은 OAuth2LoginAuthenticationToken을 만들고, AuthenticationManager을 통해 AuthenticationProvider들에게 실제 인증을 위임한다. (SpringSecurity의 인증과정과 방식과 동일하게 동작)
- `this.getAuthenticationManager().authenticate()`가 호출되면 AuthenticationProvider의 구현체인 OAuth2LoginAuthenticationProvider에서 `authenticate()`가 실행된다.
- 이 때, OAuth2LoginAuthenticationProvider가 생성되면서 OAuth2AuthorizationCodeAuthenticationProvider를 생성 & 저장, UserService를 저장한다.
- OAuth2LoginAuthenticationProvider에서 `authenticate()`에서 OAuth2AuthorizationCodeAuthenticationProvider의 `authenticate()`가 실행되고, 여기서 인가 서버에게 토큰 요청을 한다.
- OAuth2AuthorizationCodeAuthenticationProvider의 `authenticate()`는 토큰 요청의 결과로 OAuth2AuthorizationCodeAuthenticationToken를 생성해 반환한다.
- OAuth2LoginAuthenticationProvider는 반환된 OAuth2AuthorizationCodeAuthenticationToken을 이용해 userService의 `loadUser()`를 실행시킨다.
(인증 코드를 포함한 OAuth2AuthorizationCodeAuthenticationToken이 생성되고 액세스 토큰을 요청한다. 그 후 액세스 토큰과 사용자 정보를 통해 OAuth2LoginAuthenticationToken이 생성되는 것이다)
- `loadUser()`는 UserService의 구현체인 DefaultOAuth2UserService의 `loadUser()`가 호출되고 토큰을 이용해 사용자 정보 요청을 한다.
- 사용자 정보 요청의 결과로 OAuth2User를 생성해 반환한다.
- 이제 OAuth2LoginAuthenticationFilter에서 `this.getAuthenticationManager().authenticate()`가 완료된 것이다.
- 따라서 AbstractAuthenticationProcessingFilter에서 `attemptAuthentication()`가 완료되고, 인증 과정이 성공적으로 완료되면 `successfulAuthentication()`이 호출된다.
- `successfulAuthentication()`에서 SecurityContext에 인증 객체를 저장하고 인증 과정을 마친다.
+ OAuth2LoginAuthenticationFilter는 OAuth2AuthorizationRequestRedirectFilter 실행 시 저장했던 OAuth2AuthorizationRequest를 OAuth2AuthorizedClientRepository의 기본 구현체인 HttpSessionOAuth2AuthorizationRequestRepository를 통해 조회한다. ^마찬가지로 세션 대신 쿠키를 사용하고 싶다면 custom한 HttpCookieOAuth2AuthorizationRequestRepository를 만들어주면 된다.^
3. 구현 - 네이버
네이버 로그인 API 명세 : https://developers.naver.com/products/login/api/api.md
애플리케이션 설정
지난번 애플리케이션 설정 상태에서 Callback URL를 추가해준다.
Client는 8081 포트를 사용하기 때문에 'http://localhost:8081/login/oauth2/code'로 설정했다.
Client 구현
변경 전
/**
* NAVER Social Login
*
* Required Variable : response_type, client_id, redirect_uri, state
*/
const NAVER_CLIENT_ID = {CLIENT_ID};
const NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
const NAVER_AUTH_PARAMS = `?client_id=${NAVER_CLIENT_ID}
&redirect_uri=http://localhost:3000/home?registrationId=naver
&response_type=code
&state=hihi`;
const SocialLoginComponent = (props) => {
// 네이버 로그인 버튼
return (
<div className='social-button'>
<a href={NAVER_AUTH_URL + NAVER_AUTH_PARAMS} target='_blank'><img src="/assets/naver_login.png" width='40' /></a>
</div>
)
}
변경 후
const ORIGIN_URL = "http://localhost:8081"
const SocialLoginComponent = (props) => {
// 네이버 로그인 버튼
return (
<div className='social-button'>
<a href={`${ORIGIN_URL}/oauth2/login/naver`} ><img src="/assets/naver_login.png" width='40' /></a>
</div>
)
}
로그인 버튼을 눌렀을 때 인증 요청을 실행할 url을 설정한다. (http://localhost:8081/oauth2/login/naver)
Server 구현
properties파일에 환경변수를 설정해준다.
# NAVER SOCIAL LOGIN SETTING
spring.security.oauth2.client.registration.naver.client-id={NAVER_CLIENT_ID}
spring.security.oauth2.client.registration.naver.client-secret={NAVER_CLIENT_SECRET}
spring.security.oauth2.client.registration.naver.scope=name,email,profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8081/login/oauth2/code/naver
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
app.oauth2.authorized-redirect-url=http://localhost:3000/home
app.oauth2.unauthorized-redirect-url=http://localhost:3000/error
spring.security.oauth2.client.registration.naver.redirect-uri
이 값을 애플리케이션 설정 시 등록했던 Callback URL로 변경한다.
app.oauth2.authorized-redirect-url
와 app.oauth2.unauthorized-redirect-url
는 최종 로그인 성공여부에 따라 사용자에게 redirect 시킬 url이다.
- 기존 로그인 과정에 사용했던 CustomUserDetails이다. OAuth2User도 implements 해준다.
- `getAttributes()`와 `getName()`을 추가로 오버라이딩했다.
@Getter
@ToString
public class CustomUserDetails implements UserDetails, OAuth2User {
private UserDto user;
private Map<String, Object> attributes;
public CustomUserDetails(UserDto user) {
this.user = user;
}
...
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null;
}
}
- 시큐리티 설정 클래스(SecurityConfig)를 만든다.
authorizationEndpoint
: baseUri()로 로그인 요청 시 이동시킬 uri을 설정. (default 요청 경로는 ‘/oauth2/authorization/{registrationId}’)userInfoEndpoint
: userService()로 토큰 요청의 결과로 사용자 정보 요청을 담당하는 서비스를 설정successHandler
: 인증이 성공했을 때 실행할 핸들러failureHandler
: 인증이 실패됐을 때 실행할 핸들러
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// oauth2 login 설정
http
.oauth2Login(login -> login
.authorizationEndpoint(endpoint -> endpoint.baseUri("/oauth2/login"))
.userInfoEndpoint(endpoint -> endpoint.userService(customOAuth2UserService))
.successHandler(oAuth2AuthenticationSuccessHandler())
.failureHandler(oAuth2AuthenticationFailureHandler()));
return http.build();
}
@Bean
public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
return new OAuth2AuthenticationSuccessHandler();
}
@Bean
public OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler() {
return new OAuth2AuthenticationFailureHandler();
}
}
- 사용자가 Client에게 로그인 요청을 보낸다.
- `authorizationEndpoint`로 설정한 uri로 넘어온 요청이라면 OAuth2AuthorizationRequestRedirectFilter를 실행한다.
- OAuth2AuthorizationRequestRedirectFilter에서 OAuth2AuthorizationRequest를 생성한 뒤 로그인 페이지로 redirect시킨다. 사용자가 로그인을 완료하면 인가 코드가 발급되고, 이를 이용해 OAuth2LoginAuthenticationFilter에서 토큰 요청, 사용자 정보 요청을 실행하게 된다.
- DefaultOAuth2UserService의 하위 클래스로 CustomOAuth2UserService를 생성했다.
- DefaultOAuth2UserService의 `loadUser()`는 토큰을 이용해 사용자 정보 요청을 하고, 결과로 OAuth2User를 반환한다. 나는 OAuth2User을 사용해 유저를 회원가입(DB 저장) or 로그인 처리하는 과정이 필요해 CustomOAuth2UserService를 생성해줬다.
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
System.out.println("#=======OAUTH USER SERVICE=======#");
// DefaultOAuth2UserService의 loadUser()호출
OAuth2User oAuth2User = super.loadUser(userRequest);
ClientRegistration clientRegistration = userRequest.getClientRegistration();
OAuthUserAttributes userAttributes = OAuthUserAttributes.of(clientRegistration, oAuth2User.getAttributes());
String PROVIDER = clientRegistration.getRegistrationId().toString();
String USERNAME = PROVIDER + "_" + userAttributes.getResponseId();
String PASSWORD = "";
// 우리 회원가입 로직 실행 (user 저장)
User entity = User.builder()
.id(UUID.randomUUID())
.username(USERNAME)
.password(PASSWORD)
.profileName(userAttributes.getProfileName())
.profileImagePath(userAttributes.getProfileImagePath())
.roles("ROLE_USER")
.provider(PROVIDER)
.providerUserId(userAttributes.getResponseId())
.createdAt(LocalDateTime.now())
.build();
// 회원가입 여부 확인
Optional<User> duplicatedUserOpt = userRepository.findByUsername(USERNAME);
if(duplicatedUserOpt.isPresent()) {
entity = duplicatedUserOpt.get();
} else {
userRepository.save(entity);
}
UserDto userDto = UserDto.toDto(entity);
return new CustomUserDetails(userDto);
}
}
- 로그인 과정이 성공적으로 완료되었을 때 처리를 위해 SimpleUrlAuthenticationSuccessHandler의 하위 클래스로 OAuth2AuthenticationSuccessHandler를 생성한다.
- 우리 사이트 전용 access token & refresh token을 발급한다.
- `clearAuthenticationAttributes()`로 세션에 저장된 attribute들을 제거한다.
- 환경변수로 설정한
authorized-redirect-url
(localhost:3000/home)으로 redirect 시킨다.
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Value("${app.oauth2.authorized-redirect-url}")
private String authorizedRedirectUrl;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
System.out.println("-----successfulOAuthAuthorization-----");
// 로그인 성공된 user 조회
UserDto user = ((CustomUserDetails) authentication.getPrincipal()).getUser();
// access token & refresh token 발급
...
this.clearAuthenticationAttributes(request);
this.getRedirectStrategy().sendRedirect(request, response, authorizedRedirectUrl);
}
}
- 로그인 과정이 실패되었을 때 처리를 위해 SimpleUrlAuthenticationFailureHandler의 하위 클래스로 OAuth2AuthenticationFailureHandler를 생성한다.
- 쿠키에 저장된 우리 사이트 전용 access token을 제거한다. (이 과정은 생략해도 된다)
- 환경변수로 설정한
unauthorized-redirect-url
(localhost:3000/error)으로 redirect 시킨다.
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Value("${app.oauth2.unauthorized-redirect-url}")
private String unauthorizedRedirectUrl;
@Override
public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response,
final AuthenticationException exception) throws IOException, ServletException {
System.out.println("-----failureOAuth2Authorization-----");
// Cookie에 Access Token 제거
...
this.getRedirectStrategy().sendRedirect(request, response, unauthorizedRedirectUrl);
}
}
4. 마무리
기존에는 사용자로부터 넘어온 인가코드를 사용해 토큰 요청, 사용자 정보 요청을 직접 구현해야 했는데 oauth2-client
를 사용해서 이 과정을 간단하게 구현할 수 있었다.
위 방법과 같이 구현한다면, 지난번 구현(직접 토큰 요청, 사용자 정보 요청 구현)했던 컨트롤러와 서비스는 제거해줘도 된다.
제거
@RestController
@RequestMapping("/api/social-login")
@RequiredArgsConstructor
public class SocialLoginController {
private final SocialLoginService socialLoginService;
@PostMapping("/{registrationId}")
public ResponseEntity<?> socialLogin(HttpServletResponse response, @PathVariable(name = "registrationId") String registrationId, @RequestParam String code) {
Message message = new Message();
try {
socialLoginService.socialLogin(response, registrationId, code);
message.setStatus(HttpStatus.OK);
message.setMessage("success");
} catch (Exception e) {
message.setStatus(HttpStatus.BAD_REQUEST);
message.setMessage("error");
message.setMemo(e.getMessage());
}
return new ResponseEntity<>(message, message.getStatus());
}
}
public class SocialLoginService {
private final ClientRegistrationRepository clientRegistrationRepository;
public void socialLogin(HttpServletResponse response, String registrationId, String code) throws IOException {
// 0. registrationId를 확인해 어떤 플랫폼에서 요청되는지 확인
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("등록되지 않은 RegistrationId");
}
// 1. 토큰 요청
OAuthResponse.RequestToken tokenResponse = this.requestOAuth2AccessToken(clientRegistration, code);
// 2. 유저 정보 조회 요청
OAuthUserAttributes userAttributes = this.requestOAuth2UserInfo(clientRegistration, tokenResponse);
// 3. 새로운 회원이라면 user entity 저장하고, 이미 가입된 회원이라면 저장된 회원을 조회한다.
UserDto userDto = this.getRegisteredUser(registrationId, userAttributes);
// access token, refresh token 생성
...
}
// 1. 토큰 요청
private OAuthResponse.RequestToken requestOAuth2AccessToken(ClientRegistration clientRegistration, String code) {
String tokenRequestUri = clientRegistration.getProviderDetails().getTokenUri();
MultiValueMap<String, String> parameter = new LinkedMultiValueMap<>();
parameter.add("grant_type", "authorization_code");
parameter.add("client_id", clientRegistration.getClientId());
parameter.add("client_secret", clientRegistration.getClientSecret());
parameter.add("code", code);
parameter.add("state", "1234");
OAuthResponse.RequestToken tokenResponse = WebClient.create()
.post()
.uri(tokenRequestUri)
.headers(header -> {
header.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
header.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
})
.bodyValue(parameter)
.retrieve()
.bodyToMono(OAuthResponse.RequestToken.class)
.block();
return tokenResponse;
}
// 2. 유저 정보 조회 요청
private OAuthUserAttributes requestOAuth2UserInfo(ClientRegistration clientRegistration, OAuthResponse.RequestToken tokenResponse) {
String userInfoRequestUri = clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri();
OAuthUserAttributes userAttributes = OAuthUserAttributes.of(clientRegistration, WebClient.create()
.get()
.uri(userInfoRequestUri)
.headers(header -> header.setBearerAuth(tokenResponse.getAccess_token()))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.block());
return userAttributes;
}
// 3. 새로운 회원이라면 user entity 저장하고, 이미 가입된 회원이라면 저장된 회원을 조회한다.
private UserDto getRegisteredUser(String registrationId, OAuthUserAttributes userAttributes) {
String USERNAME = registrationId + "_" + userAttributes.getResponseId();
String PASSWORD = passwordEncoder.encode(UUID.randomUUID().toString());
// 3. 우리 회원가입 로직 실행(user테이블에 저장 및 access token, refresh token 발급)
User entity = User.builder()
.id(UUID.randomUUID())
.username(USERNAME)
.password(PASSWORD)
.profileName(userAttributes.getProfileName())
.profileImagePath(userAttributes.getProfileImagePath())
.roles("ROLE_USER")
.provider(registrationId)
.providerUserId(userAttributes.getResponseId())
.createdAt(LocalDateTime.now())
.build();
// 회원가입 여부 확인
Optional<User> duplicatedUserOpt = userRepository.findByUsername(USERNAME);
if(duplicatedUserOpt.isPresent()) {
entity = duplicatedUserOpt.get();
} else {
userRepository.save(entity);
}
UserDto userDto = UserDto.toDto(entity);
return userDto;
}
}
이 외에도 더 세부적으로 설정해줘야 할 것들이 많다. 실무에서 oauth2를 사용한다면 oauth2-client에서 사용되는 값, 플랫폼에서 넘어오는 값, Client 전용 회원가입 로직 등에 대한 예외 처리도 필요해 보인다.
참고자료 😃
https://velog.io/@rnqhstlr2297/Spring-Security-OAuth2-소셜로그인#39-oauth2userinfo
https://velog.io/@tmdgh0221/Spring-Security-와-OAuth-2.0-와-JWT-의-콜라보
https://letsmakemyselfprogrammer.tistory.com/40
https://github.com/InhaBas/Inhabas.com-api/wiki/Auth-module-document
https://iseunghan.tistory.com/300
https://inkyu-yoon.github.io/docs/Language/SpringBoot/OauthLogin#7-oauth2successhandler-설정
동작 과정
https://velog.io/@semi-cloud/Spring-Security-Spring-Security-Oauth2-Client-분석
https://velog.io/@nefertiri/스프링-시큐리티-OAuth2-동작-원리
'Spring' 카테고리의 다른 글
[Spring] SpringBoot에서 Redis 적용하기 (+부하 테스트) (8) | 2024.11.08 |
---|---|
[Spring] Spring에서 동시성 이슈를 해결하는 방법 (1) | 2024.10.02 |
[Spring] Apache POI (+ Multipart, Spring 구현) (0) | 2024.05.14 |
[Spring] V1. OAuth2.0으로 소셜로그인 구현하기 (0) | 2024.03.16 |
[Spring] Spring Security (스프링 시큐리티) (0) | 2024.03.13 |