이제 Spring에서 구글 로그인 구현을 해보자!
먼저 domain 패키지에 User과 Role 도메인을 만든다.
User는 사용자 정보를 담당할 도메인이다.
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String userId;
@Column(nullable = false)
private String profileName;
private String profileImg;
@Column(unique = true)
private String socialId;
@Enumerated(EnumType.STRING)
private SocialPlatformEnum socialPlatform;
@Enumerated(EnumType.STRING)
@Column(nullable = true)
private Role role;
@Builder
public User(String userId, SocialPlatformEnum socialPlatform, String socialId, String profileImg, String profileName, Role role){
this.userId = userId;
this.profileName = profileName;
this.profileImg = profileImg;
this.socialId = socialId;
this.socialPlatform = socialPlatform;
this.role = role;
}
public User update(String profileName, String profileImg) {
this.profileName = profileName;
this.profileImg = profileImg;
return this;
}
public String getRoleKey(){
return this.role.getKey();
}
}
Column
내 프로젝트에서 User 테이블에 필요한 column들을 작성했다.
위의 정보중에서
이렇게 세 column은 구글 회원가입을 통해 얻은 데이터로 채울 것이다.
Builder
builder는 객체를 생성하게 도와준다.
처음에 builder가 없을때는 직접 entity에 @Setter 어노테이션을 통해 데이터를 입력해서 객체를 생성했는데 이것이 굉장히 불편하기도 하고 안정성에서도 떨어진다고 들어서 builder 어노테이션을 통해 객체를 생성한다.
User user = new User();
user.setName = "name";
user.setProfileImg = "ImgUrl";
User user = new User("name", "ImgUrl");
setter를 사용할 때 보다 편리하지만, 순서를 지켜야하기 때문에 인자가 많아지면 뭐가 뭔지 이해하기 힘들어지는 것 같다.
User user = User.builder()
.name("name")
.profileImg("profileImg")
훨씬 명시적이고 가독성이 높다.
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
스프링 시큐리티에서는 권한 코드에 항상 ROLE_이 앞에 있어야 한다. 그래서 코드별 키 값을 지정해야 한다.
마지막으로 User Repository도 작성해준다.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
findByEmail
을 만들어 놔야 나중에 email을 통해 이미 생성된 사용자인지 여부를 가려낼 수 있다.
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-client', version: '2.5.4'
build.gradle에 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현시 필요한 의존성인 Spring-boot-starter-oauth2-client 를 추가해준다.
뒤에 나오는 SecurityConfig, CustomOAuth2UserService 클래스 등 모든 세큐리티 관련 클래스를 이곳에 담을 예정이다.
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
protected void configure(HttpSecurity http) throws Exception{
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
EnableWebSecurity
: spring security 설정들을 활성화 해준다.csrf().disable().headers().frameOptions().disable()
: • h2-console 화면을 사용하기 위해 해당 옵션들을 disable.authorizeRequests()
: URL별 권한 관리를 설정하는 옵션의 시작점이다. 이게 선언되어야만 antMatchers 옵션을 사용할 수 있다..antMatchers()
: 권한 관리 대상을 지정하는 옵션이다. • URL, HTTP 메서드별로 관리 가능하고 • "/", "/h2-console/** " 등 지정된 URL은 permitAll() 옵션을 통해 전체 열람 권한 부여할 수 있다. "/api/v1/** " 주소를 가진 API는 USER 권한만 열람 권한 부여anyRequest
logout()
auth2Login()
이 클래스에서는 구글 로그인, 회원가입 이후 가져온 사용자의 정보들을 기반으로 가입 및 정보수정, 세션 저장 등의 기능을 지원한다.
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest,OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oauth2User = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oauth2User.getAttributes());
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())), attributes.getAttributes(), attributes.getNameAttributeKey());
}
private User saveOrUpdate(OAuthAttributes attributes){
User user = userRepository.findBySocialId(attributes.getEmail())
.map(entity->entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
**registrationId**
userNameAttributeName
OAuthAttributes
**SessionUser**
**saveOrUpdate()**
OAuth2UserService를 통해 가져온 OAuth2User의 속성을 담는 클래스이다.
@Getter
public class OAuthAttributes {
private Map<String,Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
private SocialPlatformEnum socialPlatform;
private String userId;
@Builder
public OAuthAttributes(Map<String,Object> attributes, String nameAttributeKey, String name, String email, String picture, SocialPlatformEnum socialPlaform, String userId){
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
this.socialPlatform = socialPlaform;
this.userId = userId;
}
public static OAuthAttributes of(String registrationId,
String userNameAttributeName,
Map<String, Object> attributes) {
return ofGoogle(registrationId, userNameAttributeName, attributes);
}
private static OAuthAttributes ofGoogle(String userNameAttributeName,
Map<String, Object> attributes) {
// 1. social platform 추가
SocialPlatformEnum socialPlatform = SocialPlatformEnum.GOOGLE;
// 2. user ID 랜덤 생성 추가
String userId = createDefaultUserIdWithEmail((String) attributes.get("email"));
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.socialPlaform(socialPlatform)
.userId(userId)
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
public User toEntity(){
return User.builder()
.socialId(email)
.profileName(name)
.profileImg(picture)
.socialPlatform(socialPlatform)
.userId(name)
.role(Role.USER)
.build();
}
public static String createDefaultUserIdWithEmail(String email){
String[] split1_ = email.split("@");
String[] split2_ = split1_[1].split("\\.");
String id = split1_[0];
String platform = split2_[0];
return id + "_" + platform;
}
프로덕트 User 모델에 맞추기 위해 이 클래스의 코드를 책의 코드에서 많이 수정했다.
Session User는 인증된 사용자 정보만이 필요하다. 그렇기에 name, email, picture만 필드로 선언한다.
@Getter
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
public SessionUser(User user) {
this.name = user.getProfileName();
this.email = user.getSocialId();
this.picture = user.getProfileImg();
}
}
이렇게 모든 서버 세큐리티 설정을 끝냈다.
회원가입/로그인 테스트를 해보자
Application을 실행시키고 주소창에 위에 설정했던 주소를 넣는다.
http://localhost:8080/oauth2/authorization/google
주소를 넣으면 구글 로그인 화면이 잘 나오고
계정을 선택하면 회원가입이 완료된다.
DB에도 원했던 대로 정보가 잘 입력된것을 확인할 수 있다.
(console창에 뜬 sql 문)
회원가입과 로그인 모두 잘 된다.
REFERENCE.