본문 바로가기
Spring/Security

[Spring Security] OAuth2 로그인 최종장: 토큰 정책과 앱 이동 흐름

by coding_whale 2026. 5. 11.
반응형

이전 게시물에서 유저가 누구인지 판별하고 USER 혹은 NEW_USER라는 깃발(Authority)을 꽂았다면, 이제는 그 깃발을 보고 실제 어떤 열쇠(Token)를 줄 것인지, 그리고 유저를 어떤 문(Redirect)으로 보낼 것인지 결정할 차례다.
이 과정에서 성공 핸들러와 서비스의 완벽한 협업이 핵심이다.

 

[Spring Security] OAuth2 로그인 성공 후: 신규와 기존 사용자를 가르는 설계의 모든 것

OAuth2 인증 성공은 끝이 아니라 새로운 데이터 흐름의 시작이다. 서버는 공급자(Google, Naver 등)가 던져준 원본 데이터를 받아서 우리 시스템의 사용자 모델로 연결하고, 상태에 따라 다른 토큰을

myblog01150.tistory.com

 

 

1. 전체 구조 조망 (The Orchestration)

본질은 '조율(Orchestration)'이다. 핸들러는 전체적인 흐름을 지휘하고, 서비스는 구체적인 토큰 정책을 집행한다.

상세 시퀀스 다이어그램

이 단계의 경계를 명확히 하면 다음과 같다.

 

 

2. 핵심 설계 이론: 왜 이렇게 나누었을까?

① 핸들러 vs 서비스의 책임 분리 (Separation of Concerns)

  • 핸들러(Handler): Spring Security의 기술적 접점(HttpServletRequest, Redirect 등)을 담당한다. 즉, "어떻게 응답할 것인가"라는 통신 방식에 집중한다.
  • 서비스(Service): 순수 비즈니스 정책(토큰 종류 결정, 유저 상태 확인 등)을 담당한다. 즉, "무엇을 줄 것인가"라는 정책 결정에 집중한다.
  • 장점: 나중에 앱 리다이렉트 방식이 JSON 응답 방식으로 바뀌더라도, 토큰 정책을 담은 서비스 코드는 단 한 줄도 고칠 필요가 없다.

 ② 인증 성공 ≠ 즉시 서비스 권한 (Safe Landing)

소셜 인증에 성공했다고 해서 바로 우리 서비스의 모든 기능을 쓸 수 있게 하면 안 된다. 추가 정보(나이, 성별 등)가 없는 유저는 비즈니스 로직(추천 API 등)에서 에러를 유발하는 시한폭탄이다. 따라서 tempToken이라는 '임시 출입증'을 발급해 가입 경로로만 유도하는 방패가 필요하다.

 ③ 전달 채널 리스크 관리

앱 딥링크의 쿼리 파라미터로 토큰을 보내는 방식은 구현이 매우 간단하지만, 브라우저 히스토리나 서버 로그에 토큰이 남을 위험이 있다. 이를 방어하기 위해 토큰의 TTL(유효 기간)을 극도로 짧게(예: 1시간) 가져가고, 민감한 개인정보는 담지 않는 정책이 필수다.

 

 

3. 실행 순서 및 파일 위치

  1. 성공 이벤트 처리: auth/handler/OAuth2JwtSuccessHandler.java -> onAuthenticationSuccess(...)
  2. 토큰 정책 집행: auth/service/OAuthLoginSuccessService.java -> handle(...)
  3. 토큰 실무 발행: auth/config/JwtTokenProvider.java -> createAccessToken, createTempToken 등

 

 

4. 상세 코드 분석

A. OAuth2JwtSuccessHandler: 흐름의 지휘자

D4에서 준비된 신호를 소비하여 최종 여정을 결정하는 곳이다.

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 
                                    Authentication authentication) throws IOException {
    
    // 1. [Signal Reading] D4에서 principal 권한에 심어둔 NEW_USER 신호 확인
    boolean isNewUser = authentication.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("NEW_USER"));

    // 2. principal에서 데이터 추출 (생략) 및 서비스 위임
    // 기술적인 영역(Handler)에서 비즈니스 영역(Service)으로 바통 터치
    var result = successService.handle(provider, oauthId, email, name, isNewUser);

    // 3. [Redirect 분기] 결과에 따라 앱의 다른 입구로 안내
    if (result.newUser()) {
        // 신규: 온보딩 경로로 이동시키며 임시 토큰 전달
        response.sendRedirect("프론트 url://onboarding/age?tempToken=" + result.tempToken());
    } else {
        // 기존: 메인 인증 경로로 이동시키며 정식 토큰 세트 전달
        response.sendRedirect("프론트 url://auth"
                + "?accessToken=" + result.accessToken()
                + "&refreshToken=" + result.refreshToken()
                + "&newUser=false");
    }
}

 

