TOP
본문 바로가기
📚 목차
[Java]/Spring Boot

[Java] Spring Boot: 구글 로그인 기능 추가하기

by 기록자_Recordian 2025. 4. 18.
728x90
반응형
이전 내용
 

[Java] Spring Boot: 스프링 시큐리티, OAuth2, JWT

이전 내용 [Java] Spring Boot: 네이버 로그인 구현하기이전 내용 [Java] Spring Boot: 카카오 로그인 기능 추가하기이전 내용 [Java] Spring Boot: 구글 로그인 기능 추가하기이전 내용 전반적인 순서1. Gradle 또

puppy-foot-it.tistory.com


전반적인 순서
1. Gradle 또는 Maven 의존성 추가
2. 구글 API Console 설정 - 구글 개발자 콘솔 프로젝트 생성 → OAuth 2.0 클라이언트 ID 생성
3. application.yaml 설정 (보안 문제로 해당 파일 .gitignore에 등록)
4. 보안 설정 클래스 (SecurityConfig) 만들기
5. 뷰 컨트롤러 및 HTML 페이지 생성 (로그인 페이지)
6. 사용자 인증 정보를 가져오는 컨트롤러 추가
7. 애플리케이션 실행 및 테스트

구글 로그인 기능 추가하기
: 구글 클라우드 콘솔

 

구글 로그인 기능을 추가하려면 인증 서버에게 토큰을 제공받아야 하는데, 먼저 구글 클라우드 콘솔에 접속한다. (접속 후 로그인)

 

https://cloud.google.com/cloud-console

 

cloud.google.com

 

오른쪽 상단에 콘솔 버튼 클릭

 

프로젝트 생성을 위해 좌측 상단에 [My Hands On Project] 클릭

 

 

새 프로젝트 클릭

 

프로젝트 명 입력 후 만들기 클릭

사이드 바에서 [API 및 서비스] - 사용자 인증 정보 클릭

 

동의 화면 구성 클릭

시작하기 클릭

 


OAuth 클라이언트 만들기

 

구글 소셜 로그인을 활용하기 위해서는 Google Cloud에서 OAuth2.0 클라이언트 ID를 발급받아야 한다.

 

또는 대시보드에서 [사용자 인증 정보 - 사용자 인증 정보 만들기 - OAuth 클라이언트 ID] 순서대로 클릭

 

  • 애플리케이션 유형: 웹 애플리케이션
  • 이름: springboot-developer(각자 사용환경에 맞게 설정)
  • 승인된 리디렉션 URI: http://localhost:포트번호/login/auth2/code/google (https 도 추가, 총 2개)

만들기 클릭

 

그리고 이 클라이언트 ID와 보안 비밀번호를 잘 보관해 둔다.


API 사용 범위 설정하기

 

클라이언트 생성 후 데이터 액세스를 클릭하고, [범위 추가 또는 삭제] 클릭

 

범위는 email, openid 선택 (사용환경에 따라 알맞게 선택하면 된다) 후 [업데이트] 클릭

 

그리고 하단의 [SAVE] 버튼 클릭

 


인텔리제이 세팅

 

1. 의존성 추가

이제 인텔리제이로 넘어와서

build.gradle에서 dependencies에 oauth2-client를 추가한 후 빌드

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

 

2. yml 파일 생성 및 .gitignore 수정

그리고 application.yml 을 생성(src/main/resources)하고 하단의 정보를 추가해 준다. 단, id와 비밀번호는 노출되면 안 되므로 꼭 .gitignore에 등록해줘야 한다.

# application-oauth.yml에 아래 설정 추가

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: [위에서 확인한 클라이언트 ID]
            client-secret: [위에서 확인한 클라이언트 비밀번호]
            scope: # 범위
              - email
              - openid

 

.gitignore 파일 내에 해당 내용 추가

### Ignore application-oauth.yml file ###
application-oauth.yml

 

▶ 필자의 경우 해당 내용을 properties에 추가하여 진행하였다.

 

[application.properties]

