본격적인 구글 소셜 로그인 단계에 들어가기에 앞서 구글 소셜 로그인 구현의 흐름에 대해 간단하게 알아보도록 하자.
❗소셜 로그인을 쉽게 생각하면 아래와 같다.
- 사용자가 구글로 로그인 버튼 클릭
- 구글 로그인 페이지로 이동
- 구글에서 로그인
- 로그인 후 다시 우리 페이지로 돌아와서
- JWT 토큰을 만들어서 사용자에게 전달
📌 즉, 위 과정이 수행되기 위해서 우리 서버 쪽에서는 구글에게 내 웹사이트를 알려주고 구글 로그인 요청이 들어오면 잘 처리할 수 있어야 하고 로그인 성공 시 그 사용자 정보를 우리 프로그램의 사용자로 등록, 처리할 수 있어야 함. 고로!! 아래 파일들을 추가해서 구현해보자.
| 구성 요소 | 역할 | 필요한 이유 |
|---|---|---|
| application.yml | 구글 OAuth 설정 | 구글 API에 접근하기 위한 설정 |
| SecurityConfig.java | Spring Security 설정 | 어떤 URL은 로그인 없이 접근 가능하게 할지, 어떤 URL은 보호할지, 소셜 로그인 성공 시 어떤 서비스로? |
| CustomOAuth2UserService.java | 구글에서 받은 사용자 정보 처리 | 로그인한 유저의 구글 프로필(이메일 등)을 추출하고, 우리 서버에 필요한 사용자 객체로 변환 |
| OAuth2SuccessHandler.java | 로그인 성공 후 처리 | 로그인 완료 후 JWT 발급 등 후처리 |
| JwtTokenGenerator.java | JWT 토큰 생성 | 사용자의 이메일 등의 정보를 담아 토큰으로 발급하고, 이걸 클라이언트에게 넘김 |
구글 클라우드에 접속하여 OAuth 2.0 클라이언트 ID를 발급받는다.
이때 만들어둔 클라우드 ID와 클라우드 보안 비밀번호를 잘 기록해두자!
(자세한 내용은 저번 스터디 때 다같이 했기 때문에 생략.)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
스프링 부트는 기본적으로
로그인과보안 기능을 제공한다.
하지만구글 소셜 로그인은OAuth2 프로토콜을 사용하므로, build.gradle에 다음 라이브러리를 추가해줘야 한다.- spring-boot-starter-oauth2-client → 구글, 네이버, 카카오 등 다양한 소셜 로그인 서비스를 연동할 수 있게 도와주는 핵심 라이브러리이다. . . . - jwt 관련 라이브러리 → 로그인 성공 후 `JWT(Json Web Token)`를 발급하기 위해 필요합니다. 로그인한 사용자의 이메일 등 정보를 담은 `토큰`을 만들어 프론트엔드에 전달할 수 있도록 해준다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: [본인의 클라이언트 ID]
client-secret: [본인의 클라이언트 시크릿]
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- profile
- email
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://oauth2.googleapis.com/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: sub
application.yml은 스프링 부트의
외부 설정 정보를 관리하는 핵심 파일이다.- 이 파일에 구글 개발자 콘솔에서 발급받은 client-id와 client-secret을 입력해야 우리 프로젝트가 구글 서버와 통신할 수 있다. . . . - 또한 redirect-uri와 scope 등을 설정함으로써 사용자의 정보에 접근하고, 로그인 결과를 받을 수 있게 된다.즉, 이 파일은 구글 로그인과의 연결을 위한 필수 설정 포인트이다.
package com.example.config;
import com.example.oauth.CustomOAuth2UserService;
import com.example.oauth.OAuth2SuccessHandler;
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.web.SecurityFilterChain;
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(user -> user
.userService(customOAuth2UserService)
)
.successHandler(oAuth2SuccessHandler)
);
return http.build();
}
}
이 클래스는 스프링 시큐리티에서
어떤 URL에 인증을 요구하고,어떤 URL은 자유롭게 접근 가능한지를 정하는 보안 설정 클래스이다.예를 들어: /login, / 등은 인증 없이 접근 가능하도록 열어두고, 그 외의 모든 요청은 로그인(인증)을 요구하게 설정할 수 있다.또한 이 클래스에서는 다음과 같은 중요한 역할을 한다.
로그인 성공 후 사용자 정보를 `CustomOAuth2UserService`에게 전달하고, 후속 처리는 `OAuth2SuccessHandler`에게 넘기도록 지정한다.
package com.example.oauth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Map;
@Slf4j
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(request);
Map<String, Object> attributes = oAuth2User.getAttributes();
String email = (String) attributes.get("email");
log.info("Google 로그인 사용자 이메일: {}", email);
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
attributes,
"sub" // 구글의 고유 사용자 식별자
);
}
}
구글 로그인이 성공하면, 스프링은
OAuth2User 객체를 통해 사용자 정보를 넘겨준다.
이 클래스는 그 정보를 받아,우리가 원하는 방식으로 가공하고 처리하는 역할을 한다.
package com.example.oauth;
import com.example.jwt.JwtTokenGenerator;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenGenerator jwtTokenGenerator;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String email = (String) oAuth2User.getAttributes().get("email");
String token = jwtTokenGenerator.generateToken(email);
// JWT 전달 + 프론트 리다이렉트
response.sendRedirect("http://localhost:3000/main?token=" + token);
}
}
이 클래스는 소셜 로그인 성공 이후 최종적인 후처리를 담당한다.
여기서 주로 하는 일은 다음과 같다.- 로그인에 성공한 사용자의 정보를 바탕으로 JWT 토큰을 생성 . . . - 특정 페이지로 리다이렉트이 파일이 없다면, 로그인 후 단순히 메인 페이지로 이동하는
기본 동작만 가능하다.
package com.example.jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JwtTokenGenerator {
private final String secretKey = "your-secret-key";
public String generateToken(String email) {
Date now = new Date();
Date expiry = new Date(now.getTime() + 3600000);
return Jwts.builder()
.setSubject(email)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes(StandardCharsets.UTF_8))
.compact();
}
}
이 클래스는
JWT 토큰을 실제로 생성해주는 기능을 담당한다.- 사용자의 이메일 등 정보를 JWT의 subject로 포함 . . . - 유효 시간 및 시그니처를 지정하여 보안성을 확보이 파일이 없다면, 사용자는 로그인에 성공해도 그 상태를 계속 유지할 수 없어
매번 API 호출마다다시 로그인해야 한다.
사실 내 최초 목표는 로그인 후 게시판으로 넘어가지지 않는 이슈 해결 후 구글 소셜 로그인을 구현하려 했는데 지피티와 100분 토론하다보니 프로젝트가 완전 맛이 가버려서 철면피 수법으로 구글 소셜 로그인만이라도 구현하기로 했다.
근데?
위와 같은 과정으로 수행을 하면 깔끔하게 성공!할 줄 알았는데,,,그랬는데..
끊임 없는 404 오류로 사투를 벌이다가 과제 폭탄 이슈로 시간 내에 해결하는 걸 실패하고 말았다.
다음주까지는 오류 해결하고 성공해오는 걸 목표로 해야겠다..
또 쉽지 않은 싸움이 되겠지..?(종강 내놔)
라고 했지만..철면피로 구글 소셜 로그인을 구현하기에는 내 게시판 프로젝트 자체가 너무 엉망이 되어버린 것 같은...(님아 그 강을 건너지 마오)
느낌이 들어서 차근차근 다시 해보기로 한다.
그리고 전공과제만 5개가 밀려있던 저번주와 달리 이번주는 과제를 그래도 비교적 해결을 많이 해둔 상태라 시간을 좀 써보기로 한다.(아직 과제 많긴 함...^^)
사실 저번 주 제대로 한 게 없는 것 같아 다른 분들 벨로그를 더 열심히 읽어봤다. 그러던 중 지인님 벨로그를 보니 나와 상황이 너무 비슷한 것이다!! 유레카를 외치며 지인님처럼 로그인 구현부터 바꿔주기로 한다.
다시 상황을 되짚어 보니 게시판 프로젝트가 엉망이 되기 시작한 건 postman 테스트를 하면서 였던 것 같다. 로그인을 requestparam 방식으로 구현했었는데 postman 테스트를 하던 도중 다른 방식으로 수정을 했는데 완벽하게 구현이 안되고 어설프게 조각조각 맞지 않는 퍼즐을 맞추고 있었던 것 같다. 그래서 프로젝트 자체가 엉망이 된 것 같은데.. 돌고 돌아 이젠 뭔가 문제가 심각하다는 걸 알게 됐으니 수정해보자!
기존의 requestparam 방식의 로그인을 JWT 방식으로 바꿔주고, 회원가입 기능도 추가로 만들어주는 게 목표이다.(회원가입 기능이 없었음..)
추가로 현재는 board/list로 넘어가는 것이 오류가 뜨며 안되는데, 이것도 해결해보자..
+++ 이제까지는 회원가입 기능이 없었기 때문에 게시글 작성을 해도 게시물 작성자가 보이지 않았는데 이것도 추가하자!
여기까지 해결해야 비로소 구글 소셜 로그인 구현이 가능..하다는 사실^^
사실 이번 과제는 구글 소셜 로그인이 메인이기 때문에 자세하게 내용을 다루지는 않고 결과만 보여줄 예정이다! 많고 많은 에러와 프로젝트 전체를 갈아엎다싶이 지피티와 머리를 맞대고 한 결과는 다음과 같다.

