[ Spring Boot ] Google 로그인 기능 구현하는 방법

jwkwon0817·2023년 9월 2일
2

Web Back-end

목록 보기
4/26
post-thumbnail

Spring Boot 3에서 Google 로그인 기능을 구현하려면 먼저 Google OAuth에서 인증키를 받아와야 합니다.

인증키를 받아오는 방법은 다음 글에 자세하게 설명되어있습니다.

Google OAuth 인증키 발급

인증키 발급하는 방법 확인하기


application 설정 파일이 있는 경로에 application-oauth.properties 또는 application-oauth.yml 파일을 생성합니다.

application-oauth.yml

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: Your Client ID
            client-secret: Your Client Secure Password
            scope: profile,email

application-oauth.properties

spring.security.oauth2.client.registration.google.client-id=Your Client ID
spring.security.oauth.client.registration.google.client-secret=Your Client Secure Password
spring.security.oauth.client.registration.google.scope=profile,email

그리고 application 설정 파일에 다음 내용을 추가합니다.

application.yml

spring:
  profiles:
    include: oauth

application.properties

spring.profiles.include=oauth

그리고 config.auth.dto 패키지를 추가합니다.

config.auth.dto 안에 OAuthAttributesSessionUser 클래스를 만들어주고, config.auth 안에 CustomOAuth2UserServiceSecurityConfig 클래스를 만들어줍니다.


그리고 다음과 같이 프로젝트 패키지 아래 user.entities, user.enums, user.repositories 패키지를 만듭니다.

그 다음에 각각 User(Class), Role(Enum), UserRepository(Interface)를 만들어줍니다.


그리고 프로젝트 패키지 아래 entities 패키지를 만들고 BaseTimeEntity 추상 클래스를 만들어주고 아래 코드를 입력해 줍니다.

package me.jwkwon0817.springstudy.web.entities;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter // Getter 자동 생성
@MappedSuperclass // 이 추상 클래스를 상속하는 Entity 클래스에서 아래 필드를 자동으로 Column으로 등록
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    @CreatedDate // 데이터가 입력되는 시각
    private LocalDateTime createdDate;
	
    @LastModifiedDate // 데이터가 수정된 된
    private LocalDateTime modifiedDate;
}

@EntityListenersAuditingEntityListener.class를 사용하기 위해서 Application 클래스에 @EnableJpaAuditing을 추가해줘야 합니다.

package me.jwkwon0817.springstudy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class SpringStudyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringStudyApplication.class, args);
    }
}

그다음 User, Role, UserRepository부터 작성을 시작해 보겠습니다.

* 참고로 프로젝트마다 저장하는 정보가 다를 수 있기 때문에 자신의 프로젝트에 맞게 값을 입력해 주어야 합니다.


User.java

package me.jwkwon0817.springstudy.web.user.entities;

import me.jwkwon0817.springstudy.web.entities.BaseTimeEntity;
import me.jwkwon0817.springstudy.web.user.enums.Role;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter // Getter 생성
@NoArgsConstructor // Default 생성자
@Entity // Entity임을 명시
@Table(name = "users") // 테이블명 설정
public class User extends BaseTimeEntity { // BaseTimeEntity 상속
    @Id // Primary Key
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
	
    @Column(nullable = false) // nullable하지 않도록 설정
    private String name;
	
    @Column(nullable = false)
    private String email;
	
    @Column
    private String picture;
	
    @Enumerated(EnumType.STRING) // Enum 값 저장
    @Column(nullable = false)
    private Role role;
	
    @Builder // Builder Pattern 사용
    public User(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }
	
    // update 함수 구현
    public User update(String name, String picture) {
        this.name = name;
        this.picture = picture;
		
        return this;
    }
	
    public String getRoleKey() {
        return this.role.getKey();
    }
}

Role.java

package me.jwkwon0817.springstudy.web.user.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor // private 필드로 생성자 구성
public enum Role {
    GUEST("ROLE_GUEST", "Guest"),
    USER("ROLE_USER", "Common User");
	
    private final String key;
    private final String title;
}

UserRepository.java

package me.jwkwon0817.springstudy.web.user.repositories;

import me.jwkwon0817.springstudy.web.user.entities.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

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

그리고 config.auth.dto 안에 있는 OAuthAttributes, Sessionuser에 다음과 같이 작성합니다.


OAuthAttributes.java

package me.jwkwon0817.springstudy.config.auth.dto;

import me.jwkwon0817.springstudy.web.user.entities.User;
import me.jwkwon0817.springstudy.web.user.enums.Role;
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

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

SessionUser.java

package me.jwkwon0817.springstudy.config.auth.dto;

import me.jwkwon0817.springstudy.web.user.entities.User;
import lombok.Getter;

import java.io.Serializable;

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

다음은 config.auth 안에 있는 CustomOAuth2UserServiceSecurityConfig 클래스를 작성해 보겠습니다.


CustomOAuth2UserService.java

package me.jwkwon0817.springstudy.config.auth;

import me.jwkwon0817.springstudy.config.auth.dto.OAuthAttributes;
import me.jwkwon0817.springstudy.config.auth.dto.SessionUser;
import me.jwkwon0817.springstudy.web.user.entities.User;
import me.jwkwon0817.springstudy.web.user.repositories.UserRepository;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
@RequiredArgsConstructor
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.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());
		
        return userRepository.save(user);
    }
}

SecurityConfig.java

package com.codeverse.springstudy.config.auth;

import com.codeverse.springstudy.web.user.enums.Role;
import lombok.RequiredArgsConstructor;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    @Autowired
    private final CustomOAuth2UserService customOAuth2UserService;
	
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .authorizeHttpRequests()
                .requestMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
                .requestMatchers("/api/v1/**").hasRole(Role.USER.name())
                .anyRequest().authenticated()
                .and()
                .logout()
                .logoutSuccessUrl("/")
                .and()
                .oauth2Login()
                .userInfoEndpoint()
                .userService(customOAuth2UserService);
		
        return http.build();
    }
}

위 코드를 토대로 웹에서 테스트를 진행해보면 다음과 같은 결과를 얻을 수 있습니다.




profile
SRIHS 119th SW

0개의 댓글