spring.application.name=spro01
server.port = 9091

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.defer-datasource-initialization=true
spring.sql.init.mode=never
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.h2.console.enabled=true
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:templates/
spring.thymeleaf.suffix=.html
spring.profiles.include=oauth

spring.security.oauth2.client.registration.google.client-id=[본인의 구글 클라이언트 ID]
spring.security.oauth2.client.registration.google.client-secret=[본인의 구글 클라이언트 SecretKey]
spring.security.oauth2.client.registration.google.scope=openid,email
spring.security.oauth2.client.registration.google.redirect-uri=https://localhost:9091/login/oauth2/code/google
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v2/userinfo
spring.security.oauth2.client.provider.google.user-name-attribute=id

로그인 화면에서 사용할 이미지 다운받기

 

구글 로그인 브랜드 페이지에서 로그인 화면에서 사용할 구글 이미지를 다운 받는다. (하단 링크 접속)

 

Sign in with Google Branding Guidelines  |  Google Identity  |  Google for Developers

Send feedback Sign in with Google Branding Guidelines Stay organized with collections Save and categorize content based on your preferences. This document provides guidelines on how to display a Sign in with Google button on your website or app. Your websi

developers.google.com

 

다운 받은 파일을 압축 해제하고

웹 - png1x - dark - web_dark_rd_SI 파일을 복사하여 

main - resources - static 에 img 경로를 생성한 뒤, 해당 파일을 넣어준다. 그리고 리팩토링-이름변경으로 파일명을 google.png로 변경한다.

 

그리고 이 이미지를 활용하여 로그인 화면에 OAuth 연결 버튼을 생성해 본다.

templates 디렉토리에 oauthLogin.html 파일을 생성하고 코드 입력

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css">

    <style>
        .gradient-custom {
            background: #6a11cb;
            background: -webkit-linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1));
            background: linear-gradient(to right, rgba(106, 17, 203, 1), rgba(37, 117, 252, 1))
        }
    </style>
</head>
<body class="gradient-custom">
<section class="d-flex vh-100">
    <div class="container-fluid row justify-content-center align-content-center">
        <div class="card bg-dark" style="border-radius: 1rem;">
            <div class="card-body p-5 text-center">
                <h2 class="text-white">LOGIN</h2>
                <p class="text-white-50 mt-2 mb-5">서비스 사용을 위해 로그인을 해주세요!</p>

                <div class = "mb-2">
                    <a href="/oauth2/authorization/google">
                        <img src="/img/google.png">
                    </a>
                </div>
            </div>
        </div>
    </div>
</section>
</body>
</html>

 

그리고 HTML과 연결한 자바스크립트 파일을 만들기 위해

resources - static - js 경로에 token.js 파일을 생성하고 파라미터로 받은 토큰이 있다면 토큰을 로컬 스토리지에 저장하기 위한 코드를 작성한다.

const token = searchParam('token');

if (token) {
    localStorage.setItem('access_token', token)
}

function searchParam(key) {
    return new URLSearchParams(location.search).get(key);
}

 

그리고 token.js를 가져올 수 있도록 html에 아래 한 줄을 추가해 준다. (필자의 경우 ArticleList.html)

<script src="/js/token.js"></script>

토큰 기반 요청 사용하기

 

resources/js 패키지 있는 articles.js 파일을 생성하여 토큰 기반 요청을 사용하도록 코드를 작성한다.

// 삭제 기능
const deleteButton = document.getElementById('delete-btn');

if (deleteButton) {
    deleteButton.addEventListener('click', event => {
        let id = document.getElementById('article-id').value;
        function success() {
            alert('삭제가 완료되었습니다.');
            location.replace('/articles');
        }

        function fail() {
            alert('삭제 실패했습니다.');
            location.replace('/articles');
        }

        httpRequest('DELETE',`/api/articles/${id}`, null, success, fail);
    });
}

// 수정 기능
const modifyButton = document.getElementById('modify-btn');