B. OAuthLoginSuccessService: 정책의 집행자

"어떤 열쇠를 깎아서 줄 것인가?"를 최종 집행한다.

public Result handle(String providerStr, String oauthId, String email, String name, boolean ignored) {
    OauthProvider provider = OauthProvider.valueOf(providerStr);
    
    // 1. [Final Check] 다시 한번 DB를 조회하여 정합성 확인
    Optional<User> existing = userRepository.findByOauthProviderAndOauthId(provider, oauthId);

    // 2. [Policy Distribution] 사용자 상태에 맞는 토큰 전략 실행
    if (existing.isPresent()) {
        // [기존 사용자 정책] 유저 엔티티 기반 정식 토큰 세트 발급
        User user = existing.get();
        return new Result(false,
                jwtTokenProvider.createAccessToken(user),
                jwtTokenProvider.createRefreshToken(user),
                null);
    }

    // [신규 사용자 정책] Stateless 가입을 위한 임시 토큰 발급
    // DB를 오염시키지 않기 위해 필수 정보를 Base64로 말아서 보냄
    String tempToken = jwtTokenProvider.createTempToken(base64Data, 60 * 60 * 1000); // 1시간 유효
    return new Result(true, null, null, tempToken);
}

 

 

5. 실무 관점에서의 리뷰 (Practical Insights)

① Stateless 가입의 실무적 가치

현재 코드는 가입 버튼을 누르기 전까지 DB에 데이터를 생성하지 않는다. 실무에서는 '로그인만 해보고 가입은 안 하는 유저'가 엄청나게 많다. 만약 처음부터 DB에 행을 생성했다면, 우리 서비스는 '미완성 유저'들로 가득 찼을 것이다. 현재 방식은 DB의 정합성을 지키는 아주 깔끔한 설계다.

② 리다이렉트 토큰 전달의 위험성 (Deep Dive)

현재 sendRedirect의 쿼리 파라미터로 토큰을 보낸다.

  • 개선책: 토큰 내부에 가입 정보를 다 넣으면 토큰이 너무 길어져 URL이 잘릴 수 있다. 실무에서는 정보를 Redis에 잠깐 넣고, 그 'Key(UUID)'값만 토큰에 담아 넘긴 뒤 가입 시점에 Redis에서 꺼내 쓰는 방식을 선호한다.

③ 토큰 유효 기간(TTL) 전략

정식 AccessToken은 짧게(예: 30분), RefreshToken은 길게(예: 2주), 그리고 tempToken은 온보딩 과정이 끝날 정도의 적당한 시간(예: 1시간)으로 설정하여 노출 리스크를 최소화하는 정책이 실전 보안의 핵심이다.

 

 

6. 장애 시나리오 및 디버깅 가이드

  1. 로그인 후 앱 복귀 실패:
    • 체크: 리다이렉트 URL 쿼리 파싱 로직이나 앱의 딥링크 스킴 등록 여부를 확인하라.
  2. 신규/기존 판별 반전:
    • 체크: authority(NEW_USER) 판별 경로가 D4에서 꽂은 깃발과 정확히 매핑되는지 확인하라.
  3. 토큰 발급 실패:
    • 체크: provider Enum 변환 오류, 혹은 JWT Secret 설정이 누락되지 않았는지 체크하라.

 

 

7. 마무리: 단계별 책임 경계 정리

구분 인증 및 판별 단계 최종 정책 집행 단계
입력 Provider Raw JSON 준비 완료된 사용자 객체 (신호 포함)
핵심 동작 데이터 정제 및 가입 여부 판별 토큰 전략 결정 및 앱 리다이렉트
출력 상태 신호가 심어진 객체 상태별 토큰 및 리다이렉트 결과

"이 최종 처리 과정의 본질은 앞서 판별된 신호를 해석하여, 유저의 상태에 최적화된 토큰과 최적의 앱 화면으로 유저를 안내하는 데 있다."

반응형