회원가입 기능

로그인 기능

로그인하면 이동되는 board/list 창
(여기까지 하고 너무 감격스러웠다..솔직 고백으로 postman 과제할 때즈음부터 이 게시글 리스트를 본 기억이...ㅎ 500 오류만 수없이 봐와서 너무 스트레스 받았는데 드디어! 너무 반가웠다.)
테스트용으로 hi라는 계정과 202301506이라는 계정으로 회원가입 및 로그인 후 게시글 작성도 해보았다. 게시글 작성자와 작성한 시간도 정상적으로 보이는 것을 확인할 수 있다.
끝판왕 두둥등장
저번주 벨로그에 정리해놨던 것과 구글 클라이언트를 만들어둔 것만 해도 심리적 안정감이 왔다. 지피티의 도움도 받고 내 벨로그도 참고하여 구현을 하니 비교적 수월하게 성공했다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
const params = new URLSearchParams(window.location.search);
const token = params.get("token");
if (token) {
document.cookie = "token=" + token + "; path=/";
window.location.href = "/board/list";
}
</script>
</head>
<body>
<p>로그인 처리 중입니다...</p>
</body>
</html>
추가로 request.html 파일을 만들었다. 위 파일은 구글 로그인을 성공하면, JWT 토큰을 받아서 브라우저에 저장해주는 역할을 한다.
구글 로그인을 하면 서버가 토큰을 만들어주는데 이 토큰은 브라우저에 저장이 되지 않기 때문에 위 파일이 중간 역할을 해주는 것이다. 받은 토큰을 브라우저 쿠키에 넣고, board/list로 보내준다.

로그인 창에서 아래에 구글로 로그인 버튼을 추가하고, 누르면 아래와 같이 구글로 로그인 창이 뜨게 된다.

원하는 계정을 선택하면 아래와 같이 게시글 리스트 창으로 넘어가서 게시글 작성이 가능하다!

사실 구글 소셜 로그인 이전에 해결해야 할 것들이 너무 많다보니 엄두도 안 나고 시작 전부터 스트레스도 많이 받고 했는데 또 차근차근 해나가다 보니 많이 뿌듯했다. 무엇보다 내 게시판 리스트를 볼 수 있다는 게 감격 그 자체였다. 코드가 뒤죽박죽이 되어버려서 인텔리제이는 쳐다보기도 싫었는데 해결돼서 너무 좋다!! 구글 소셜 로그인도 다음 번에는 좀 더 내 힘으로 스스로 구현해보고 싶다. 구글 말고 다른 소셜 로그인도 해보고 싶고 배포까지 해서 자랑도 하고 싶다.
고생하셨습니다