if (modifyButton) {
    modifyButton.addEventListener('click', event => {
        let params = new URLSearchParams(location.search);
        let id = params.get('id');

        body = JSON.stringify({
            title: document.getElementById('title').value,
            content: document.getElementById('content').value
        })

        function success() {
            alert('수정 완료되었습니다.');
            location.replace(`/articles/${id}`);
        }

        function fail() {
            alert('수정 실패했습니다.');
            location.replace(`/articles/${id}`);
        }

        httpRequest('PUT',`/api/articles/${id}`, body, success, fail);
    });
}

// 생성 기능
const createButton = document.getElementById('create-btn');

if (createButton) {
    // 등록 버튼을 클릭하면 /api/articles로 요청을 보낸다
    createButton.addEventListener('click', event => {
        body = JSON.stringify({
            title: document.getElementById('title').value,
            content: document.getElementById('content').value
        });
        function success() {
            alert('등록 완료되었습니다.');
            location.replace('/articles');
        };
        function fail() {
            alert('등록 실패했습니다.');
            location.replace('/articles');
        };

        httpRequest('POST','/api/articles', body, success, fail)
    });
}


// 쿠키를 가져오는 함수
function getCookie(key) {
    var result = null;
    var cookie = document.cookie.split(';');
    cookie.some(function (item) {
        item = item.replace(' ', '');

        var dic = item.split('=');

        if (key === dic[0]) {
            result = dic[1];
            return true;
        }
    });

    return result;
}

// HTTP 요청을 보내는 함수
function httpRequest(method, url, body, success, fail) {
    fetch(url, {
        method: method,
        headers: { // 로컬 스토리지에서 액세스 토큰 값을 가져와 헤더에 추가
            Authorization: 'Bearer ' + localStorage.getItem('access_token'),
            'Content-Type': 'application/json',
        },
        body: body,
    }).then(response => {
        if (response.status === 200 || response.status === 201) {
            return success();
        }
        const refresh_token = getCookie('refresh_token');
        if (response.status === 401 && refresh_token) {
            fetch('/api/token', {
                method: 'POST',
                headers: {
                    Authorization: 'Bearer ' + localStorage.getItem('access_token'),
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    refreshToken: getCookie('refresh_token'),
                }),
            })
                .then(res => {
                    if (res.ok) {
                        return res.json();
                    }
                })
                .then(result => { // 재발급이 성공하면 로컬 스토리지값을 새로운 액세스 토큰으로 교체
                    localStorage.setItem('access_token', result.accessToken);
                    httpRequest(method, url, body, success, fail);
                })
                .catch(error => fail());
        } else {
            return fail();
        }
    });
}

 


글 수정, 삭제, 글쓴이 확인 로직 추가하기

 

service 폴더 내에 있는 BlogService.java 파일을 만들고 코드 작성

import com.example.spro01.domain.Article;
import com.example.spro01.dto.AddArticleRequest;
import com.example.spro01.dto.UpdateArticleRequest;
import com.example.spro01.repository.BlogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Service
public class BlogService {
    private final BlogRepository blogRepository;

    // 블로그 글 추가 메소드
    public Article save(AddArticleRequest request, String userName) {
        return blogRepository.save(request.toEntity(userName));
    }

    public List<Article> findAll() {
        return blogRepository.findAll();
    }

    public Article findById(long id) {
        return blogRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found: " + id));
    }

    // 블로그 글 삭제 메소드
    public void delete(long id) {
        Article article = blogRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found: " + id));

        authorizedArticleAuthor(article);
        blogRepository.delete(article);
    }


    @Transactional // 트랜잭션 메소드
    public Article update(long id, UpdateArticleRequest request) {
        Article article = blogRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("not found: " + id));

        authorizedArticleAuthor(article);
        article.update(request.getTitle(), request.getContent());

        return article;
    }

    // 게시글을 작성한 유저인지 확인
    private static void authorizedArticleAuthor(Article article) {
        String userName = SecurityContextHolder.getContext().getAuthentication().getName();
        if (!article.getAuthor().equals(userName)) {
            throw new IllegalArgumentException("not authorized: ");
        }
    }
}

보안 설정 클래스 (WebOAuthSecurityConfig) 만들기

 

