[22년도 하계 모각코] Springboot Security와 OAuth2.0을 활용한 소셜로그인(구글)<개발>

Kyunghwan Ko·2022년 8월 5일
3

22년도 하계 모각코

목록 보기
5/13

앞서 User엔티티와 User_temp엔티티로 나누고, com.cos.securityteam_project.beer_community로 패키지를 구분해서 진행했는데 이제 User엔티티와 team_porject.beer_community로 몰아서 원래프로젝트 상에서 진행하겠습니다.
따라서 파일구조는 다음과 같습니다.

그리고 앞으로 사용할 User 엔티티는 다음과 같습니다.

# domain/User.java

package team_project.beer_community.domain;

import com.sun.istack.NotNull;
import lombok.*;

import javax.persistence.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter @Setter //setter는 개발 단계동안 열어놓는다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)  //Spring data jpa 사용시 필요
@ToString(of = {"id", "username"})
@Table(name = "user")
public class User extends BaseTimeEntity{

    @Id @GeneratedValue
    private Long id;

    @NotNull
    private String email;

    @NotNull
    private String password;

    @NotNull
    private String username;

//    @NotNull
    private LocalDate birthday;

    private String imageUrl;

    // 로그인-회원가입 진행을 위해 추가한 필드
    private String role; // ROLE_USER, ROLE_ADMIN
    private String provider; // google, naver
    private String providerId; // 각 사이트에서 사용자별로 부여된 고유id

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) // User가 삭제되면 User가 작성한 댓글들도 다 삭제됨
    private List<Comment> comments = new ArrayList<>();

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<LikeBeer> likeBeers = new ArrayList<>();

    public User(String email, String password, String username) {
        this.email = email;
        this.password = password;
        this.username = username;
    }

    //==연관관계 편의 메소드==//
    public void addLikeBeer(LikeBeer likeBeer){
        likeBeers.add(likeBeer);
        likeBeer.setUser(this);
    }

    public void addComment(Comment comment){
        comments.add(comment);
        comment.setUser(this);
    }
}

로그인한 사용자의 정보 상세보기

이를 위해 config/auth패키지 안에 Spring Security에서 로그인한 사용자의 정보 상세보기를 위해 지원하는 UserDetailsUserDetailsService interface를 구현한
PrincipalDetailsPrincipalDetailsService class를 작성할 것입니다.

impelements 이기 때문에 Intellij기준 Ctrl+O 명령어를 통해 함수들을 오버라이딩해줍니다.

UserDetailsimplements한 이유는 Security Session에 저장할 수 있는 Authenctication 타입의 객체는 OAuth2User 혹은 UserDetails 타입의 객체만 담을 수 있습니다. 현재 프로젝트에서는 회원가입할 때 User타입의 객체가 필요한대, 앞서 언급한 OAuth2UserUserDetails에는 User타입의 객체를 받을 수 없기 때문에 아래와 같이 PrincipalDetails 클래스로 UserDetails를 구현함으로써 일반로그인한 사용자의 정보를 상세보기 할 수 있도록 하기위해 implements한 것 입니다.
(소셜로그인을 통한 사용자의 정보에 접근용이하게 하기위해 추후 OAuth2User 인터페이스도 implements할 예정입니다.)

UserDetails에 없는 필드인 User타입의 Composition변수로 user를 둡니다. 이를 생성자에서 초기화해줍니다.
현재 사용하지않는 부가적인 함수는 return true로 설정해둡니다.

# auth/PrincipalDetails
package team_project.beer_community.config.auth;


// Security가 "/login" 주소로 요청이 오면 낚아채서 로그인을 진행시킨다
// 로그인을 진행이 완료가 되면 Security session을 만들어준다(Security ContextHolder라는 키값에 넣어준다)
// security가 만드는 session에 들어갈 수 있는 Object가 정해져있다.
//  => Authentication 타입의 객체(안에 User정보가 있어야됨)
// User 객체 타입 -> UserDetails 타입 객체
// 즉, Security Session에 Authentication 객체가 있고
// 이 안에 UserDetails 타입의 객체가 있어서 이 것을 통해 User 객체에 접근할 수 o

