본문으로 건너뛰기

OAuthClient

1. OAuth2 Client

1.1 OAuth2 Client 사용 사례

  • OAuth 2.0 또는 OpenID Connect 1.0을 사용하여 사용자 로그인을 구현하고 싶은 경우
  • 사용자를 위해 서드파티 API에 액세스하기 위한 액세스 토큰을 얻기 위해 RestClient를 사용하고 싶은 경우
  • 사용자를 위해 서드파티 API에 액세스하기 위한 액세스 토큰을 얻기 위해 WebClient를 사용하고 싶은 경우
  • 두 가지 모두 구현하고 싶은 경우 (사용자 로그인 그리고 서드파티 API 액세스)

1.2 의존성 추가

  • OAuth2 Client를 시작하려면 프로젝트에 spring-security-oauth2-client 의존성을 추가하세요.
  • Spring Boot를 사용하는 경우, 다음 스타터를 추가하면 됩니다.
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

2. OAuth2 Login

  • OAuth 2.0 로그인 기능은 사용자가 OAuth 2.0 제공업체(GitHub 등)나 OpenID Connect 1.0 제공업체(Google 등)의 기존 계정을 사용하여 애플리케이션에 로그인할 수 있도록 지원합니다.
  • "Google로 로그인" 또는 "GitHub로 로그인"과 같은 소셜 로그인 시나리오를 구현합니다.
  • OAuth2 로그인은 OAuth2 클라이언트 기능 위에 구축된 상위 수준의 기능으로, 독립적으로 동작하지 않으며 OAuth2 클라이언트가 필수적으로 필요합니다.
  • OAuth 2.0 인증 프레임워크와 OpenID Connect Core 1.0에서 명시된 Authorization Code Grant 플로우를 기반으로 구현됩니다.

2.1 Spring Boot 샘플

  • Spring Boot는 OAuth 2.0 로그인을 위한 완전한 자동 구성 기능을 제공합니다.
  • 이 섹션에서는 Google을 인증 제공업체로 사용하여 OAuth 2.0 로그인 샘플을 구성하는 방법을 설명합니다.

2.2 Google OAuth 2.0 로그인 설정

  • 로그인을 위해 Google의 OAuth 2.0 인증 시스템을 사용하려면, OAuth 2.0 자격 증명을 얻기 위해 Google API Console에서 프로젝트를 설정해야 합니다.
  • Google의 OAuth 2.0 구현은 OpenID Connect 1.0 사양을 준수하며 OpenID 인증을 받았습니다.
  • 구글 콘솔에서 프로젝트 설정을 완료한 후, 클라이언트 ID와 클라이언트 시크릿으로 구성된 자격 증명을 가진 새로운 OAuth 클라이언트가 생성됩니다.

2.3 리다이렉트 URI 설정

  • 리다이렉트 URI는 최종 사용자가 Google로 인증을 완료하고 동의 페이지에서 OAuth 클라이언트(이전 단계에서 생성)에 대한 액세스 권한을 부여한 후 사용자 에이전트가 다시 리다이렉트되는 애플리케이션 내의 경로입니다.
  • "Set a redirect URI" 하위 섹션에서 Authorized redirect URIs 필드가 localhost:8080/login/oauth2/code/google로 설정되어 있는지 확인하세요.
  • 기본 리다이렉트 URI 템플릿은 {baseUrl}/login/oauth2/code/{registrationId}입니다. registrationId는 ClientRegistration의 고유 식별자입니다.
  • OAuth 클라이언트가 프록시 서버 뒤에서 실행되는 경우, 애플리케이션이 올바르게 구성되었는지 확인하기 위해 프록시 서버 구성을 확인해야 합니다. 또한 redirect-uri에 지원되는 URI 템플릿 변수들을 참조하세요.

2.4 application.yml 설정

spring:
security:
oauth2:
client:
registration:
google:
client-id: google-client-id
client-secret: google-client-secret
  • 이제 Google과 함께 새로운 OAuth 클라이언트가 있으므로, 인증 플로우에 OAuth 클라이언트를 사용하도록 애플리케이션을 위와 같이 구성합니다.
  • spring.security.oauth2.client.registration은 OAuth 클라이언트 속성의 기본 속성 접두사입니다.
  • 기본 속성 접두사 다음에는 Google과 같은 ClientRegistration의 ID가 옵니다.
  • client-id와 client-secret 속성의 값을 앞서 생성한 OAuth 2.0 자격 증명으로 바꿔주세요.

