.oauth2Login(oauth2 -> oauth2
.clientRegistrationRepository(clientRegistrationRepository)
.userInfoEndpoint(it -> it.userService(oAuthService))
.successHandler(oAuthAuthneticationSuccessHandler))
먼저 security config에 이렇게 추가시키자.
userInfoEndpoint란, ClientRegistrationRepository에서 등록한 userInfoUri 에서 로그인 정보를 받아오는 point를 말하는 것이고, 잘 받아오면, oAuthService에서 작업을 authentication 작업을 하고 난 후, handler로 뒤처리를 한다.
JWT관련 로그인을 구현할 때, UserDetailsService를 구현하여 AuthService를 만들었던 기억이 있다.
마찬가지이다. 이번엔 OAuth2UserService<OAuth2UserRequest, OAuth2User> 를 구현하자.
@Slf4j
@Service
@RequiredArgsConstructor
public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final SocialMemberRepository socialMemberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
log.info("oauth2user = {}",oAuth2User);
String email = oAuth2User.getAttribute("email");
String nickname = UUID.randomUUID().toString().substring(0,15);
String password = "default";
// Role role = Role.ROLE_USER;
Optional<SocialMember> socialMember = socialMemberRepository.findByEmail(email);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if(socialMember.isEmpty()){
SocialMember savedSocialMember = SocialMember.createSocialMember(email, nickname);
SaveMemberResponseDto savedResponse = socialMemberRepository.save(savedSocialMember);
authorities.add(new SimpleGrantedAuthority("ROLE_FIRST_JOIN"));
User savedUser = new User (String.valueOf(savedResponse.getId()),password,authorities);
return new CustomUserDetails(String.valueOf(savedResponse.getId()),authorities,savedUser,oAuth2User.getAttributes());
}
else{
authorities.add(new SimpleGrantedAuthority("ROLE_EXIST_USER"));
User savedUser = new User (String.valueOf(socialMember.get().getUserId()),password,authorities);
return new CustomUserDetails(savedUser.getUsername(),authorities,savedUser,oAuth2User.getAttributes());
}
}
}
먼저 소셜 로그인이므로, SocialMemberRepository라는 구현체를 직접 주입받았다. 추상화가 필요한 상황은 아니었다. 구현체를 계속 변경할 일이 없기 때문이다.
그후 loadUser
메서드를 override한다. 이는 UserDetailsService의 loadUserByUsername과 동일하다.
그후, oauth2User정보, 즉, DefaultOAuth2UserService 인스턴스를 통해 request에 담긴 구글 로그인 정보들을 받아오고, email 정보, nickname정보들을 꺼낸다. 여기서 nickname정보는 우리 서비스에서 직접 입력하게 할 것이므로 UUID를 이용하여 아무거나 집어 넣었다. (DB nickname이 not null로 설정되어있어서 null로 보내면 안되는 상황이었다)
그 후, 받은 정보들을 토대로, 로그인 정보가 이미 db에 존재하는지 존재하지 않은지에 따라 시나리오가 분기되었다.
그 코드는Optional<SocialMember> socialMember = socialMemberRepository.findByEmail(email);
이 코드였다. 등록된 email이 있는지 체크하고, 이것이 empty인지 아닌지에 따라, ROLE을 다르게 주었다.
물론 그러라고 만든 ROLE은 아니었지만 지금 당장에는 급히 구현해야 하므로, 추후에 리팩토링하면 되지 않을까 싶다. 그리고 우리 서비스에는 딱히 ROLE까지는 필요하진 않기 때문에, 이렇게 임시방편으로 사용해도 괜찮지 않을까 생각했다.
우리 서비스에 가입한적 없으면
DB에 해당 유저정보를 저장 후, 저장된 userId 값과 ROLE_FIRST_JOIN 이라는 ROLE을 부여하여 authority에 추가시킨후 CusmtomUserDetails 인스턴스를 생성, return하여
OAuth2User 객체에 담았다.
이미 우리 서비스에 가입한 유저라면, DB에 등록된 정보를 그대로 OAuth2User 객체에 담아 return 하였다.
여기에서는 앞서 OAuthService로 부터 return된 authentication 객체를 다루는 로직이 수행되어야 한다.
어구만 봐도, OAuthAuthentication이 성공적으로 부여된 것을 Handle한다. 착착 달라붙는다.
@Component
@RequiredArgsConstructor
@Slf4j
public class OAuthAuthneticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
@Value("${jwt.domain}") private String domain;
@Value("${oauth-signup-uri}") private String signUpURI;
@Value("${oauth-signin-uri}") private String signInURI;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
String accessToken = tokenProvider.createToken(authentication);
if(request.getServerName().equals("localhost")){
String cookieValue = "accessToken=" + accessToken + "; Path=/; Domain=" + domain + "; Max-Age=1800; HttpOnly";
response.setHeader("Set-Cookie", cookieValue);
log.info("redirect url ={}",redirectUriByFirstJoinOrNot(authentication));
response.sendRedirect(redirectUriByFirstJoinOrNot(authentication));
}
else{
String cookieValue = "accessToken="+accessToken+"; "+"Path=/; "+"Domain="+domain+"; "+"Max-Age=1800; HttpOnly; SameSite=None; Secure";
response.setHeader("Set-Cookie",cookieValue);
response.sendRedirect(redirectUriByFirstJoinOrNot(authentication));
}
}
private String redirectUriByFirstJoinOrNot(Authentication authentication){
OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = oAuth2User.getAuthorities();
//사실 authority 가 ROLE_FIRST_JOIN인게 이상하긴함. 하지만 authentication 객체를 활용하기 위해서 해당 방법을 사용하였음.
//어차피 role은 우리 로직엔 사용되지 않기 때문임.
if(authorities.stream().filter(o -> o.getAuthority().equals("ROLE_FIRST_JOIN")).findAny().isPresent()){
return UriComponentsBuilder.fromHttpUrl(signUpURI)
.path(authentication.getName())
.build().toString();
}
else{
return UriComponentsBuilder.fromHttpUrl(signInURI)
.build().toString();
}
}
}
이 handler의 역할은 두가지이다.
1. 로그인 후 cookie값 반환하여, 로그인 유지 (쿠키안에 jwt 토큰 담음)
2. 이미 가입된 유저라면 main page로, 첫 사용자라면 nickname창으로 redirection 분기
이렇게 두가지 작업을 하기위해 success handler로 구현하였다.
이러한 handler 역할로 request와 resposne의 역할을 처리할 수 있었다.
이렇게 하여 우리 서비스의 회원가입/로그인은 어느정도 마칠 수 있었다.