Spring Security 구글 로그인 및 자동 회원가입

바그다드·2023년 4월 8일
0

Spring Security

목록 보기
9/17
  • 앞서 스프링 시큐리티 세션에 접근하기 위해 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 수정

  • user에 @Builder를 추가해주자
	@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를 참조하기 때문에 순환 참조가 일어나는 것이다.

해결 방법

  1. SecurityConfig의 BCryptPasswordEncoder생성 부분을 주석처리 하고, 새로 클래스를 정의해 빈으로 등록해주자
@Component
public class CustomBCryptPasswordEncoder extends BCryptPasswordEncoder {
}
  1. 무식한 방법인데, application.properties에 순환 참조를 허용해주자
spring.main.allow-circular-references=true
  • 순환참조를 해결하면 아래와 같이 정보를 가져오는 것을 확인할 수 있다.
  1. 일반 로그인
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)
  1. 구글 로그인
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가 자동으로 처리를 해주지만 굳이 오버라이드를 한 이유는
  1. 컨트롤러에서 PrincipalDetails로 매개변수를 받기 위함이고,
  2. 구글 로그인 진행시에 강제로 회원가입을 진행하기 위함이다.
  • 이제 구글 로그인을 한적이 있는 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가 다시 로그인하면 위와 같은 결과가 출력된다.
  • 이렇게 구글 로그인 시 회원가입을 자동으로 진행해 보았다!!!
profile
꾸준히 하자!

0개의 댓글