2.5 애플리케이션 실행

  • Spring Boot 샘플을 실행하고 localhost:8080으로 이동하세요.
  • 그러면 기본 자동 생성된 로그인 페이지로 리다이렉트되며, 이 페이지에는 Google 링크가 표시됩니다.
  • Google 링크를 클릭하면 인증을 위해 Google로 리다이렉트됩니다.
  • Google 계정 자격 증명으로 인증한 후, 동의 화면이 표시됩니다.
  • 동의 화면에서는 앞서 생성한 OAuth 클라이언트에 대한 액세스를 허용하거나 거부하도록 요청합니다. OAuth 클라이언트가 이메일 주소와 기본 프로필 정보에 액세스할 수 있도록 허용하려면 Allow를 클릭하세요.
  • 이 시점에서 OAuth 클라이언트는 UserInfo 엔드포인트에서 이메일 주소와 기본 프로필 정보를 검색하고 인증된 세션을 설정합니다.

2.6 CommonOAuth2Provider

  • CommonOAuth2Provider는 널리 알려진 여러 제공업체(Google, GitHub, Facebook, Okta)에 대한 기본 클라이언트 속성 세트를 미리 정의합니다.
  • 예를 들어, authorization-uri, token-uri, user-info-uri는 제공업체별로 자주 변경되지 않습니다.
  • 따라서 기본값을 제공하여 필요한 구성을 줄이는 것이 합리적입니다.

2.6.1 기본 CommonOAuth2Provider 사용

  • 앞에서 설명한 것처럼 Google 클라이언트를 구성할 때는 client-idclient-secret 속성만 필요합니다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: google-client-id
client-secret: google-client-secret
  • 클라이언트 속성의 자동 기본값 설정은 registrationId(google)가 CommonOAuth2Provider의 GOOGLE enum과 일치하기 때문에 원활하게 작동합니다(대소문자 구분 안 함).

2.6.2 다른 registrationId 사용하기

  • google-login과 같이 다른 registrationId를 지정하고 싶은 경우에도 provider 속성을 구성하여 클라이언트 속성의 자동 기본값 설정을 활용할 수 있습니다
spring:
security:
oauth2:
client:
registration:
google-login: # registrationId를 google-login으로 설정
provider: google # provider 속성을 google로 설정
client-id: google-client-id
client-secret: google-client-secret
  • registrationId는 google-login으로 설정됩니다.
  • provider 속성은 google로 설정되어 CommonOAuth2Provider.GOOGLE.getBuilder()에 설정된 클라이언트 속성의 자동 기본값 설정을 활용합니다.
  • 이 방식을 사용하면 동일한 OAuth2 제공업체에 대해 여러 개의 다른 등록을 만들 수 있습니다.
    • 예를 들어, 서로 다른 scope나 redirect-uri를 가진 여러 Google 로그인 옵션을 제공할 수 있습니다.

2.6.3 지원되는 CommonOAuth2Provider

  • Spring Security는 다음과 같은 제공업체를 기본적으로 지원합니다:
    • GOOGLE: Google OAuth2 로그인
    • GITHUB: GitHub OAuth2 로그인
    • FACEBOOK: Facebook OAuth2 로그인
    • OKTA: Okta OAuth2/OIDC 로그인
  • 각 제공업체에 대해 다음과 같은 기본값이 설정됩니다:
    • Authorization URI
    • Token URI
    • User Info URI
    • User Name Attribute Name
    • Client Name
    • Scopes (제공업체별로 다름)

3 OAuth2 Login 고급 설정

  • 레퍼런스
  • 앞서 보여준 OAuth2 Login 샘플을 사용하면 기본적인 OAuth2 로그인 기능을 구현할 수 있습니다.
  • 기본적인 설정을 사용하면 대부분의 경우 충분하지만, 더 복잡한 요구 사항이 있는 경우 OAuth2 Login을 세밀하게 조정할 수 있습니다.
  • HttpSecurity.oauth2Login() 메서드를 통해 OAuth 2.0 Login의 다양한 구성 옵션을 설정할 수 있습니다.
  • 주요 구성 옵션들은 OAuth 2.0 프로토콜의 엔드포인트별로 그룹화되어 있어 직관적인 설정이 가능합니다.