import lombok.Data;
import team_project.beer_community.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

@Data
public class PrincipalDetails implements UserDetails {
    // UserDetails를 구현함으로써 PrincipleDetails는 UserDetails와 같은 타입이됬다.

    private User user; // 콤포지션 변수
    public PrincipalDetails(User user){
        this.user = user;
    }

    // 해당 user의 권한을 return하는 함수
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collect;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        // 1년동안 회원이 로그인을 안하면 휴먼계정으로 하기로 한다면?
        // model/User에서  loginDate(로그인한 시점 기록)라는 컬럼이 필요하다
        // logtime = user.getLogindDte
        // now_teim - logtime 이 1년을 초과하면 return false;
        return true;
    }
}

로그인을 통해 유저가 접속했을 때, 해당 유저를 repository에서 찾아주는 함수인 loadUserByUsername()를 오버라이딩합니다.

# auth/PrincipalDetailsService

package team_project.beer_community.config.auth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import team_project.beer_community.domain.User;
import team_project.beer_community.repository.UserRepository;

// Security에서 loginProcessUrl("/login"); 요청이 오면
// 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername 함수가 실행됨.

@Service // 해당 어노테이션을 통해 PrincipalDetailService 클래스를 IoC에 등록시킴
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;


    // Security seesion 안에있는 Authentication 타입객체의 안에 UserDetails 타입객체가 있다.
    // Security session(내부 Authentication(내부 UserDetails))
    // 아래의 함수는 UserDetails를 구현한 PrincipalDetails를 return한다
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);
        if(userEntity != null){
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
}

소셜로그인 후처리 절차

  1. 코드받기(인증)

  2. AccessToken(권한)

  3. 사용자프로필 정보가져옴

  4. 그 정보를 토대로 회원가입을 자동으로 진행시키기도 함
    4-1. (이메일, 전화번호,이름, 아이디)만 받았는대 -> 추가정보로(집주소, 닉네임)등등이 필요하다면 별도의 회원가입창을 띄워서 추가정보 입력받아야됨o

이를 수행하기 위해

config/oauth패키지를 생성하고 Spring Security에서 소셜로그인을 통해 접속한 사용자의 정보를 볼수 있는 함수(loadUser())를 제공하는 DefaultOAuth2UserService를 상속 후 함수를 오버라이딩한PrincipalOauth2UserService.java클래스를 생성합니다.

package team_project.beer_community.config.oauth;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Override // 구글소셜로그인 후 구글로 부터 받은 userRequest 데이터에 대한 후처리되는 함수
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        System.out.println("userRequest = " + userRequest);
        // org.springframwork.security.oauth2.client.userinfo.OAuth2UserRequest@4e6edb55
        System.out.println("getClientRegistration() = " + userRequest.getClientRegistration());
        // registrationId로 어떤 OAuth로 로그인 했는지 확인가능(ex. google, naver)
        System.out.println("getAccessToken = " + userRequest.getAccessToken().getTokenValue());
        // 구글로그인 버튼클릭 -> 구글로그인 창 -> 로그인을 완료 -> code를 return(OAuth-Client라이브러리) -> code를 사용해서 AccessToken을 요청해서 받는다.
        // 여기까지가 userRequest정보이다. -> loadUser() 함수호출 -> 구글로부터 회원프로필 얻을 수 있다.(ex. email, family_name 등등)

        System.out.println("getAttributes() = " + super.loadUser(userRequest).getAttributes());
        // {sub=101301106118139334837, name=고경환, given_name=경환, family_name=고, picture=https://lh3.googleusercontent.com/a-/AFdZucqfqgcr-H-cRolGyJETVNk, email=gkw1207@likelion.org, email_verified=true, locale=en, hd=likelion.org}
        // **회원가입할때 저장될 정보** => username: "google_101301106118139334837", password: "암호화(get in there)", email: "gkw1207@likelion.org", role: "ROLE_USER"

        OAuth2User oAuth2User = super.loadUser(userRequest);

        return super.loadUser(userRequest);
    }
}

