본문 바로가기
Framework/Springboot

스프링부트로 Google OAuth 2.0 개발하기

by IFLA 2024. 1. 30.

OAuth 란

개발 환경

  • JDK 17
  • Spring boot 버전 : 3.0.6
  • PostgreSQL : 14.0

사용 플러그인

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

구글 OAuth 설정하기

구글 앱 등록하기

  • 구글 Cloud 사이트로 이동 후 왼쪽 상단에 보면 Google Cloud 제목 오른쪽에 네모 버튼을 클릭하면 위와 같은 팝업창이 화면에 나타납니다.
  • 팝업창이 나타났을 때 팝업창에서 오른쪽 위에 [ 새 프로젝트 ] 버튼을 클릭합니다.

 

  • 새 프로젝트를 생성하는 화면으로 이동합니다.
  • 프로젝트 이름은 사용자가 원하는 명칭으로 설정하면 됩니다. 명칭을 입력 후 아래의 [ 만들기 ] 버튼을 클릭하면 신규 프로젝트가 생성됩니다.
  • 생성되는 데 몇 초 정도의 시간이 소요됩니다.

  • 생성 후 위 화면에서 빨간 네모칸을 눌러줍니다. 그러면 생성한 프로젝트 목록이 표시되면서 생성한 프로젝트를 선택해줍니다.
  • 선택 후 생성된 프로젝트로 이동합니다.

 

사용자 인증 정보 - OAuth 동의 하면 추가하기

  • 왼쪽 상단의 Google Cloud 글자 왼쪽에 있는 버튼을 클릭 후 API 및 서비스 클릭 후 사용자 인증 정보를 클릭합니다.

  • 사용자 인증 정보 화면으로 이동합니다.
  • 오른쪽의 빨간 네모칸에 있는 [ 동의 하면 구성 ] 버튼을 클릭합니다.

 

  • OAuth 동의 화면 이동합니다.
  • User Type 을 선택을 하는데, [ 외부 ] 를 선택하고 만들기 버튼을 클릭합니다.

 

  • 앱 이름, 사용자 지원 이메일, 개발자 연락처 정보를 입력 후 제일 하단의 [ 저장 후 계속 ] 버튼을 클릭합니다.

 

  • 범위를 지정하는 화면으로 이동합니다.
  • 왼쪽의 [ 범위 추가 또는 삭제 ] 버튼을 클릭하면 오른쪽에 범위를 선택할 수 있는 화면이 나타납니다.
  • 오른쪽에 나타난 화면에서 제일 상단의 위에 3개만 선택 후 [ 저장 후 계속 ] 버튼을 클릭합니다.

 

  • 테스트 사용자를 추가하는 화면으로 이동합니다.
  • 추가할 테스트 사용자가 없기 때문에 별도의 추가는 하지 않고, [ 저장 후 계속 ] 버튼을 클릭합니다.

 

사용자 인증 정보 만들기

  • 사용자 인증 정보 화면에서 오른쪽 상단에 있는 [ + 사용자 인증 정보 만들기 ] 버튼을 클릭하면 아래에 메뉴가 표시됩니다.
  • 아래에 표시된 메뉴 중에서 [ OAuth 클라이언트 ID ] 클릭합니다.

 

  • 애플리케이션 유형은 [ 웹 애플리케이션 ] 을 선택해줍니다.
  • 이름과 승인된 리디렉션 URI 를 입력해줍니다. 저희는 로컬로 서버를 운영하기 때문에 주소에는 localhost 를 넣어줍니다.
  • 입력 후 하단의 [ 만들기 ] 버튼을 클릭합니다.

  • 생성 후 사용자 인증 정보 화면으로 이동되면서 만든 OAuth 정보가 팝업창에 표시됩니다.
  • 표시된 화면에서 클라이언트 ID, 클라이언트 보안 비밀번호는 스프링부트에서 추가할 설정 파일에 있어야 하는 정보기 때문에 따로 복사를 해줍니다.

 


개발

프로젝트 구조

 

application-oauth.properties

