앞서 User
엔티티와 User_temp
엔티티로 나누고, com.cos.security
와 team_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에서 로그인한 사용자의 정보 상세보기를 위해 지원하는 UserDetails
와 UserDetailsService
interface를 구현한
PrincipalDetails
와 PrincipalDetailsService
class를 작성할 것입니다.
impelements
이기 때문에 Intellij기준 Ctrl+O
명령어를 통해 함수들을 오버라이딩해줍니다.
UserDetails
를implements
한 이유는 Security Session에 저장할 수 있는Authenctication
타입의 객체는OAuth2User
혹은UserDetails
타입의 객체만 담을 수 있습니다. 현재 프로젝트에서는 회원가입할 때User
타입의 객체가 필요한대, 앞서 언급한OAuth2User
와UserDetails
에는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;
}
}
코드받기(인증)
AccessToken(권한)
사용자프로필 정보가져옴
그 정보를 토대로 회원가입을 자동으로 진행시키기도 함
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()
가능 그리고 PrincipalDetails
는 UserDetails
를 implements
했기 때문에 다형성에 의해 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/PrincipalOauth2UserService
의 loadUser()
함수에서 .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가능한 이유는PrincipalDetails
가OAuth2User
를implements
했기 때문입니다.
그리고 마지막으로 url매핑을 위해 controller/IndexController.java
코드를 수정하겠습니다.
...
// OAuth로그인을 해도 PrincipalDetails로 받을 수 있고
// 일반로그인을 해도 PrincipalDetails로 받을 수 있다.
@GetMapping("/user")
public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails){
System.out.println("principalDetails = " + principalDetails); // 소셜로그인 or 일반로그인을 해도 동일하게 출력됨.
return "user";
}
소셜로그인으로 한것과 일반로그인으로 했을때 결과가 동일한 것을 볼 수 있다.
앞서 구현한 PrincipalDetailsService 클래스와 PrincipalOAuth2UserService 클래스를 보면 각각 함수를 overriding한 것을 볼 수 있다. 하지만 굳이 해당함수들을 overriding하지 않아도 자동으로 불린다. 이를 통해 return된 객체가 Authentication에 저장되고 이때 @AuthenticationPrincipal 어노테이션이 생성되는 것입니다.
(각 함수를 overriding한 이유는 return타입을 PrincipalDetails로 맞추고, 내부적으로 회원가입을 진행하는 로직을 추가하기 위함.)
서버를 실행시킬 때, 아래와 같이 에러가 발생할 수 있습니다.
말그대로 애플리케이션 컨텍스트에서 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-references
를 true
로 설정했습니다.
일반 로그인과 소셜로그인이 정상적으로 작동하고,
기존에 회원가입되있지 않는 구글계정으로 로그인을 시도하면 위와같이 insert 쿼리문이 실행되면서 DB에서 확인할 수 있습니다.