어떤 정보들을 제공받을 수 있는지 확인하기 위해 sout을 많이 찍어보았습나다. 부가적으로 더 알고싶을 정보는 공식홈페이지를 이용해서 함수를 호출해보면 좋을 것입니다.

# config/auth/SecurityConfig.java

package team_project.beer_community.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import team_project.beer_community.config.oauth.PrincipalOauth2UserService;
// 소셜로그인 후처리 절치
// 1. 코드받기(인증) 2.AccessToken(권한) 3. 사용자프로필 정보가져옴 4-1.그 정보를 토대로 회원가입을 자동으로 진행시키기도 함
// 4-2. (이메일, 전화번호,이름, 아이디)만 받았는대 -> 추가정보로(집주소, 닉네임)등등이 필요하다면 별도의 회원가입창을 띄워서 추가정보 입력받아야됨o

@Configuration
@EnableWebSecurity // Spring Security 필터가 Spring 필터체인에 등록됨
//  지금부터 등록할 필터가 기본 필터에 등록이 된다.
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
// @Secured 어노테이션 활성화(controller에서 확인가능), @PreAuthorize 과 @PostAuthorize어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean // 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
    public BCryptPasswordEncoder encodePassword(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private PrincipalOauth2UserService principalOauth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/login")
//                .usernameParameter("userName") // username이 아니라 userName으로 param을 받고싶을때 사용o
                .loginProcessingUrl("/login") // login주소가 호출되면 Spring Security가 낚아채서 대신 로그인 진행
                .defaultSuccessUrl("/")
                .and()
                .oauth2Login()
                .loginPage("/login")
                .userInfoEndpoint()
                .userService(principalOauth2UserService); // 구글소셜로그인 성공 후 코드를 받는게 아니라 AccessToken+사용자프로필정보 바로 함께받는다(편리함)

    }
}

.defaultSuccessUrl("/")이후에 추가적으로 함수를 호출해줍니다. 이때 userService()함수에 principalOAuth2UserService(소셜로그인을 통해 로그인한 사용자의 정보에 접근할 수 있게 앞서 구현)를 param으로 전달해주면 소셜로그인을 통해 접속한 사용자의 정보를 얻을 수 있습니다.

소셜로그인을 통해 로그인한 사용자의 정보를 전달받아서 출력해보기 위해
controller/IndexController안에서 testLogin()함수를 해보겠습니다.
(참고: PrincipalDetails 클래스에서 @Data 어노테이션을 사용했기 때문에 .getUser()가능 그리고 PrincipalDetailsUserDetailsimplements했기 때문에 다형성에 의해 UserDetails 타입의 변수를 PrincipalDetails로 다운캐스팅이 가능합니다.)

    @GetMapping("/test/login")
    public @ResponseBody String testLogin(Authentication authentication, @AuthenticationPrincipal PrincipalDetails userDetails){// DI(의존성주입)
        // @AuthenticationPrincipal이라는 어노테이션을 통해서 세션정보를 받을 수 있다.(원래는 UserDetails userDetails 인데 UserDetails를 구현한 PrincipalDetails로 타입으로 지정해도 무방 -> .getUser()사용하기 위함)
        System.out.println("=====IndexController.testLogin====");
        //System.out.println("authentication.getPrincipal() = " + authentication.getPrincipal()); // authentication.getPrincipal() = PrincipalDetails(user=User(id=1, username=ko1))
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); // .getPrincipal()이 Obeject타입이기 때문에 PrincipalDetails타입으로 다운캐스팅한다.
        System.out.println("principalDetails.getUser() = " + principalDetails.getUser()); // principalDetails.getUser() = User(id=1, username=ko1) -> User Entity에서 toString을 ({"id", "username"})만 했기 때문임.
        
        System.out.println("userDetails.getUser() = " + userDetails.getUser());
        // userDetails.getUser() = User(id=1, username=ko1) -> 위와 동일함
        return "세션 정보 확인하기";
    }