3.1 OAuth 2.0 프로토콜 엔드포인트

  • OAuth 2.0 Authorization Framework에서 정의하는 주요 엔드포인트는 다음과 같습니다.
    • Authorization Endpoint: 클라이언트가 리소스 소유자로부터 인가를 받기 위해 사용하는 엔드포인트
    • Token Endpoint: 클라이언트가 인가 코드를 액세스 토큰으로 교환하기 위해 사용하는 엔드포인트
    • Redirection Endpoint: 인가 서버가 인가 결과를 클라이언트에게 반환하기 위해 사용하는 엔드포인트
    • UserInfo Endpoint: 인증된 사용자에 대한 클레임 정보를 반환하는 OAuth 2.0 보호 리소스

3.2 OAuth 2.0 Login 구성 예시

@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
// Authorization Endpoint 설정
)
.redirectionEndpoint(redirection -> redirection
// Redirection Endpoint 설정
)
.tokenEndpoint(token -> token
// Token Endpoint 설정
)
.userInfoEndpoint(userInfo -> userInfo
// UserInfo Endpoint 설정
)
);
return http.build();
}
}

3.3 Redirection Endpoint 구성

3.3.1 기본 Redirection Endpoint

  • Redirection Endpoint란 OAuth 2.0 인증 플로우에서 인가 서버가 인가 결과를 클라이언트에게 반환하는 엔드포인트입니다.
  • OAuth 2.0 Login은 Authorization Code Grant를 활용하므로, 인가 자격 증명은 인가 코드입니다.
  • 기본 Authorization Response baseUri는 /login/oauth2/code/*입니다.

3.3.2 커스텀 Redirection Endpoint 설정

@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/callback/*")
)
);
return http.build();
}
}
  • 위 예시에서는 Redirection Endpoint의 baseUri를 /login/oauth2/callback/*로 설정했습니다.
  • 위와 같이 커스텀하게 설정한 경우, ClientRegistration.redirectUri도 일치하도록 수정해야 합니다.

3.4 UserInfo Endpoint 고급 구성

4. OAuth2 Client 핵심 구성 요소

4.1 ClientRegistration

  • ClientRegistration은 OAuth 2.0 Provider에 등록된 클라이언트의 정보를 담는 핵심 클래스입니다.
  • 클라이언트 ID, 시크릿, 인증 방식, 리다이렉트 URI 등 OAuth2 통신에 필요한 모든 정보를 포함합니다.

4.1.1 주요 속성들

  • 기본 클라이언트 정보
    • registrationId: ClientRegistration을 고유하게 식별하는 ID
    • clientId: OAuth2 Provider에서 발급한 클라이언트 식별자
    • clientSecret: 클라이언트 인증을 위한 비밀키
    • clientAuthenticationMethod: Provider와의 인증 방식 (client_secret_basic, client_secret_post 등)
  • OAuth2 플로우 설정
    • authorizationGrantType: 인증 그랜트 타입 (authorization_code, client_credentials 등)
    • redirectUri: 인증 완료 후 리다이렉트될 URI
    • scopes: 요청할 권한 범위 (openid, email, profile 등)
  • Provider 엔드포인트 정보
    • authorizationUri: 인증 서버의 Authorization Endpoint
    • tokenUri: 토큰 발급을 위한 Token Endpoint
    • userInfoEndpoint: 사용자 정보 조회 엔드포인트
    • jwkSetUri: JWT 서명 검증을 위한 JWK Set URI

4.2 ClientRegistrationRepository

  • ClientRegistrationRepository는 여러 OAuth2 클라이언트 등록 정보를 관리하는 저장소 역할을 합니다.
  • Spring Boot는 자동으로 InMemoryClientRegistrationRepository를 생성하여 ApplicationContext에 Bean으로 등록합니다.

4.3 OAuth2AuthorizedClient

  • OAuth2AuthorizedClient는 인증이 완료된 클라이언트를 나타내는 클래스입니다.
  • OAuth2AccessToken, OAuth2RefreshToken, ClientRegistration, 그리고 리소스 소유자(Principal) 정보를 함께 관리합니다.

4.3.1 주요 구성 요소

  • OAuth2AccessToken: API 호출에 사용되는 액세스 토큰
  • OAuth2RefreshToken: 액세스 토큰 갱신을 위한 리프레시 토큰 (선택적)
  • ClientRegistration: 클라이언트 등록 정보
  • principalName: 인증을 허가한 리소스 소유자의 식별자

4.4 OAuth2AuthorizedClientRepository

  • 웹 요청 간에 OAuth2AuthorizedClient 정보를 유지하는 역할
  • HttpSession 기반의 저장소로 주로 사용됨
  • 웹 애플리케이션에서 사용자별 인증 정보 관리

4.5 OAuth2AuthorizedClientService

  • 애플리케이션 레벨에서 OAuth2AuthorizedClient를 관리
  • 서비스 애플리케이션이나 백그라운드 작업에 적합
  • 사용자 상호작용 없이 동작하는 시스템에서 사용

5. 소셜 로그인 상세 플로우

5.1 전체 아키텍처 개요

  • Frontend(React, Vue 등)와 Backend(Spring Boot) 간의 OAuth2 로그인 플로우를 이해하기 위해 전체 아키텍처를 살펴보겠습니다.
  • 이 아키텍처는 JWT를 사용하여 인증을 처리하고, OAuth2 Provider(예: Google)와 통신하여 사용자 인증을 수행합니다.
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ Frontend │────▶│ Backend │────▶│OAuth2 Provider│
│ (SPA) │◀────│(Spring Boot)│◀────│ (Google) │
└─────────────┘ └─────────────┘ └─────────────┘
JWT OAuth2 OAuth2

5.2 OAuth2 로그인 플로우 상세 분석

  • OAuth2 로그인 플로우는 여러 Spring Security 컴포넌트들이 유기적으로 협력하여 동작합니다.
  • 각 단계별로 어떤 컴포넌트가 어떤 역할을 수행하는지 상세히 살펴보겠습니다.

5.2.1 인증 시작 단계

  • 사용자가 "Google로 로그인" 버튼을 클릭했을 때 인증 플로우가 시작됩니다.
  • 프론트엔드는 백엔드의 /oauth2/authorization/google 엔드포인트로 요청을 보냅니다.
  • 이 요청은 OAuth2AuthorizationRequestRedirectFilter에 의해 가로채집니다.
  • 필터는 ClientRegistrationRepository에서 "google"이라는 registrationId로 등록된 ClientRegistration 정보를 조회합니다.
    • registrationId는 요청 엔드포인트(/oauth2/authorization/{registrationId})의 마지막 값에서 추출됩니다. 여기서는 google입니다.
  • DefaultOAuth2AuthorizationRequestResolver가 OAuth2 인증 요청을 생성합니다:
    • Google의 authorization-uri를 기반으로 리다이렉트 URL 구성
    • client-id, redirect-uri, response-type, scope 등의 파라미터 추가
    • CSRF 공격 방지를 위한 state 파라미터 생성 (랜덤 UUID)
    • PKCE(Proof Key for Code Exchange) 사용 시 code_challenge 추가
  • 생성된 OAuth2AuthorizationRequest 객체는 AuthorizationRequestRepository에 저장됩니다 (기본적으로 HttpSession 사용).
  • 필터는 HTTP 302 응답을 생성하고, Location 헤더에 위에서 만든 Google 인증 URL을 넣어 브라우저에 보냅니다.
  • 브라우저는 이 302 응답을 받아 자동으로 Google 인증 서버로 이동(리다이렉트)합니다.
  • 사용자는 Google 로그인 및 동의 화면을 보게 됩니다.

5.2.2 OAuth2 Provider 인증 단계

  • 이전 단계에서 브라우저가 Google 인증 서버로 리다이렉트되었습니다.
  • 따라서 사용자는 Google 로그인 페이지를 보게 됩니다.
  • 이미 Google에 로그인되어 있다면 바로 권한 동의 화면으로 이동합니다.
  • 권한 동의 화면에서는 애플리케이션이 요청한 scope(email, profile 등)에 대한 접근 권한을 표시합니다.
  • 사용자가 동의하면 Google은 Authorization Code와 state 파라미터를, Google API 콘솔에서 등록한 redirect-uri로 브라우저를 리다이렉트하여 전달합니다.
    • Google API 콘솔에서 등록한 redirect-uri는 http://localhost:8080/login/oauth2/code/google로 설정했다고 가정합니다.

5.2.3 Authorization Code 처리 단계

  • 리다이렉트된 브라우저가 /login/oauth2/code/google?code=인증코드&state=상태값 형태로 백엔드에 요청합니다.
  • OAuth2LoginAuthenticationFilter가 이 요청을 가로채고 처리를 시작합니다.
  • 먼저 AuthorizationRequestRepository에서 저장된 OAuth2AuthorizationRequest를 조회합니다.
  • state 파라미터를 검증하여 CSRF 공격이 아님을 확인합니다.
  • OAuth2LoginAuthenticationToken을 생성하여 AuthenticationManager에 전달합니다.
  • OAuth2LoginAuthenticationProvider가 실제 인증 처리를 수행합니다:
    • OAuth2AccessTokenResponseClient를 사용하여 Authorization Code를 Access Token으로 교환합니다.
    • 내부적으로 Google의 token endpoint로 POST 요청 전송
    • client-id, client-secret, code, grant-type 등을 전송
    • 응답으로 access_token, token_type, expires_in, scope 등을 받음

5.2.4 사용자 정보 조회 단계

  • Token 교환이 성공하면 OAuth2UserService가 호출됩니다.
  • 기본적으로 DefaultOAuth2UserService가 사용되지만, 커스터마이징을 위해 CustomOAuth2UserService를 구현합니다.
  • OAuth2UserRequest 객체가 생성되어 서비스에 전달됩니다:
    • ClientRegistration 정보
    • OAuth2AccessToken
    • 추가 파라미터들
  • UserInfo Endpoint로 GET 요청을 보내 사용자 정보를 조회합니다:
    • Authorization 헤더에 Bearer 토큰 포함
    • Google의 경우 https://www.googleapis.com/oauth2/v2/userinfo 엔드포인트 사용
  • 응답으로 받은 JSON 데이터를 OAuth2User 객체로 변환합니다.

5.2.5 커스텀 사용자 정보 처리

CustomOAuth2UserService에서의 처리:

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1. 부모 클래스의 loadUser를 호출하여 기본 OAuth2User 획득
OAuth2User oauth2User = super.loadUser(userRequest);

// 2. Provider 정보 확인
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

// 3. Provider별 사용자 정보 파싱
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
registrationId,
oauth2User.getAttributes()
);

// 4. 로컬 데이터베이스와 동기화
User user = processOAuth2User(registrationId, userInfo);

// 5. CustomOAuth2User 객체 생성 및 반환
return new CustomOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
oauth2User.getAttributes(),
userNameAttributeName,
user
);
}

private User processOAuth2User(String registrationId, OAuth2UserInfo userInfo) {
// 이메일이 없는 경우 예외 처리
if (StringUtils.isEmpty(userInfo.getEmail())) {
throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
}

// 기존 사용자 조회
Optional<User> userOptional = userRepository.findByEmail(userInfo.getEmail());
User user;

if (userOptional.isPresent()) {
user = userOptional.get();
// Provider가 다른 경우 예외 처리
if (!user.getProvider().equals(AuthProvider.valueOf(registrationId.toUpperCase()))) {
throw new OAuth2AuthenticationProcessingException(
"Looks like you're signed up with " + user.getProvider() + " account. " +
"Please use your " + user.getProvider() + " account to login."
);
}
// 사용자 정보 업데이트
user = updateExistingUser(user, userInfo);
} else {
// 새 사용자 등록
user = registerNewUser(registrationId, userInfo);
}

return user;
}
}
  • 이 단계에서는 OAuth2 Provider로부터 받은 사용자 정보를 애플리케이션의 도메인 모델로 변환합니다.
  • Provider별로 다른 응답 형식을 통일된 인터페이스로 추상화합니다.
  • 데이터베이스에 사용자 정보를 저장하거나 업데이트합니다.
  • 필요한 권한(authorities)을 설정합니다.

5.2.6 인증 완료 및 후처리

인증 성공 후의 처리:

  • OAuth2LoginAuthenticationProvider는 최종적으로 OAuth2LoginAuthenticationToken을 생성합니다.
  • 이 토큰은 인증된 상태를 나타내며, Principal로 OAuth2User 객체를 포함합니다.
  • SecurityContextHolder에 인증 정보가 저장됩니다.
  • OAuth2AuthorizedClientServiceOAuth2AuthorizedClient 객체를 생성하여 저장합니다:
    • ClientRegistration
    • PrincipalName
    • OAuth2AccessToken
    • OAuth2RefreshToken (있는 경우)
  • 설정된 AuthenticationSuccessHandler가 호출됩니다.

5.2.7 JWT 토큰 발급 및 리다이렉트

최종 클라이언트 응답:

  • OAuth2AuthenticationSuccessHandler에서 JWT 토큰을 생성합니다.
  • 인증된 사용자 정보를 기반으로 클레임을 구성합니다.
  • 프론트엔드 URL로 토큰과 함께 리다이렉트합니다.
  • 프론트엔드는 URL에서 토큰을 추출하여 로컬 스토리지에 저장합니다.

5.3 핵심 컴포넌트 상호작용 다이어그램

┌─────────────────────┐
│ OAuth2Authorization │
│ RequestRedirectFilter│
└──────────┬──────────┘
│ 1. 인증 요청 시작

┌─────────────────────┐
│ClientRegistration │
│ Repository │
└──────────┬──────────┘
│ 2. 클라이언트 정보 조회

┌─────────────────────┐
│ OAuth2 Provider │
│ (Google) │
└──────────┬──────────┘
│ 3. 사용자 인증

┌─────────────────────┐
│OAuth2Login │
│AuthenticationFilter │
└──────────┬──────────┘
│ 4. Authorization Code 처리

┌─────────────────────┐
│OAuth2AccessToken │
│ ResponseClient │
└──────────┬──────────┘
│ 5. Token 교환

┌─────────────────────┐
│CustomOAuth2User │
│ Service │
└──────────┬──────────┘
│ 6. 사용자 정보 처리

┌─────────────────────┐
│AuthenticationSuccess│
│ Handler │
└─────────────────────┘
7. JWT 발급 및 리다이렉트

이렇게 Spring Security OAuth2 Client는 여러 컴포넌트들이 체계적으로 협력하여 안전하고 표준화된 OAuth2 로그인 플로우를 구현합니다.

6. JWT 토큰 기반 인증 시스템 구축

6.1 핵심 구현 컴포넌트

6.1 JWT 토큰 서비스

@Service
public class JwtTokenService {

@Value("${jwt.secret}")
private String secretKey;

@Value("${jwt.expiration}")
private Long tokenExpiration;

public String generateToken(Authentication authentication) {
Date expiryDate = new Date(System.currentTimeMillis() + tokenExpiration);

return Jwts.builder()
.setSubject(authentication.getName())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}

public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();

return claims.getSubject();
}
}

6.2 Custom OAuth2 User Service

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);

// Provider별 사용자 정보 추출
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
userRequest.getClientRegistration().getRegistrationId(),
oauth2User.getAttributes()
);

// 사용자 조회 또는 생성
User user = findOrCreateUser(userInfo);

return new CustomOAuth2User(oauth2User, user);
}

private User findOrCreateUser(OAuth2UserInfo userInfo) {
return userRepository.findByEmail(userInfo.getEmail())
.map(user -> updateUser(user, userInfo))
.orElseGet(() -> createUser(userInfo));
}
}

6.3 OAuth2 인증 성공 핸들러

@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JwtTokenService tokenService;

@Value("${app.oauth2.redirect-uri}")
private String redirectUri;

@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
String token = tokenService.generateToken(authentication);

String targetUrl = UriComponentsBuilder.fromUriString(redirectUri)
.queryParam("token", token)
.build().toUriString();

getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}

6.4 JWT 인증 필터

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenService tokenService;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractTokenFromRequest(request);

if (token != null && tokenService.validateToken(token)) {
String username = tokenService.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String extractTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

6.5 사용자 도메인 모델

@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String email;

@Column(nullable = false)
private String name;

private String profileImageUrl;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AuthProvider provider;

@Column(nullable = false)
private String providerId;

@Column(nullable = false)
private LocalDateTime createdAt;

private LocalDateTime updatedAt;

@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}

@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

7. 보안 설정 통합

7.1 완성된 Security Configuration

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2AuthenticationSuccessHandler successHandler;
private final OAuth2AuthenticationFailureHandler failureHandler;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/error", "/oauth2/**").permitAll()
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(successHandler)
.failureHandler(failureHandler)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

7.2 환경별 설정 관리

application.yml

spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- openid
- email
- profile
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope:
- user:email
- read:user

app:
oauth2:
redirect-uri: http://localhost:3000/oauth2/redirect
jwt:
secret: ${JWT_SECRET}
expiration: 86400000 # 24시간

참고