[Spring OAuth2] Security & OAuth2

✅ OAuth2 로그인 정리

OAuth2를 활용하여 네이버, 카카오, 구글 등의 소셜 로그인을 적용하는 방식과 테크 기업들이 선호하는 처리 방법을 정리한다. 특히, API Gateway를 통과하는 구조를 고려하여 최적의 구현 방식을 설명한다.


🔹 1. OAuth2 로그인 흐름

OAuth2 로그인은 권한 코드(Authorization Code) 플로우를 따른다.

📌 OAuth2 로그인 과정

1️⃣ 사용자가 /oauth2/authorization/naver로 로그인 요청 2️⃣ Spring Security가 네이버 로그인 페이지로 리디렉트 3️⃣ 사용자가 네이버 로그인 완료 → Authorization Code를 서버로 반환 4️⃣ 서버가 네이버에 Authorization Code를 보내고, Access Token을 요청 5️⃣ 네이버에서 Access Token과 함께 사용자 정보를 반환 6️⃣ 서버에서 사용자 정보를 DB에 저장하거나 JWT 발급 후 응답


🔹 2. Spring Security 설정

📌 application.yml 설정

spring:
  security:
    oauth2:
      client:
        registration:
          naver:
            client-id: YOUR_CLIENT_ID
            client-secret: YOUR_CLIENT_SECRET
            redirect-uri: "http://localhost:8081/oauth2/callback/naver"
            client-name: Naver
            authorization-grant-type: authorization_code
            scope:
              - name
              - gender
              - mobile
          kakao:
            client-id:
            client-secret:
            redirect-uri:
            client-name: Kakao
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_post
            scope:
                - profile_nickname
          google:
            client-id:
            client-secret:
            scope:
              - email
              - profile
        provider: # kakao, naver만 추가로 작성
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id
          naver:
            authorization_uri: https://nid.naver.com/oauth2.0/authorize
            token_uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user_name_attribute: response

📌 SecurityConfig 설정 (SecurityFilterChain)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/oauth2/**", "/login/**").permitAll()
            .anyRequest().authenticated()
        )
        // .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) 전체 허용
        .csrf(AbstractHttpConfigurer::disable)
        .formLogin(AbstractHttpConfigurer::disable)
        .oauth2Login(oauth2 -> oauth2
            .userInfoEndpoint(userInfoEndpointConfig ->
                userInfoEndpointConfig.userService(customOAuth2UserService)
            )
            .successHandler(oAuth2LoginSuccessHandler)
        );

    return http.build();
}

🔹 3. OAuth2 사용자 정보 처리 방식

OAuth2 로그인 후 사용자 정보를 처리하는 방식은 두 가지가 있다.

✅ 1. userInfoEndpoint() 방식 (Spring Security 자동 처리)

  • Spring Security가 UserInfo API를 호출하여 사용자 정보를 가져옴.
  • Provider(네이버, 카카오, 구글)별로 다른 필드명을 표준화할 수 있음.
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        String provider = userRequest.getClientRegistration().getRegistrationId();
        String email = oAuth2User.getAttribute("email");
        String name = oAuth2User.getAttribute("name");

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                Map.of("provider", provider, "email", email, "name", name),
                "email"
        );
    }
}

✅ 2. successHandler() 방식 (로그인 성공 후 직접 처리)

  • 로그인 성공 후 JWT를 발급하고 응답
  • API Gateway를 사용하는 경우 유용함
@Component
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final TokenService tokenService;
    
    public OAuth2LoginSuccessHandler(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

        String email = oAuth2User.getAttribute("email");
        String name = oAuth2User.getAttribute("name");
        String provider = oAuth2User.getAttribute("provider");

        String accessToken = tokenService.generateToken(email);

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write("{"accessToken": \"" + accessToken + "\", \"email\": \"" + email + "\", \"name\": \"" + name + "\", \"provider\": \"" + provider + "\"}");
    }
}

🔹 4. API Gateway를 통과하는 OAuth2 인증 처리

API Gateway를 사용할 경우, 클라이언트는 JWT를 받아서 API 요청 시 사용해야 함.

📌 API Gateway에서 JWT를 검증하는 방식 1️⃣ 사용자가 /oauth2/authorization/naver로 로그인 요청 2️⃣ OAuth2 로그인 후 successHandler()에서 JWT 발급 3️⃣ 클라이언트가 JWT를 Authorization 헤더에 포함하여 API 요청 4️⃣ API Gateway는 JWT를 검증하여 유효한 요청만 Backend 서비스로 전달

📌 OAuth2 로그인 후 응답 예제

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI...",
  "email": "user@example.com",
  "name": "홍길동",
  "provider": "naver"
}

📌 클라이언트에서 API 요청 시 JWT 사용

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI...

🔹 5. userInfoEndpoint() + successHandler() 조합 방식의 장점

| 기능 | userInfoEndpoint() | successHandler() | |——|———————|——————-| | OAuth2 사용자 정보 가져오기 | ✅ 자동 호출 | ❌ 직접 호출 필요 | | OAuth2 Provider 표준화 | ✅ Provider별 필드 통합 가능 | ❌ 직접 매핑 필요 | | JWT 발급 | ❌ 별도 구현 필요 | ✅ JWT 발급 가능 | | API Gateway 지원 | ❌ 기본 지원 X | ✅ JWT 기반 인증 가능 | | 로그인 성공 후 추가 처리 | ❌ 추가 로직 적용 어려움 | ✅ DB 저장, 리디렉션 가능 |


🎯 최종 정리

✔ OAuth2 로그인 후 userInfoEndpoint()에서 사용자 정보를 가져오고, successHandler()에서 JWT 발급 및 추가 로직을 처리하는 방식이 일반적 ✔ API Gateway를 사용할 경우 successHandler()에서 JWT를 발급하고, Backend 서비스는 JWT 검증만 수행 ✔ MSA 환경에서는 userInfoEndpoint() + successHandler() 조합을 통해 인증을 최적화할 수 있음

🚀 즉, 테크 기업들은 API Gateway + JWT 조합을 통해 OAuth2 인증을 처리하는 경우가 많고, userInfoEndpoint()를 활용하여 표준화한 후 successHandler()에서 추가 처리를 수행하는 방식을 선호함!


© 2023 Lee. All rights reserved.