위에서 확인할 수 있듯이, testLogin()함수의 param으로 전달된 (1)Authentication authenctication(2)@AuthenticationPrincipal PrincipalDetails userDetails 을 통해 .getUser()한 결과를 보면 모두 동일하게 로그인한 사용자의 정보에 접근할 수 있습니다.

소셜로그인한 사용자 정보보기

이 상태에서 서버를 실행시키고 일반로그인을 통해서 로그인 후 /test/login 주소로 접근가능하지만 소셜로그인을 통해 로그인한 후 /test/login으로 접근하면
500에러가 뜨면서 아래와 같이 ClassCastException이 발생하는 것을 볼 수 있습니다.
(PrincipalDetails) 하는 부분에서 에러가 발생한것 입니다.

2022-07-30 15:13:35.765 ERROR 8316 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: class org.springframework.security.oauth2.core.user.DefaultOAuth2User cannot be cast to class team_project.beer_community.config.auth.PrincipalDetails (org.springframework.security.oauth2.core.user.DefaultOAuth2User is in unnamed module of loader 'app'; team_project.beer_community.config.auth.PrincipalDetails is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @4aa7b866)] with root cause

java.lang.ClassCastException: class org.springframework.security.oauth2.core.user.DefaultOAuth2User cannot be cast to class team_project.beer_community.config.auth.PrincipalDetails (org.springframework.security.oauth2.core.user.DefaultOAuth2User is in unnamed module of loader 'app'; team_project.beer_community.config.auth.PrincipalDetails is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @4aa7b866)

따라서 별도의 함수를 추가작성해보겠습니다.

controller/IndexController.java에서

# controller/IndexController
...

@GetMapping("/test/oauth/login")
    public @ResponseBody String testOAuthLogin(Authentication authentication ,@AuthenticationPrincipal OAuth2User oauth){// DI(의존성주입)
        // @AuthenticationPrincipal이라는 어노테이션을 통해서 세션정보를 받을 수 있다.
        System.out.println("=====IndexController.testLogin====");
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
        // oAuth2User.getAttributes() = {sub=103489475512635244738, name=‍고경환[재학 / 정보통신공학과], given_name=고경환[재학 / 정보통신공학과], family_name=‍, profile=https://plus.google.com/103489475512635244738, picture=https://lh3.googleusercontent.com/a/AItbvmlIUxyycyZvUHNNhzX20-5mvGrmrDbw6G1_Ylqn=s96-c, email={이메일}, email_verified=true, locale=ko, hd=hufs.ac.kr}
        System.out.println("oauth.getAttributes() = " + oauth.getAttributes());
        // oauth.getAttributes() = {sub=103489475512635244738, name=‍고경환[재학 / 정보통신공학과], given_name=고경환[재학 / 정보통신공학과], family_name=‍, profile=https://plus.google.com/103489475512635244738, picture=https://lh3.googleusercontent.com/a/AItbvmlIUxyycyZvUHNNhzX20-5mvGrmrDbw6G1_Ylqn=s96-c, email={이메일}, email_verified=true, locale=ko, hd=hufs.ac.kr}
        return "세션 정보 확인하기";
    }

타입캐스팅 부분을 (OAuth2User)로 변경해주면 에러없이 잘 나오는 것을 볼 수 있습니다.
이 정보들은 앞서 작성했던 oauth/PrincipalOauth2UserServiceloadUser()함수에서 .getAttributes()를 호출한 결과와 동일함.

(참고: 이 경로(/test/oauth/login)는 일반로그인한 사용자가 접근하게 되면 500에러와 앞서 봤던 ClassCastException이 발생합니다.)

