저번에 이어서 customOAuth2UserService 를 살펴보겠습니다.
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);
// 어떤 소셜로그인을 이용하는지 구분 하기위해 쓰임.
// ex) registrationId = "naver", registrationId = "google" 등등
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 시 키 값이 된다.
// 구글은 키 값이 "sub"이고, 네이버는 "response"이고, 카카오는 "id"이다.
// 각각 다르므로 이렇게 따로 변수로 받아서 넣어줘야함.
String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2 로그인을 통해 가져온 OAuth2User의 attribute를 담아주는 of 메소드.
// oAuth2User.getAttributes() 에는 다음 값이 담긴다.
// 구글 예시) { sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
String encode = passwordEncoder.encode(UUID.randomUUID().toString());
// user 예시) username=google_1231344534523565757 , password = $2a$10$nlkgA6oUE.7KpHSO6tDDpOBth4PICf1DeQiHQ2qbbaA8o3s1osGvG
User user = new User(registrationId+"_"+attributes.getUsername(), encode ,registrationId);
User loadOrSaveUser = loadOrSave(user);
return new CustomIntegratedLogin(loadOrSaveUser,attributes);
}
// 이미 저장된 유저라면 load, 아니면 save
private User loadOrSave(User user) {
Optional<User> loadUser = userRepository.findByUsername(user.getUsername());
if (loadUser.isEmpty()){ // 소셜로그인을 시도한 아이디가 없다면 저장하고
return userRepository.save(user);
} else { // 소셜로그인을 시도한 아이디가 있다면 가져온다
return loadUser.get();
}
}
}
우선 customOAuth2UserService 는 OAuth2UserService<OAuth2UserRequest, OAuth2User> 을 구현하고 있습니다 이것을 왜 구현하고 있냐??
시큐리티 기본흐름에서 5,6번에 해당하는 내용입니다. 기본 로그인은 UserDetailsService 를 통해 UserDetails를 가져오고 있습니다.
그러나 소셜로그인은 OAuth2UserService 를 이용하여 OAuth2User 를 가져오기때문에 OAuth2UserService 를 구현해야 합니다.
OAuth2UserService 는 인터페이스로 loadUser 라는 메서드를 가지고 있고 이를 오버라이딩해서 구현 해주어야 합니다.
DefaultOAuth2UserService 를 생성한 이유는 그 안에 있는 loadUser 를 통해
OAuth2User 를 가져올수 있기때문에 생성하였습니다.
그 다음 주석을 읽다가 보면 OAuthAttributes.of(~); 가 있는것을 볼 수 있습니다.
그럼 OAuthAttributes 를 살펴보겠습니다.
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes; // ex) { sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
private String nameAttributeKey; // ex) { google="sub", kakao="id", naver="response" }
private String name; // ex) 홍길동
private String email; // ex) test@gmail.com
private String username; // ex) 1231344534523565757
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email,String username) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.username=username;
}
// 해당 로그인인 서비스가 kakao 인지 google 인지 구분하여, 알맞게 매핑을 해주도록 합니다.
// registrationId는 소셜로그인을 시도한 서비스 명("google","kakao","naver"..)이 되고,
// userNameAttributeName은 해당 서비스의 map의 키 값이 되는 값이 됩니다. { google="sub", kakao="id", naver="response" }
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
if (registrationId.equals("kakao")) {
return ofKakao(userNameAttributeName, attributes);
} else if (registrationId.equals("naver")) {
return ofNaver(userNameAttributeName,attributes);
}
return ofGoogle(userNameAttributeName, attributes);
}
// 카카오 예시
// attributes = {
// id=3498023493, 이 부분을 가져온다(username)
// connected_at=2022-07-17T12:40:28Z,
// properties={nickname=xxx},
// kakao_account={
// profile_nickname_needs_agreement=false,
// profile={nickname=xxx}, 이 부분을 가져온다
// has_email=true,
// email_needs_agreement=false,
// is_email_valid=true,
// is_email_verified=true,
// email=xxx@nate.com 이 부분을 가져온다
// }
// }
private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> kakao_account = (Map<String, Object>) attributes.get("kakao_account"); // 카카오로 받은 데이터에서 계정 정보가 담긴 kakao_account 값을 꺼낸다.
Map<String, Object> profile = (Map<String, Object>) kakao_account.get("profile"); // 마찬가지로 profile(nickname 등) 정보가 담긴 값을 꺼낸다.
String username = attributes.get(userNameAttributeName).toString();
return new OAuthAttributes(attributes,
userNameAttributeName,
(String) profile.get("nickname"),
(String) kakao_account.get("email"),
username
);
}
// 네이버 예시
// attributes = {
// resultcode=00,
// message=success,
// response= {
// id=lkdslsdkfjsdf-sdf98334twfdgdfv, 이 부분을 가져온다
// nickname=xx,
// email=xx@naver.com, 이 부분을 가져온다
// name=xxx 이 부분을 가져온다
// }
// }
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response"); // 네이버에서 받은 데이터에서 프로필 정보다 담긴 response 값을 꺼낸다.
return new OAuthAttributes(attributes,
userNameAttributeName,
(String) response.get("name"),
(String) response.get("email"),
(String)response.get("id")
);
}
private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
String username = attributes.get(userNameAttributeName).toString();
return new OAuthAttributes(attributes,
userNameAttributeName,
(String) attributes.get("name"),
(String) attributes.get("email"),username
);
}
}
OAuthAttributes 는 여러 필드를 가지고 있는데 주석에 예시를 적어놓았으니 쓱 보고 넘어 가시면 됩니다.
앞서 호출한 of 메서드를 살펴보면, 우선 어떤서비스로 왔는지 구분하여 처리하고 있습니다 뭔가 복잡해보이는 카카오를 살펴보겠습니다.
ofKakao() 에서 이를 처리하고 있습니다.
attributes 가 넘어오는데 주석문과 같이 값이 넘어오게 됩니다.
id, connected_at, properties, kakao_account 등이 key 값이 되며 = 옆에 있는게 value 값이 됩니다. 그런데 kakao_account 는 값이 여러개 있는것을 볼 수 있는데 이건 뭘까요??
단순히 생각해서 value값이 또 key-value 형태의 쌍으로 넘어온 것 입니다 그럼 profile, has_email, email 등이 다시 키 값이 되는거겠죠?? 간단하죠??
다시 ofKakao() 의 첫줄을 보면 kakao_account 를 Map<String, Object> 의 형태로 받고있습니다 당연하겠죠? value값이 key-value 쌍으로 넘어왔기 때문이죠.
같은 원리로 profile 또한 동일하게 넘어오기 때문에 Map으로 받습니다.
attributes.get(userNameAttributeName) 은 attributes.get("id") 랑 같은 의미입니다.
이해가 되셨다면 리턴값의 구성을 전부 아시게 된겁니다. 다시 CustomOAuth2UserService 로 넘어가겠습니다.
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);
// 어떤 소셜로그인을 이용하는지 구분 하기위해 쓰임.
// ex) registrationId = "naver", registrationId = "google" 등등
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 시 키 값이 된다.
// 구글은 키 값이 "sub"이고, 네이버는 "response"이고, 카카오는 "id"이다.
// 각각 다르므로 이렇게 따로 변수로 받아서 넣어줘야함.
String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2 로그인을 통해 가져온 OAuth2User의 attribute를 담아주는 of 메소드.
// oAuth2User.getAttributes() 에는 다음 값이 담긴다.
// 구글 예시) { sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
String encode = passwordEncoder.encode(UUID.randomUUID().toString());
// user 예시) username=google_1231344534523565757 , password = $2a$10$nlkgA6oUE.7KpHSO6tDDpOBth4PICf1DeQiHQ2qbbaA8o3s1osGvG
User user = new User(registrationId+"_"+attributes.getUsername(), encode ,registrationId);
User loadOrSaveUser = loadOrSave(user);
return new CustomIntegratedLogin(loadOrSaveUser,attributes);
}
// 이미 저장된 유저라면 load, 아니면 save
private User loadOrSave(User user) {
Optional<User> loadUser = userRepository.findByUsername(user.getUsername());
if (loadUser.isEmpty()){ // 소셜로그인을 시도한 아이디가 없다면 저장하고
return userRepository.save(user);
} else { // 소셜로그인을 시도한 아이디가 있다면 가져온다
return loadUser.get();
}
}
}
또 User 라는게 등장하는데 별거 없습니다.
주석문 예시 형태로 넘어가게 됩니다.
그 다음, 이전에 동일한 카카오계정으로 로그인 한 적이 있다면 불러오고 없다면 저장해서 가져오는 간단한 메소드를 거치고 난 후
new CustomIntegratedLogin(loadOrSaveUser,attributes); 라는 것을 거쳐 시큐리티가 알아서 저장하는데 CustomIntegratedLogin 을 확인해 봐야겠죠??
public class CustomIntegratedLogin implements OAuth2User, UserDetails {
private User user;
private OAuthAttributes oAuthAttributes;
public CustomIntegratedLogin(User user) {
this.user=user;
}
public CustomIntegratedLogin(User user, OAuthAttributes oAuthAttributes) {
this.user=user;
this.oAuthAttributes=oAuthAttributes;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
@Override
// ex) {sub=1231344534523565757, name=홍길동, given_name=길동, family_name=홍, picture=https://xxx, email=xxx@gmail.com, email_verified=true, locale=ko}
public Map<String, Object> getAttributes() {
return oAuthAttributes.getAttributes();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collection;
}
@Override
// ex) 1231344534523565757
public String getName() {
return oAuthAttributes.getUsername();
}
}
시작부터 OAuth2User, UserDetails 를 구현한다고 적혀 있습니다.
이 둘을 하나로 묶은 이유는 일반 로그인이랑 소셜로그인 둘 다 이용하는 상황에서
따로 나누면 코드를 더 작성해야 해서 하나로 묶었습니다.
그리고 하나로 묶으면 SuccessHandler 를 같이 쓸 수있는 장점이 있습니다.
다시 본론으로 돌아와서
public CustomIntegratedLogin(User user, OAuthAttributes oAuthAttributes) {
this.user=user;
this.oAuthAttributes=oAuthAttributes;
}
이 생성자를 이용해서 객체를 만들어 주면 시큐리티에서 저장하고 SuccessHandler 에서 객체를 받아서 후 처리를 진행해 주게 됩니다.
다음 글 에서 SuccessHandler 부분을 살펴보겠습니다