OAuth2UserService 의OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest)
메서드는
서드파티에 사용자 정보를 요청할 수 있는 access token 을 얻고나서 실행됩니다.
이 때 access token과 같은 정보들이 oAuth2UserRequest 파라미터에 들어있습니다.
이 메서드가 할 일은 다음과 같습니다.
- access token을 이용해 서드파티 서버로부터 사용자 정보를 받아온다.
- 해당 사용자가 이미 회원가입 되어있는 사용자인지 확인한다.
만약 회원가입이 되어있지 않다면, 회원가입 처리한다.
만약 회원가입이 되어있다면, 프로필사진URL 등의 정보를 업데이트한다. - UserPrincipal 을 return 한다.
세션 방식에서는 여기서 return한 객체가 시큐리티 세션에 저장된다.
하지만 JWT 방식에서는 저장하지 않는다.
(JWT 방식에서는 인증&인가 수행시 HttpSession을 사용하지 않을 것이다.)
1. 커스텀 OAuth2UserService 클래스 생성
DefaultOAuth2UserService를 상속해서 만들 것입니다.
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
/**
* 서드파티 접근을 위한 accessToken까지 얻은다음 실행된다.
*/
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
return null;
}
}
2. super.loadUser(oAuth2UserRequest)
처음에 loadUser 메서드의 역할을 1 ~ 3번으로 정의했었는데요.
그 중 1번이었던 "access token을 이용해 서드파티 서버로부터 사용자 정보를 받아온다" 기능을 넣을 것입니다.
아주 쉽습니다. super.loadUser() 를 호출하기만하면 됩니다.
DefaultOAuth2UserService 클래스의 loadUser() 메서드에 이 기능이 구현되어있기 때문입니다.
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
// accessToken으로 서드파티에 요청해서 사용자 정보를 얻어옴
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
return null;
}
}
3. 사용자 정보 validation
앞서 OAuth2User 타입 객체로 전달받은 사용자 정보를
간단하게 validation 해주도록 하겠습니다.
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
validateAttributes(oAuth2User.getAttributes());
return null;
}
private void validateAttributes(Map<String, Object> attributes) {
if (!attributes.containsKey("email")) {
throw new IllegalArgumentException("서드파티의 응답에 email이 존재하지 않습니다!!!");
}
}
}
4. 자동 회원가입 / 회원정보 갱신
처음에 정의했던 loadUser 메서드의 역할 2번을 구현해볼 것입니다.
회원가입 한 적이 있는지 확인해서
회원가입한 적 없으면 자동으로 회원가입을 시키고,
회원가입한 적 있으면 정보갱신을 하도록 구현....하려고 했으나 제 테스트 프로젝트에서는 갱신할 정보가 없네요.
프로필사진 url 같은걸 쓴다면 이런걸 갱신해주면 됩니다.
package com.yelim.security.service;
import com.yelim.security.domain.Provider;
import com.yelim.security.domain.User;
import com.yelim.security.domain.UserPrincipal;
import com.yelim.security.repository.UserRepository;
import java.util.Map;
import java.util.Optional;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
public CustomOAuth2UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
Map<String, Object> attributes = super.loadUser(oAuth2UserRequest).getAttributes();
Provider provider = Provider.from(oAuth2UserRequest.getClientRegistration().getRegistrationId());
validateAttributes(attributes);
registerIfNewUser(attributes, provider);
return null;
}
private void validateAttributes(Map<String, Object> userInfoAttributes) {
if (!userInfoAttributes.containsKey("email")) {
throw new IllegalArgumentException("서드파티의 응답에 email이 존재하지 않습니다!!!");
}
}
private void registerIfNewUser(Map<String, Object> userInfoAttributes, Provider provider) {
String email = (String) userInfoAttributes.get("email");
Optional<User> optionalUser = userRepository.findByEmailAndProvider(email, provider);
if (optionalUser.isPresent()) {
return;
}
User user = new User(null, email, "대충 아무 텍스트", provider);
userRepository.save(user);
}
}
.
5. UserPrincipal 리턴
loadUser 메서드는 OAuth2User 타입을 return 합니다.
만약 인증/인가를 세션 방식으로 구현하면,
return 한 OAuth2User 객체가 시큐리티 세션에 저장됩니다.
JWT 방식으로 구현할 경우 세션을 사용하지 않으므로 세션에 저장하지는 않습니다.
하지만 그렇다고해서 지금처럼 null 을 리턴하면 안됩니다.
스프링 시큐리티 코드 어딘가에서 이 객체에 접근하기 때문에
그 부분이 실행될 때 Null Pointer Exception이 발생합니다.
package com.yelim.security.service;
import com.yelim.security.domain.Provider;
import com.yelim.security.domain.User;
import com.yelim.security.domain.UserPrincipal;
import com.yelim.security.repository.UserRepository;
import java.util.Map;
import java.util.Optional;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
public CustomOAuth2UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) {
Map<String, Object> attributes = super.loadUser(oAuth2UserRequest).getAttributes();
Provider provider = Provider.from(oAuth2UserRequest.getClientRegistration().getRegistrationId());
validateAttributes(attributes);
User user = registerIfNewUser(attributes, provider);
return UserPrincipal.create(user, attributes);
}
private void validateAttributes(Map<String, Object> userInfoAttributes) {
if (!userInfoAttributes.containsKey("email")) {
throw new IllegalArgumentException("서드파티의 응답에 email이 존재하지 않습니다!!!");
}
}
private User registerIfNewUser(Map<String, Object> userInfoAttributes, Provider provider) {
String email = (String) userInfoAttributes.get("email");
Optional<User> optionalUser = userRepository.findByEmailAndProvider(email, provider);
if (optionalUser.isPresent()) {
return optionalUser.get();
}
User user = new User(null, email, "대충 아무 텍스트", provider);
return userRepository.save(user);
}
}
이제 OAuth2UserService 구현이 끝났습니다!
5. 테스트코드
도대체 OAuth2UserService 는 테스트코드를 어떻게 짜야할까요?...
언젠가 알게되면 꼭 정리하겠습니다....
'Spring Security' 카테고리의 다른 글
소셜로그인 7. OAuth2AuthenticationSuccessHandler (0) | 2021.09.06 |
---|---|
소셜로그인 5. UserDetails, OAuth2User 타입 객체 만들기 (0) | 2021.09.05 |
소셜로그인 4. 토큰 정보 정의 (0) | 2021.09.05 |
소셜로그인 3. Spring Security 의존성 및 yml (0) | 2021.09.05 |
소셜로그인 2. 구글 콘솔에서 Oauth 앱 생성하기 - 승인된 리다이렉션 URI란 (1) | 2021.09.05 |