일반로그인과 소셜로그인 시 발생하는 타입 문제 해결

그렇다면 소셜로그인을 할때는 OAuth2User로 일반로그인을 하게되면 UserDetails로 타입을 따로 구분해서 찾아야만 하는 걸까?

이 문제는 인터페이스 다중구현(Multi-Implementation)을 통해 해결할 수 있습니다.
Security가 관리하는 세션인 Security Session에서 Authentication 타입의 객체는 OAuth2User와 UserDetails타입의 객체만 받을 수 있습니다. 하지만 PrincipialDetails가 두 인터페이스를 모두 구현한다면 일반로그인, 소셜로그인 구분없이 같은 타입(PrincipalDetails)의 객체로 받아서 처리할 수 있습니다.

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
    // UserDetails를 구현함으로써 PrincipleDetails는 UserDetails와 같은 타입이됬다.

    private User user; // 콤포지션 변수
    
    ...
    
    public PrincipalDetails(User user){
        this.user = user;
    }
    
    @Override
    public String getName() {
        return null;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return null;
    }

다중구현하기 위해 Map<String, Objec>을 return 하는 getAttributes()getName()함수를 override 하면됩니다.

일반로그인과 소셜로그인 시 입력되는 정보가 다르기 때문에 이 차이를 생성자를 다르게 주어 처리하였고, Map<String, Oject>타입을 return하는 getAttributes()함수는 구글로부터 받은 사용자의 정보들을 Map<String, Object>타입으로 return합니다. 그리고
getName()은 소셜로그인한 사용자의 성명을 return합니다.

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
   // UserDetails를 구현함으로써 PrincipleDetails는 UserDetails와 같은 타입이됬다.

   private User user; // 콤포지션 변수

   private Map<String, Object> attributes;
   //일반 로그인할때 사용하는 생성자
   public PrincipalDetails(User user){
       this.user = user;
   }
   // 소셜로그인(OAuth2.0사용)할때 사용하는 생성자
   public PrincipalDetails(User user, Map<String, Object> attributes){
       this.user = user; this.attributes = attributes;
   }

   @Override
   public String getName() {
       return (String) attributes.get("name"); // name키에 저장된 값은 google에서 해당 유저의 성명에 해당함(given_name은 이름, family_name은 성)
   }

   @Override
   public Map<String, Object> getAttributes() {
       return attributes;
   }

   // 해당 user의 권한을 return하는 함수
   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
       Collection<GrantedAuthority> collect = new ArrayList<>();
       collect.add(new GrantedAuthority() {
           @Override
           public String getAuthority() {
               return user.getRole();
           }
       });
       return collect;
   }

로그인 후처리를 위해 PrincipalOauth2UserService 에 코드를 추가로 작성합니다.

먼저 회원가입을 통해 User를 저장하기 위해 필요한 정보를 한번에 넣을 수 있는 생성자를 생성 후 작성해보겠습니다.

# domain/User.java

...
   @Builder
   public User(Long id, String email, String password, String username, LocalDate birthday, String imageUrl, String role, String provider, String providerId) {
       this.id = id;
       this.email = email;
       this.password = password;
       this.username = username;
       this.birthday = birthday;
       this.imageUrl = imageUrl;
       this.role = role;
       this.provider = provider;
       this.providerId = providerId;
   }
