OAuth2 Google Login

스프링 시큐리티 설정

  • build.gradle에 스프링 시큐리티 관련 의존성 추가
    1. spring-boot-starter-oauth2-client
      • 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성
      • spring-boot-oauth2-client와 spring-security-oauth2-jose를 기본으로 관리
dependencies {
    // ...
    compile('org.springframework.boot:spring-boot-starter-oauth2-client')

    // test
}

사용자 관련 클래스 생성

  • Dto 분류
    • 권한
    • 사용자 정보

사용자 권한(User Authrization)을 위한 Role 클래스 생성

  • Role 클래스
    • 각 사용자의 권한을 관리할 Enum 클래스
    • 스프링 시큐리티에서는 권한 코드에 항상 "ROLE_" 이 앞에 있어야 한다.
    • 코드별 키 값을 ROLE_GUEST, ROLE_USER 등으로 지정
@Getter
@RequiredArgsConstructor
public enum Role {

    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반 사용자");

    private final String key;
    private final String title;
}

사용자 정보(User) 담당할 도메인 클래스 생성

  • User

    • @Enumerated(EnumType.STRING)

      • JPA를 이용해 데이터베이스로 저장할 때 Enum 값을 어떤 형태로 저장할지를 결정
      • 기본적으로 int로 된 숫자가 저장된다.
      • 숫자로 저장되면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는 지 알 수가 없다.
      • 문자열(EnumType.STRING)로 저장될 수 있도록 선언
@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

    public User update(String name, String picture) {
        this.name = name;
        this.picture = picture;

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}

사용자관련 CRUD 작업할 UserRepository 생성

  • UserRepositry
    • findByEmail
      • 소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위한 메서드
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

시큐리티 관련 패키지 및 클래스 생성

  • config.auth 패키지 생성

    • 시큐리티 관련 클래스 관리

    • SessionUser

    • OAuthAttributes

    • CustomOAuth2UserService

    • SecurityConfig

SessionUser 생성

  • SessionUser

    • 인증된 사용자 정보를 저장하는 클래스
    • User클래스 내에 세션 정보를 가질 수 없는 이유
  • SessionUser 클래스의 이유

    • User 클래스에 Session을 저장하려는 경우, User 클래스에 직렬화를 하지 않았기 때문에 오류가 발생한다.
    • User 클래스에 직렬화 코드를 넣는다면 ?
      • User 클래스는 Entity 클래스이기 때문에 문제가 발생할 가능성이 높다.
      • Entity 클래스는 언제 다른 엔티티와 관계가 형성될 지 모르기 때문이다.
      • @OneToMany, @ManyToMany 등 자식 엔티티를 갖고 있다면 직렬화 대상에 자식들까지 포함되니 성능 이슈, 부수 효과가 발생할 확률이 높다.
      • 직렬화 기능을 가진 세션 DTO를 하나 추가로 만드는 것이 이후 운영 및 유지보수에 도움이 된다.
@Getter
public class SessionUser implements Serializable {
    private String name;
    private String email;
    private String picture;

    public SessionUser(User user) {
        this.name = user.getName();
        this.email = user.getEmail();
        this.picture = user.getPicture();
    }
}

OAuthAttributes 생성

  • OAuthAttributes 클래스
    1. of()
      • OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야 한다.
    2. toEntity()
      • User 엔티티를 생성
      • OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때이다.
      • 가입할 때의 기본 권한을 GUEST로 주기 위해서 role 빌더 값에는 Role.GUEST를 사용
@Getter
public class OAuthAttributes {
    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;

    @Builder
    public OAuthAttributes(Map<String, Object> attributes
            , String nameAttributeKey
            , String name
            , String email
            , String picture) {
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

    public static OAuthAttributes of(
            String registrationId
            , String userNameAttributeName
            , Map<String, Object> attributes) {

        return ofGoogle(userNameAttributeName, attributes);
    }

    public static OAuthAttributes ofGoogle(
            String userNameAttributeName
            , Map<String, Object> attributes) {

        return OAuthAttributes.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .picture((String) attributes.get("picture"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    public User toEntity() {
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
}

CustomOAuth2UserService 생성

  • CustomOAuth2UserService
    1. registrationId
      • 현재 로그인 진행 중인 서비스를 구분하는 코드
      • 추후 네이버 로그인 연동 시에 네이버 로그인인지, 구글 로그인인지 구분하기 위한 값
    2. userNameAttributeName
      • OAuth2 로그인 진행 시 키가 되는 필드값을 이야기한다.
      • Primary Key와 같은 의미
      • 구글의 경우 기본적으로 코드를 지원하지만, 네이버 카카오 등은 기본 지원하지 않는다.
      • 구글의 기본 코드는 'sub'
      • 이후 네이버 로그인과 구글 로그인을 동시 지원할 때 사용
    3. OAuthAttributes
      • OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
      • 이후 네이버 등 다른 소셜 로그인도 이 클래스를 사용
    4. SessionUser
      • 세션에 사용자 정보를 저장하기 위한 Dto 클래스
@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 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.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(
                        attributes.getName()
                        , attributes.getPicture())
                ).orElse(attributes.toEntity());

        return userRepository.save(user);
    }
}

Security Config 클래스 생성

  • SecurityConfig 클래스
    1. @EnableWebSecurity
      • Spring Security 설정들을 활성화
    2. http.csrf().disable().headers().frameOptions().disable()
      • h2-console 화면을 사용하기 위해 해당 옵션을 disable
    3. .authorizeRequests()
      • URL별 권한관리를 설정하는 옵션의 시작점
      • authorizeRequests가 선언되어야 antMatchers 옵션 사용 가능
    4. .antMatchers()
      • 권한 관리 대상을 지정하는 옵션
      • URL, HTTP 메서드 별로 관리가 가능
      • "/" 등 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 권한 부여
      • POST 메서드이면서 "/api/v1/**" 주소를 가진 API는 USER 권한을 가진 사람만 가능하도록 부여
    5. andRequest
      • 설정된 값을 이외 나머지 URL들을 나타낸다.
      • 여기서는 authenticated() 추가하여 나버지 URL들은 모두 인증된 사용자들에게만 허용하도록 한다.
      • 인증된 사용자 즉, 로그인한 사용자들을 이야기 함
    6. logout().logoutSuccessUrl("/")
      • 로그아웃 기능에 대한 여러 설정의 진입점
      • 로그아웃 성공 시 "/" 주소로 이동
    7. oauth2Login
      • OAuth2 로그인 기능에 대한 여러 설정의 진입점
    8. userInfoEndpoint
      • OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
    9. userService
      • 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록
      • 리소스 서버(즉, 소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있다.
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    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);
    }
}

+ Recent posts