[SpringBoot] OAuth2 활용해서 Google login 구현하기(2) - 스프링 시큐리티 적용하기

zooju·2023년 1월 18일

이제 Spring에서 구글 로그인 구현을 해보자!

3. Spring에서 User 관련 Class 만들기

먼저 domain 패키지에 User과 Role 도메인을 만든다.

User는 사용자 정보를 담당할 도메인이다.

User.java

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들을 작성했다.

위의 정보중에서

  • socialId
  • profileImg
  • role

이렇게 세 column은 구글 회원가입을 통해 얻은 데이터로 채울 것이다.

Builder

builder는 객체를 생성하게 도와준다.

처음에 builder가 없을때는 직접 entity에 @Setter 어노테이션을 통해 데이터를 입력해서 객체를 생성했는데 이것이 굉장히 불편하기도 하고 안정성에서도 떨어진다고 들어서 builder 어노테이션을 통해 객체를 생성한다.

  • setter를 통해 객체를 생성할 때
    User user = new User();
    user.setName = "name";
    user.setProfileImg = "ImgUrl";
  • ArgConstructor를 통해 객체를 생성할 때
User user = new User("name", "ImgUrl");

setter를 사용할 때 보다 편리하지만, 순서를 지켜야하기 때문에 인자가 많아지면 뭐가 뭔지 이해하기 힘들어지는 것 같다.

  • Builder를 통해 객체를 생성할 때
User user = User.builder()
						.name("name")
						.profileImg("profileImg")

훨씬 명시적이고 가독성이 높다.

Role.java

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_이 앞에 있어야 한다. 그래서 코드별 키 값을 지정해야 한다.

UserRepository.java

마지막으로 User Repository도 작성해준다.

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

findByEmail 을 만들어 놔야 나중에 email을 통해 이미 생성된 사용자인지 여부를 가려낼 수 있다.


4. Spring에서 세큐리티 관련 로직 구현하기

1) build.gradle

// 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 를 추가해준다.

2) config.auth 패키지 생성

뒤에 나오는 SecurityConfig, CustomOAuth2UserService 클래스 등 모든 세큐리티 관련 클래스를 이곳에 담을 예정이다.

SecurityConfig.java

@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
    • antMatchers로 설정된 URL 외의 나머지 URL에 대한 설정
    • authenticated() 옵션으로 인증된 사용자, 즉 로그인한 사용자들에게만 열람 권한 부여
  • logout()
    • 로그아웃 기능에 대한 설정의 시작점
    • logoutSuccessUrl("/")은 로그아웃 성공시 "/" 주소로 이동을 의미
  • auth2Login()
    • OAuth2 로그인 기능에 대한 설정의 시작점
    • userInfoEndpoint()은 로그인 성공 후 사용자 정보를 가져올 때의 설정을 담당
    • userService()은 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록여기서는 customOAuth2UserService를 UserService 인터페이스의 구현체로 등록리소스 서버(구글, 네이버, 카카오 등)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능 명시 가능

CustomOAuth2UserService.java

이 클래스에서는 구글 로그인, 회원가입 이후 가져온 사용자의 정보들을 기반으로 가입 및 정보수정, 세션 저장 등의 기능을 지원한다.

@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**
    • 현재 로그인 진행 중인 서비스를 구분해준다. ex)구글, 네이버 등
    • 구글의 소셜로그인만 구현할 때는 필요 없지만, 이후 다른 플랫폼의 로그인을 연동할 때는 구글 로그인인지 네이버 로그인인지 구분하기 위해 사용해야 한다.
    • 구글 로그인인 지금은 “google”라는 값을 가진다.
  • userNameAttributeName
    • OAuth2 로그인 진행 시 키가 되는 필드값으로 Primary Key와 같은 의미이다.
    • 구글은 "sub"이라는 기본 코드를 제공하지만, 네이버와 카카오는 기본 지원하지 않는다.
    • 네이버 로그인과 구글 로그인을 동시 지원할 때 사용된다.
  • OAuthAttributes
    • OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스이다.
  • **SessionUser**
    • 세션에 사용자 정보를 저장하기 위한 Dto 클래스이다.
  • **saveOrUpdate()**
    • 이메일을 통해 사용자 데이터가 DB에 있는지 확인한다.
    • DB에 데이터가 이미 있고 + 구글 사용자 정보가 업데이트 된 경우 유저의 정보를 업데이트한다.
    • 유저 정보가 없다면 유저 정보를 새로 저장한다.

OAuthAttributes.java

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;
    }
  • of()
    • OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야 한다.
    • 지금은 google login 하나뿐이라서 바로 ofGoogle을 반환해주지만, 이후에 다른 플랫폼도 추가되면 registrationId에 따라 함수를 만들어줘야 할 것 같다.
  • ofGoogle()
    • 구글 로그인인 경우 이 함수를 통해 User를 만들어낸다.
    • 프로덕트 상에서 userId가 unique 해야한다는 제약사항이 있어서, unique한 값을 만들어내는 함수를 따로 만들었다.
  • toEntity()
    • User Entity를 생성한다.
    • OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입 시점이다.
    • 가입 시 기본 권한을 USER로 주기 위해 role 빌더 값에 Role.USER 값 입력

프로덕트 User 모델에 맞추기 위해 이 클래스의 코드를 책의 코드에서 많이 수정했다.

SessionUser.java

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();
    }
}

이렇게 모든 서버 세큐리티 설정을 끝냈다.

회원가입/로그인 테스트를 해보자


5. 회원가입/로그인 테스트하기

Application을 실행시키고 주소창에 위에 설정했던 주소를 넣는다.

http://localhost:8080/oauth2/authorization/google

회원가입 테스트

주소를 넣으면 구글 로그인 화면이 잘 나오고

계정을 선택하면 회원가입이 완료된다.

DB에도 원했던 대로 정보가 잘 입력된것을 확인할 수 있다.

(console창에 뜬 sql 문)

회원가입과 로그인 모두 잘 된다.

REFERENCE.

Spring Security와 OAuth2(2) - 구글 서비스 등록하고 적용하기

profile
이것 저것 새로운 분야에 관심이 많은 서버 개발자

0개의 댓글