# oauth/PrincipalOAuth2UserService.java

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
   @Autowired
   private BCryptPasswordEncoder bCryptPasswordEncoder; // password암호화할때 사용됨
   @Autowired
   private UserRepository userRepository; // User객체 저장하기 위해 사용됨

   @Override // 구글소셜로그인 후 구글로 부터 받은 userRequest 데이터에 대한 후처리되는 함수
   public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

       OAuth2User oAuth2User = super.loadUser(userRequest);
       System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
       // {sub=101301106118139334837, name=고경환, given_name=경환, family_name=고, picture=https://lh3.googleusercontent.com/a-/AFdZucqfqgcr-H-cRolGyJETVNk, email=gkw1207@likelion.org, email_verified=true, locale=en, hd=likelion.org}
       // **회원가입할때 저장될 정보** => username: "google_101301106118139334837", password: "암호화(get in there)", email: "gkw1207@likelion.org", role: "ROLE_USER"

       String provider = userRequest.getClientRegistration().getClientId(); // google
       String providerId = oAuth2User.getAttribute("sub"); // sub키에 저장된 값은 google에서 사용자에게 부여한 pk이다
       String username = oAuth2User.getAttribute("name");
       String password = bCryptPasswordEncoder.encode("password") ; // 소셜로그인이기 때문에 굳이 저장안해도되지만 임의로 생성해서 저장함
       String email = oAuth2User.getAttribute("email");
       String role = "ROLE_USER";

       User userEntity = userRepository.findByUsername(username);
       if(userEntity == null){
           // User에 생성자를 통해 새로운 User를 생성시킴(회원가입)
           userEntity = User.builder()
                   .username(username)
                   .password(password)
                   .email(email)
                   .role(role)
                   .provider(provider)
                   .providerId(providerId)
                   .birthday(null)
                   .imageUrl(null)
                   .build();
           userRepository.save(userEntity);
       }
       // 회원가입이 이미 되어있다면 그냥 앞서받은 userEntity사용해도 됨
       return new PrincipalDetails(userEntity, oAuth2User.getAttributes()); // Authentication에 저장된다.
   }
}

위 코드에서 loadUser()함수의 return타입이 OAuth2User이지만 PrincipalDetails타입으로 return가능한 이유는 PrincipalDetailsOAuth2Userimplements했기 때문입니다.

그리고 마지막으로 url매핑을 위해 controller/IndexController.java 코드를 수정하겠습니다.

...
// OAuth로그인을 해도 PrincipalDetails로 받을 수 있고
// 일반로그인을 해도 PrincipalDetails로 받을 수 있다.
@GetMapping("/user")
   public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails){
       System.out.println("principalDetails = " + principalDetails); // 소셜로그인 or 일반로그인을 해도 동일하게 출력됨.
       return "user";
   }

소셜로그인으로 한것과 일반로그인으로 했을때 결과가 동일한 것을 볼 수 있다.

@AuthenticationPrincipal은 언제 만들어지는가?

앞서 구현한 PrincipalDetailsService 클래스와 PrincipalOAuth2UserService 클래스를 보면 각각 함수를 overriding한 것을 볼 수 있다. 하지만 굳이 해당함수들을 overriding하지 않아도 자동으로 불린다. 이를 통해 return된 객체가 Authentication에 저장되고 이때 @AuthenticationPrincipal 어노테이션이 생성되는 것입니다.

(각 함수를 overriding한 이유는 return타입을 PrincipalDetails로 맞추고, 내부적으로 회원가입을 진행하는 로직을 추가하기 위함.)

The dependencies of some of the beans in the application context form a cycle 에러해결

서버를 실행시킬 때, 아래와 같이 에러가 발생할 수 있습니다.

말그대로 애플리케이션 컨텍스트에서 Bean에 등록된 dependency들이 순환되고 있다는 에러입니다.
에러에 명시된 대로 application.yml파일을 수정해보겠습니다.

spring:
 datasource:
   url: jdbc:mysql://localhost:3306/beer_community?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
   username: root
   password: {비밀번호}
   driver-class-name: com.mysql.cj.jdbc.Driver
 main:
   allow-circular-references: true

spring.main.allow-circular-referencestrue로 설정했습니다.

시연

일반 로그인과 소셜로그인이 정상적으로 작동하고,

기존에 회원가입되있지 않는 구글계정으로 로그인을 시도하면 위와같이 insert 쿼리문이 실행되면서 DB에서 확인할 수 있습니다.

profile
부족한 부분을 인지하는 것부터가 배움의 시작이다.

0개의 댓글