반응형
Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
Tags
- javascript
- Array
- 테스트자동화
- 정규식
- 스프링부트
- 배열
- ArrayList
- SpringBoot
- html
- Java
- 문자열
- list
- junit5
- math
- string
- Eclipse
- 자바문법
- vscode
- js
- java테스트
- CSS
- 자바스크립트
- IntelliJ
- 인텔리제이
- junit
- Visual Studio Code
- input
- HashMap
- 자바
- 단위테스트
Archives
- Today
- Total
어제 오늘 내일
[Spring Security] 16편 - "3초 만에 가입" OAuth2 소셜 로그인 연동 (Google) 본문
IT/SpringBoot
[Spring Security] 16편 - "3초 만에 가입" OAuth2 소셜 로그인 연동 (Google)
hi.anna 2026. 2. 25. 07:50요즘 서비스에서 "회원가입" 버튼을 눌러서 ID, 비밀번호, 이메일, 이름을 일일이 입력하는 유저는 거의 없습니다. "구글로 시작하기", "카카오로 시작하기" 버튼 하나면 끝이죠.
기존의 JWT 구조를 유지하면서 소셜 로그인을 붙이는 핵심 전략은 다음과 같습니다.
- 사용자가 소셜 로그인(구글/카카오) 인증을 마침.
- 스프링 시큐리티가 소셜 정보를 받아옴 (이메일, 이름 등).
- SuccessHandler에서 이 정보를 이용해 강제로 우리 서버의 JWT(Access + Refresh)를 발급.
- 프론트엔드로 리다이렉트시키며 토큰을 전달.
Step 1. 사전 준비 (Google Cloud Console)
코드를 짜기 전에 구글에서 "내 앱이 로그인 좀 쓸게"라고 허락을 받아야 합니다.
- Google Cloud Console 접속 & 새 프로젝트 생성.
- API 및 서비스 > 사용자 인증 정보 > 사용자 인증 정보 만들기 > OAuth 클라이언트 ID.
- 애플리케이션 유형: 웹 애플리케이션.
- 승인된 리디렉션 URI:
http://localhost:8080/login/oauth2/code/google
- (스프링 시큐리티가 기본적으로 사용하는 고정 경로입니다.)
- 생성된 Client ID와 Client Secret을 메모해 둡니다.
Step 2. 의존성 추가
OAuth2 Client 기능을 사용하기 위한 라이브러리입니다.
- 파일:
build.gradle
dependencies {
// ... 기존 의존성 ...
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
Step 3. 설정 파일 추가 (application.yml)
발급받은 키를 등록합니다.
- 파일:
application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: 여러분의_클라이언트_ID
client-secret: 여러분의_클라이언트_Secret
scope:
- email
- profile
Step 4. Member Entity 수정
소셜 로그인 사용자는 비밀번호가 없습니다. 그리고 "구글로 가입했는지", "카카오로 가입했는지" 구분할 필드가 필요합니다.
- 위치:
src/main/java/com/example/board/domain/entity/Member.java
// ... 기존 코드 ...
public class Member implements UserDetails {
// ... 기존 필드 ...
// OAuth2를 위해 추가되는 필드
private String provider; // google, kakao, naver
private String providerId; // 소셜 서비스의 식별자 값
@Builder
public Member(..., String provider, String providerId) {
// ... 기존 생성자 로직 ...
this.provider = provider;
this.providerId = providerId;
}
// ...
}
(기존 DB 테이블에 컬럼을 추가해야 하므로, H2라면 자동 반영되지만 MySQL이라면 alter table 쿼리가 필요할 수 있습니다.)
Step 5. 소셜 유저 정보 가져오기 (CustomOAuth2UserService)
구글 로그인 버튼을 누르고 인증이 완료되면, 구글이 사용자 정보를 줍니다. 이 정보를 받아서 "우리 DB에 없으면 회원가입(Save), 있으면 업데이트" 하는 로직입니다.
- 위치:
src/main/java/com/example/board/service/CustomOAuth2UserService.java
package com.example.board.service;
import com.example.board.domain.entity.Member;
import com.example.board.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1. 기본 OAuth2UserService 객체 생성
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 2. 서비스 구분 (구글, 카카오, 네이버 등)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
// 3. 사용자 정보 추출
Map<String, Object> attributes = oAuth2User.getAttributes();
String email = (String) attributes.get("email");
String name = (String) attributes.get("name");
// (참고: 카카오, 네이버는 attributes 구조가 달라서 별도 DTO로 추출하는 과정이 필요하지만,
// 여기선 구글 기준으로 간단히 설명합니다.)
log.info("Social Login Info: provider={}, email={}", registrationId, email);
// 4. DB 저장 또는 업데이트
Member member = saveOrUpdate(registrationId, email, name);
// 5. UserDetails와 비슷한 OAuth2User 반환
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(member.getRoles().get(0))),
attributes,
userNameAttributeName
);
}
private Member saveOrUpdate(String provider, String email, String name) {
Member member = memberRepository.findByUsername(email)
.map(entity -> entity.update(name)) // 이름이 바뀌었으면 업데이트 (update 메서드 Member 엔티티에 추가 필요)
.orElse(Member.builder()
.username(email)
.password("") // 소셜 로그인은 비번 없음
.nickname(name)
.provider(provider)
.roles(Collections.singletonList("USER"))
.build());
return memberRepository.save(member);
}
}
Step 6. 성공 시 JWT 발급 (OAuth2SuccessHandler)
여기가 가장 중요합니다!
세션 기반이었다면 그냥 메인 페이지로 가면 되지만, 우리는 JWT를 써야 합니다.
따라서 로그인 성공 후 JWT를 생성해서 프론트엔드로 전달해주는 핸들러가 필요합니다.
- 위치:
src/main/java/com/example/board/oauth/OAuth2SuccessHandler.java
package com.example.board.oauth;
import com.example.board.dto.JwtToken;
import com.example.board.jwt.JwtTokenProvider;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 1. 토큰 생성
JwtToken jwtToken = jwtTokenProvider.createToken(authentication);
// 2. 토큰을 가지고 프론트엔드로 리다이렉트
// (실무에서는 프론트엔드 주소로 보내지만, 여기선 테스트용으로 URL 파라미터에 토큰을 붙여서 보냄)
String targetUrl = UriComponentsBuilder.fromUriString("http://localhost:8080/login/oauth2/code/google") // 프론트엔드 페이지 주소
.queryParam("accessToken", jwtToken.getAccessToken())
.queryParam("refreshToken", jwtToken.getRefreshToken())
.build().toUriString();
log.info("Social Login Success! Redirect to: {}", targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
Step 7. SecurityConfig 설정
이제 만든 서비스와 핸들러를 시큐리티 설정에 등록합니다.
- 위치:
src/main/java/com/example/board/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final OAuth2SuccessHandler oAuth2SuccessHandler; // 주입
private final CustomOAuth2UserService customOAuth2UserService; // 주입
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ... (기존 설정들: csrf, session, authorizeRequests 등) ...
// [OAuth2 설정 추가]
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint((userInfo) -> userInfo
.userService(customOAuth2UserService) // 유저 정보 가져오는 서비스
)
.successHandler(oAuth2SuccessHandler) // 성공 시 토큰 발행 핸들러
);
// ... (필터 추가 설정들) ...
return http.build();
}
}
Step 8. 테스트
- 서버를 시작합니다.
- 브라우저 주소창에 다음 주소를 입력합니다. (프론트엔드의 "구글 로그인" 버튼이 이 주소를 호출합니다.)
http://localhost:8080/oauth2/authorization/google
- 구글 로그인 화면이 뜨고 로그인을 완료합니다.
- 로그인이 성공하면 아까 핸들러에서 설정한 주소로 리다이렉트됩니다.
http://localhost:8080/...?accessToken=eyJ...&refreshToken=eyJ...
- 주소창에 있는 AccessToken을 복사해서 Swagger나 Postman에서 사용하면 인증 성공! 🎉
🎉 마무리
이제 여러분의 서비스는:
- 일반 회원가입도 되고,
- 구글 소셜 로그인도 되며,
- 어느 쪽으로 로그인하든 동일한 JWT 토큰을 발급받아 API를 사용할 수 있는 강력한 하이브리드 인증 시스템이 되었습니다.
카카오나 네이버도 CustomOAuth2UserService에서 registrationId로 구분하여 속성(Attribute) 파싱 로직만 추가하면 금방 붙일 수 있습니다.
고생하셨습니다!
반응형
'IT > SpringBoot' 카테고리의 다른 글
| [Spring Boot] 로그, 노가다 그만! AOP로 요청/응답/시간 자동 기록하기 (0) | 2026.02.27 |
|---|---|
| [Spring Boot] 기초편 - System.out.println은 그만! 로그(Log) 제대로 찍기 (0) | 2026.02.26 |
| [Spring Security] 15편 - API 문서를 자동으로! Swagger (SpringDoc) 적용하기 (0) | 2026.02.25 |
| [Spring Security] 14편 - 필터 예외 처리의 정석 (ExceptionHandlerFilter) (0) | 2026.02.24 |
| [Spring Security] 13편 - 예외 처리 (401/403 예쁘게 응답하기) (0) | 2026.02.24 |
Comments