spring.security.oauth2.client.registration.google.client-id={clientID}
spring.security.oauth2.client.registration.google.client-secret={client secret key}
spring.security.oauth2.client.registration.google.scope=profile,email
  • Google Cloud 에서 추가한 OAuth 의 클라이언트 ID와 클라이언트 시크릿 키 값을 추가합니다.
  • scope 의 기본값은 openid, profile, email이 포함되어 있습니다. 저희는 OAuth2 Service 직접 구현하기 때문에 open id 를 제외하고 profile, email 만 추가해줍니다.

 

application.properties

spring.profiles.include=oauth

spring.datasource.hikari.maximum-pool-size=4

# Database 설정
spring.datasource.url=jdbc:postgresql://localhost:5432/study
spring.datasource.username=testuser
spring.datasource.password=testpass

# JPA 설정
spring.jpa.hibernate.dialect = org.hibernate.dialect.PostgreSQL10Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql = true
spring.jpa.show-sql = true
  • spring.profiles.include 는 oauth 설정 파일을 읽을 수 있게 추가하는 부분입니다.

 

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

 

OAuthAttributes.java

package com.ina.oauth_test.common.auth.dto;

import com.ina.oauth_test.api.domain.Users;
import com.ina.oauth_test.common.status.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 Users toEntity() {
        return Users.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
}

 

SessionUser.java

package com.ina.oauth_test.common.auth.dto;

import com.ina.oauth_test.api.domain.Users;
import lombok.Getter;

import java.io.Serializable;

@Getter
public class SessionUser implements Serializable {
    private String name;
    private String email;
    private String picture;

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

 

CustomOAuth2UserService.java

package com.ina.oauth_test.common.auth;

import com.ina.oauth_test.api.domain.Users;
import com.ina.oauth_test.api.repository.UserRepository;
import com.ina.oauth_test.common.auth.dto.OAuthAttributes;
import com.ina.oauth_test.common.auth.dto.SessionUser;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

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 java.util.Collections;

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

        Users user = saveOrUpdate(attributes);

        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey());
    }

    private Users saveOrUpdate(OAuthAttributes attributes) {
        Users user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());

        return userRepository.save(user);
    }
}

 

Users.java

package com.ina.oauth_test.api.domain;

import com.ina.oauth_test.common.status.Role;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Entity
public class Users {
    @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 Users(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

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

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}
  • 사용자 정보 Entity 입니다.
  • PostgreSQL 사용 시 User 로 Entity 를 생성할 수 없기 때문에 Users 이름으로 Entity 객체를 생성해줘야 합니다.

 

UserRepository.java

package com.ina.oauth_test.api.repository;

import com.ina.oauth_test.api.domain.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

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

 

OAuthController.java

package com.ina.oauth_test.api.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class OAuthController {
    @GetMapping("/loginForm")
    public String home() {
        return "loginForm";
    }

    @GetMapping("/private")
    public String privatePage() {
        return "privatePage";
    }
}

 

SecurityConfig.java

package com.ina.oauth_test.common.config;

import com.ina.oauth_test.common.auth.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
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;

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .requestMatchers("/private/**").authenticated() //private로 시작하는 uri는 로그인 필수
                .anyRequest().permitAll() //나머지 uri는 모든 접근 허용
                .and().oauth2Login()
                .loginPage("/loginForm") //로그인이 필요한데 로그인을 하지 않았다면 이동할 uri 설정
                .defaultSuccessUrl("/") //OAuth 구글 로그인이 성공하면 이동할 uri 설정
                .userInfoEndpoint()//로그인 완료 후 회원 정보 받기
                .userService(customOAuth2UserService).and().and().build(); //로그인 후 받아온 유저 정보
    }
}
  • oauth2Login() 설정을 추가를 해줌으로써 oauth 로 로그인할 수 있게 합니다.
  • 로그인 성공 후 토큰을 발행이 필요한 경우 .successHandler 를 추가해주면 된다.

 

loginForm.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <a href="/oauth2/authorization/google" class="btn btn-sm btn-success active" role="button">Google Login</a><br>
</body>
</html>
  • index.html, logout.html, privatePage.html 은 기본 생성 html 로 생성해주시면 됩니다.

 


참고 사이트

Spring Security와 Oauth 2.0으로 로그인 구현하기(SpringBoot + React)

OAuth 2.0 + JWT + Spring Security로 회원 기능 개발하기 - 앱등록과 OAuth 2.0 기능구현

댓글


\