Spring Boot 3에서 Google 로그인 기능을 구현하려면 먼저 GCP에서 인증키를 받아와야 한다.
인증키를 받아오는 방법은 다음 글에 자세하게 설명되어있다.
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
안에 OAuthAttributes
와 SessionUser
클래스를 만들어주고, config.auth
안에 CustomOAuth2UserService
와 SecurityConfig
클래스를 만들어준다.
그리고 다음과 같이 프로젝트 패키지 아래 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;
}
@EntityListeners
의 AuditingEntityListener.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부터 작성을 시작해 보겠다.
* 당연한 소리지만 프로젝트마다 저장하는 정보가 다를 수 있기 때문에 자신의 프로젝트에 맞게 값을 입력해 주어야 한다.
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();
}
}
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;
}
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
에 다음과 같이 작성한다.
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();
}
}
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
안에 있는 CustomOAuth2UserService
와 SecurityConfig
클래스를 작성해 보겠다.
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);
}
}
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();
}
}
위 코드를 토대로 웹에서 테스트를 진행해보면 다음과 같은 결과를 얻을 수 있다.