config 경로 내에 WebOAuthSecurityConfig.java를 만들고,구글 인증을 처리하기 위한 보안 설정 클래스를 작성한다.

import com.example.spro01.config.jwt.TokenProvider;
import com.example.spro01.config.oauth.OAuth2AuthorizationRequestBasedOnCookieRepository;
import com.example.spro01.config.oauth.OAuth2SuccessHandler;
import com.example.spro01.config.oauth.OAuth2UserCustomService;
import com.example.spro01.repository.RefreshTokenRepository;
import com.example.spro01.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;

@RequiredArgsConstructor
@Configuration
public class WebOAuthSecurityConfig {
    private final OAuth2UserCustomService oAuth2UserCustomService;
    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;
    private final UserService userService;

    @Bean
    public WebSecurityCustomizer configure() { // 스프링 시큐리티 기능 비활성화
        return (web) -> web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers("/img/**", "/css/**", "/js/**");

    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(
                                "/login",
                                "/signup",
                                "/user",
                                "/oauth2/**"  // OAuth2 로그인 관련 요청 허용
                        ).permitAll()  // 허용된 요청 URL 설정
                        .anyRequest().authenticated()  // 나머지 요청은 인증 필요
                )
                .oauth2Login(oauth2Login -> oauth2Login
                        .loginPage("/login")  // 사용자 정의 로그인 페이지
                        .defaultSuccessUrl("/api/articles")  // 로그인 성공 시 리다이렉트 URL
                        .failureUrl("/login?error=true")  // 로그인 실패 시 리다이렉트 URL
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/login")  // 로그아웃 성공 시 리다이렉트 URL
                        .invalidateHttpSession(true)  // 로그아웃 시 세션 무효화
                );

        return http.build();  // SecurityFilterChain 빌드하여 반환
    }

    @Bean
    public OAuth2SuccessHandler oAuth2SuccessHandler() {
        return new OAuth2SuccessHandler(tokenProvider,
                refreshTokenRepository,
                oAuth2AuthorizationRequestBasedOnCookieRepository(),
                userService);
    }

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter(tokenProvider);
    }

    @Bean
    public OAuth2AuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository() {
        return new OAuth2AuthorizationRequestBasedOnCookieRepository();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

뷰 컨트롤러 구현

 

구글 소셜 로그인 기능을 구현한 후에는 사용자에게 로그인 화면과 관련된 다양한 작업을 처리할 수 있는 뷰 컨트롤러를 생성하는 것이 좋다. 뷰 컨트롤러는 사용자가 로그인 요청을 할 수 있는 페이지를 제공하고, 필요한 리다이렉션 및 로그인 성공/실패 메시지를 처리하는 역할을 한다.

 

controller 폴더 내에 UserApiController.java / UserViewController.java 파일 생성

 

[UserApiController.java]

import com.example.spro01.dto.AddUserRequest;
import com.example.spro01.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@RequiredArgsConstructor
@Controller
public class UserApiContorller {
    private final UserService userService;

    @PostMapping("/user")
    public String signup(AddUserRequest request) {
        userService.save(request); // 회원 가입 메소드 호출
        return "redirect:/login"; // 회원 가입 완료된 이후에 로그인 페이지로 이동
    }

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
        return "redirect:/login";
    }
}

 

[UserViewController.java]

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

@Controller
public class UserViewController {
    @GetMapping("/login")
    public String login() {
        return "oauthLogin";
    }

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

 

이제 Application 파일을 실행해 서버를 켜고,

localhost:포트번호/login으로 접속하면

 

구글 로그인 기능이 가능하도록 잘 설정되었다.


다음 내용

 

[Java] Spring Boot: 카카오 로그인 기능 추가하기

이전 내용 [Java] Spring Boot: 구글 로그인 기능 추가하기이전 내용 전반적인 순서1. Gradle 또는 Maven 의존성 추가2. 구글 API Console 설정 - 구글 개발자 콘솔 프로젝트 생성 → OAuth 2.0 클라이언트 ID 생성

puppy-foot-it.tistory.com

 

728x90
반응형