- 앞서 스프링 시큐리티 세션에 접근하기 위해 Authentication을 사용하고 Authentication을 사용하기 위해 UserDetails와 OAuth2User를 상속받는 PrincipalDetails를 생성하여 Authentication의 매개변수로 넣어주었다.
- 또한 OAuth2를 이용하여 구글 user 정보를 가져왔을 때 아래와 같은 데이터를 가져올 수 있었다.
- 이제 이 데이터를 이용해 회원가입을 진행해보자!!
{sub=105156291955329144943,
name=박민,
given_name=민,
family_name=박,
picture=https://lh3.googleusercontent.com/a/AGNmyxaP5PqWp_Owhh4DmwwCmzSYx_4114WaKnhyuOpQ=s96-c,
email=magicofclown@gmail.com,
email_verified=true,
locale=ko}
- 여기서 sub 값은 구글에서 사용하는 user의 primary key이다.
- 이 user의 데이터 값은 Map<String, Object>타입으로 저장되어 있다.
PrincipalDetails 수정
- attributes를 추가하고 메서드를 수정해주자
...
private Map<String, Object> attributes;
// 일반 로그인
public PrincipalDetails(User user) {
this.user = user;
}
// OAuth 로그인
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
...
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null;
}
User 수정
@Builder
public User(int id, String username, String password, String email, String role,
String provider, String providerId, Timestamp loginDate, Timestamp createDate) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.provider = provider;
this.providerId = providerId;
this.loginDate = loginDate;
this.createDate = createDate;
}
- 이렇게 생성자를 따로 명시하면 기본 생성자는 생성되지 않는데 JPA에서는 기본생성자가 필수이기 때문에 @NoArgsConstructor도 추가해주자!
PrincipalOauth2UserService 수정
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserRepository userRepository;
// 구글로부터 받은 userRequest 데이터에 대한 후처리가 되는 함수
// 해당 함수 종료시에 @Authentication이 만들어진다!!!
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// registrationId로 어떤 oauth로 로그인 했는지 알 수 있음
System.out.println("getClientRegistration : " + userRequest.getClientRegistration());
System.out.println("getAccessToken = " + userRequest.getAccessToken().getTokenValue());
OAuth2User oAuth2User = super.loadUser(userRequest);
// 구글 로그인 버튼 클릭 -> 구글 로그인 창 -> 로그인을 완료 -> code를 리턴(Oauth-client라이브러리
// -> AccessToken 요청 -> userRequest정보생성 -> loadUser함수 호출
// -> 구글로부터 회원 프로필을 받아줌
System.out.println("getAttributes = " + super.loadUser(userRequest).getAttributes());
// 회원가입을 강제로 진행해볼 예정
String provider = userRequest.getClientRegistration().getRegistrationId(); // google
String providerId = oAuth2User.getAttribute("sub");
String username = provider + "_" + providerId; // google_105156291955329144943
String password = bCryptPasswordEncoder.encode("겟인데어");
String email = oAuth2User.getAttribute("email");
String role = "ROLE_USER";
// 회원 등록
User userEntity = userRepository.findByUsername(username);
// 회원 중복 체크
if (userEntity == null) {
userEntity = User.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userRepository.save(userEntity);
}
return new PrincipalDetails(userEntity, oAuth2User.getAttributes());
}
}
- loadUser의 리턴 타입은 OAuth2User인데 정작 리턴 값은 PrincipalDetails이다.
- 앞서 PrincipalDetails에서 UserDetails와 OAuth2User를 상속 받았기 때문에 가능하다.
- loadUser메소드가 종료될 때 @AuthenticationPrincipal 어노테이션이 생성된다!!
IndexController 수정
- PrincipalDetails가 UserDetails와 OAuth2User를 상속하고 있어 어떤 방식으로 로그인을 하든지 PrincipalDetails를 이용해 Authentication을 생성할 수 있다!!
- user메소드의 매개변수인 @AuthenticationPrincipal이 앞에서 loadUser메소드가 종료될 때 생성된 @AuthenticationPrincipal이다!!!!
@GetMapping("/user")
@ResponseBody
public String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
System.out.println("principalDetails = " + principalDetails.getUser());
return "user";
}
순환참조
- 이렇게 수정을 하고 프로젝트를 실행해보면 순환참조가 발생할 것이다.
그 이유는 스프링은 각 객체를 싱글톤으로 관리하는데, SecurityConfig 객체를 생성하는 중에 @Bean으로 정의되어 있는 BCryptPasswordEncoder를 생성할 것이다.
이후에 SecurityConfig가 의존하고 있는 PrincipalOauth2UserService를 생성하는데, PrincipalOauth2UserService에서는 SecurityConfig의 BCryptPasswordEncoder를 참조하며 SecurityConfig를 의존하게 된다.
- 의존 관계를 따져보면
SecurityConfig가 PrincipalOauth2UserService를 참조하고,
PrincipalOauth2UserService가 SecurityConfig를 참조하기 때문에 순환 참조가 일어나는 것이다.
해결 방법
- SecurityConfig의 BCryptPasswordEncoder생성 부분을 주석처리 하고, 새로 클래스를 정의해 빈으로 등록해주자
@Component
public class CustomBCryptPasswordEncoder extends BCryptPasswordEncoder {
}
- 무식한 방법인데, application.properties에 순환 참조를 허용해주자
spring.main.allow-circular-references=true
- 순환참조를 해결하면 아래와 같이 정보를 가져오는 것을 확인할 수 있다.
- 일반 로그인
principalDetails = User(id=4, username=test,
password=$2a$10$BoYddAVEZUr6OWm2ttODaOdMykaawnD4zW11RLbSCQvZ6b8Yh7bJu, email=test@naver.com,
role=ROLE_USER, provider=null, providerId=null, loginDate=null, createDate=2023-04-03 15:37:02.575)
- 구글 로그인
principalDetails = User(id=8,
username=510342946043-hesh5cb2t1asij1ktqdvvp8mrik37ohk.apps.googleusercontent.com_105156291955329144943,
password=$2a$10$hzfnT3LPb859x4aucAOguekCYggb6Y9RzT/ttOt.Ah7yoV8TKH05G,
email=magicofclown@gmail.com, role=ROLE_USER,
provider=510342946043-hesh5cb2t1asij1ktqdvvp8mrik37ohk.apps.googleusercontent.com,
providerId=105156291955329144943, loginDate=null, createDate=2023-04-08 20:31:22.865)
loadUser와 loadUserByUsername을 오버라이드한 이유
- 우리가 loadUser와 loadUserByUsername을 오버라이드 하지 않아도 OAuth2가 자동으로 처리를 해주지만 굳이 오버라이드를 한 이유는
- 컨트롤러에서 PrincipalDetails로 매개변수를 받기 위함이고,
- 구글 로그인 진행시에 강제로 회원가입을 진행하기 위함이다.
- 이제 구글 로그인을 한적이 있는 user를 확인하기 위해 PrincipalOauth2UserService의 if문을 수정해보자
// 기존 코드에서 else만 추가함
if (userEntity == null) {
userEntity = User.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userRepository.save(userEntity);
}else {
System.out.println("구글 로그인을 이미 한적이 있습니다. 당신은 자동회원가입이 되어 있습니다.");
}
- 한번 구글로 로그인 한 user가 다시 로그인하면 위와 같은 결과가 출력된다.
- 이렇게 구글 로그인 시 회원가입을 자동으로 진행해 보았다!!!