AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@f69a4eb 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.
관련 소스 파일
이 페이지는 1D1S의 인증 경로를 프론트엔드와 서버 코드 사이의 호출·응답 흐름으로 설명합니다. 소셜 로그인, 로그인 후 추가 회원가입 정보 입력, JWT access token과 refresh token의 발급·회전, 웹뷰 쿠키 세션과 네이티브 앱 세션의 교환 구조를 다룹니다.
클라이언트에서는 authApi, useAppleLogin, useLogout, refreshAccessTokenOnce, Next.js authMiddleware가 인증 상태를 만들고 유지하는 방식을 설명합니다. 서버에서는 SecurityConfig, JwtAuthenticationFilter, JwtTokenProvider, TokenReissueService, NativeSessionService 및 인증 컨트롤러가 요청을 어떻게 처리하는지 연결합니다.
회원 엔티티의 전체 필드와 프로필 화면은 3.3 도메인 데이터 모델 및 2.6 마이페이지와 활동에서 다룹니다. 공통 응답 형식과 예외 응답은 3.2 API와 응답 구조, 화면 라우팅 구조는 4.1 클라이언트 화면 구조, 배포 환경 변수는 6.1 개발과 배포 설정을 참고합니다.
다만 원문에는 클라이언트의 구글 로그인 훅·콜백 화면, 네이티브 셸 자체의 구현, SocialMemberService의 내부 조회 기준이 포함되어 있지 않습니다. 해당 부분은 코드에서 확인되는 연결 지점까지만 기술합니다.
출처: authApi.ts:16-77, SecurityConfig.java:51-134
authApi는 인증 관련 HTTP 요청을 모읍니다. 소셜 로그인은 publicApiClient와 withCredentials: true를 사용해 쿠키를 주고받고, 추가 정보 입력과 로그아웃은 인증된 apiClient를 사용합니다.
| 구성 요소 | 역할 | 서버 연결 |
|---|---|---|
authApi.socialLogin | OAuth 인가 코드 전달 | GET /login/oauth2/code/:provider |
authApi.appleLogin | Apple identityToken 전달 | POST /auth/apple/login |
authApi.completeSignUpInfo | 추가 회원가입 정보 저장 | PUT /signup/info |
authApi.refreshToken | access token 갱신 | refreshAccessTokenOnce |
authApi.logout | 로그아웃 요청 | POST /auth/logout |
useAppleLogin | Apple 인증 결과를 서버 세션으로 교환하고 라우팅 | authApi.appleLogin 호출 |
useLogout | 로컬 인증 상태·캐시·소켓 정리 | authApi.logout 호출 |
authMiddleware | Next.js 보호 경로의 쿠키 확인·갱신 | 백엔드 GET /auth/token 호출 |
서버는 Spring Security의 세션을 사용하지 않고 SessionCreationPolicy.STATELESS를 설정합니다. 따라서 서버의 인증 상태는 HTTP 세션 객체가 아니라 요청에 포함된 JWT를 JwtAuthenticationFilter가 해석해 SecurityContextHolder에 넣는 방식으로 구성됩니다.
JwtAuthenticationFilter는 access token에서 id, email, role, provider, session_type, sid를 읽습니다. id로 MemberRepository에서 회원을 조회하고, 회원의 MemberStatus가 ACTIVE인 경우에만 MemberPrincipal을 생성합니다. 생성된 주체의 권한은 ROLE_와 회원 역할을 조합해 만듭니다.
MemberPrincipal은 UserDetails와 OAuth2User를 동시에 구현하며 다음 인증 정보를 보유합니다.
| 필드 | 의미 |
|---|---|
id | 회원 식별자 |
email | getUsername()이 반환하는 이메일 |
role | Spring Security 권한 생성에 사용되는 역할 |
signupRoute | 가입 소셜 경로 |
sessionType | WEBVIEW 또는 NATIVE 세션 유형 |
sessionId | JWT의 sid 값 |
SecurityConfig는 미인증 요청에 OAuth 리다이렉트를 수행하지 않고 401 UNAUTHORIZED를 반환하도록 HttpStatusEntryPoint를 사용합니다. OAuth 로그인 시작은 클라이언트가 /oauth2/authorization/*으로 직접 이동하는 구조입니다.
출처: auth.ts:172-245, JwtAuthenticationFilter.java:25-81, MemberPrincipal.java:13-44, SecurityConfig.java:64-73
일반 소셜 로그인은 클라이언트가 OAuth 인가 코드를 서버의 GET /login/oauth2/code/:provider로 전달하는 형태입니다. 이 엔드포인트 자체의 컨트롤러 구현은 원문에 없지만, 서버 보안 설정에서 /login/oauth2/**와 /oauth2/**를 허용하고 oauth2Login에 OAuth2LoginSuccessHandler, OAuth2LoginFailureHandler, CustomOAuth2UserService를 연결한 사실을 확인할 수 있습니다.
Apple 웹 로그인은 별도의 명시적인 경로를 사용합니다.
useAppleLogin의 signInWithApple()이 Apple 인증 결과를 반환합니다.useAppleLogin이 authApi.appleLogin()을 호출해 POST /auth/apple/login에 identityToken과 선택적 authorizationCode를 보냅니다.AppleLoginController가 AppleWebLoginService.login()을 호출합니다.AppleWebLoginService가 AppleOidcCredentialVerifier로 자격 증명을 검증하고, SocialMemberService.findOrCreate()로 회원을 찾거나 생성합니다.nativeCodeChallenge가 없으면 SessionTokenIssuer.issue(member, SessionType.WEBVIEW)로 세션을 만들고 JwtTokenProvider.sendAccessAndRefreshToken()으로 쿠키를 응답합니다.OAuth2LoginResponse로 isProfileComplete를 반환합니다.authStorage.markAuthenticated()를 호출한 뒤, 프로필이 미완성인 경우 /signup, 완성된 경우 returnTo 또는 /로 이동합니다.AppleWebLoginService는 Apple authorizationCode를 현재 실제 revoke 요청에 사용하지 않고 로그로만 받습니다. 따라서 Apple 계정 연결 해제 처리까지 구현되었다고 볼 수 없습니다.
소셜 자격 증명이 검증되고 회원이 생성된 뒤에도 member.isProfileComplete()가 false일 수 있습니다. 이 경우 useAppleLogin은 /signup으로 이동합니다. 추가 정보 저장은 authApi.completeSignUpInfo()가 PUT /signup/info로 텍스트 데이터를 전송하는 구조입니다.
이 페이지의 원문에는 해당 /signup 화면과 서버의 회원가입 컨트롤러·DTO 구현이 없으므로, 어떤 필드가 필수인지와 SignUpInfoRequest의 구체적인 필드 목록은 확인할 수 없습니다. 확인되는 범위는 소셜 로그인 응답의 profileComplete 값이 라우팅 조건으로 사용되고, 추가 정보 API가 인증 클라이언트로 호출된다는 점입니다.
출처: authApi.ts:16-50, useAuthMutations.ts:44-85, AppleLoginController.java:19-36, AppleWebLoginService.java:31-77
SessionTokenIssuer.issue()는 하나의 세션에 대해 sessionId를 생성하고 access token과 refresh token을 함께 만듭니다. 두 토큰 모두 JWT이며 session_type과 sid를 포함합니다. refresh token은 RefreshTokenService.saveActiveToken()을 통해 저장되므로 서버는 서명이 유효한 JWT인지뿐 아니라 저장된 refresh token인지도 확인합니다.
JwtTokenProvider는 다음 설정을 사용합니다.
| 설정 키 | 사용 위치 | 기본값 또는 의미 |
|---|---|---|
jwt.secret-key | JWT 서명 키 | Base64 디코딩 후 HMAC 키로 사용 |
jwt.access-token-exp-time | access token 만료 기간 | 코드에서 기간 값만 주입 |
jwt.refresh-token-exp-time | refresh token 만료 기간 | 코드에서 기간 값만 주입 |
jwt.cookie.access-token-name | access 쿠키 이름 | 기본값 accessToken |
jwt.cookie.refresh-token-name | refresh 쿠키 이름 | 기본값 refreshToken |
jwt.cookie.domain | 쿠키 도메인 | 비어 있으면 도메인 속성 생략 |
CORS_EXTRA_ALLOWED_ORIGINS | 추가 CORS Origin | 서버 SecurityConfig에서 사용 |
쿠키는 HttpOnly, Secure, SameSite=None, Path=/로 설정됩니다. 서버는 현재 이름과 레거시 이름인 access_token, refresh_token도 삭제 대상으로 포함합니다. access token은 쿠키뿐 아니라 Authorization: Bearer ... 헤더에서도 읽을 수 있고, refresh token은 Authorization-Refresh: Bearer ... 헤더에서도 읽을 수 있습니다.
TokenReissueService.reissue()는 refresh token을 파싱하고 회원과 session_type을 확인한 뒤 새 access token과 새 refresh token을 생성합니다. 이후 RefreshTokenService.rotate()가 기존 토큰을 폐기하고 동일한 familyId로 새 토큰을 저장합니다.
회전된 refresh token이 다시 제출되면 RefreshTokenService는 기본적으로 재사용으로 판단해 같은 family의 활성 토큰을 폐기하고 REFRESH_TOKEN_REUSE_DETECTED를 발생시킵니다. 다만 REUSE_GRACE인 60초 안의 재제시는 웹 미들웨어와 클라이언트, 여러 웹뷰에서 발생할 수 있는 정상 경합으로 보고 family를 유지합니다.
출처: JwtTokenProvider.java:36-110, SessionTokenIssuer.java:15-32, TokenReissueService.java:15-53, RefreshTokenService.java:44-103
refreshAccessTokenOnce()는 클라이언트의 모든 access token 갱신 요청이 공유하는 single-flight Promise를 사용합니다. 이미 갱신 요청이 진행 중이면 새 요청을 만들지 않고 기존 inFlight를 반환합니다.
refreshAccessTokenOnce()가 호출됩니다.requestNativeTokenRefresh()를 먼저 시도합니다.NativeTokenRefreshError로 실패하면 웹 쿠키 방식의 GET /auth/token으로 대체합니다.API_BASE_URL/auth/token에 withCredentials: true로 요청합니다.authStorage.markAuthenticated()로 로그인 힌트를 복구합니다.inFlight를 null로 되돌립니다.앱 부팅 시 runAuthBootProbe()가 같은 갱신 경로를 사용합니다. 네이티브 환경에서 첫 요청이 실패하면 native:auth_ready 신호를 기다리고, 다시 실패하면 2.5초 후 한 번 더 시도합니다. 최종 실패 시 authStorage.settleGuest()를 호출합니다.
authMiddleware는 보호 경로에 access token 쿠키가 없을 때 서버에서 GET /auth/token을 호출합니다. 갱신에 성공하면 백엔드의 Set-Cookie를 응답에 전달하고, 새 access token을 현재 요청의 쿠키 헤더에 주입합니다. 따라서 같은 문서 요청의 RSC 처리도 새 토큰을 사용합니다.
보호 경로는 다음과 같이 분류됩니다.
| 분류 | 경로 예시 | 실패 시 동작 |
|---|---|---|
list-redirect | /diary/:id | /diary?loginRequired=true로 이동 |
login-redirect | /mypage, /chat, /likes | 로그인 URL로 이동 |
| 편집·생성 | /challenge/:id/edit, /challenge/create | returnTo를 포함해 로그인 이동 |
| 알림 | /notification | 로그인 이동 |
| 강퇴 상세 | /challenge/kick/:id | 로그인 이동 |
세션 힌트 쿠키가 있으나 실제 access token이 없으면 미들웨어가 /auth/token 갱신을 시도합니다. 갱신에 실패해도 list-redirect 경로는 힌트가 있는 경우 통과시킬 수 있고, 네이티브 앱 User-Agent인 경우에는 클라이언트의 재주입·재시도 로직에 처리를 위임합니다. 브라우저의 민감 경로는 buildUnauthorizedResponse()로 차단합니다.
출처: tokenRefresh.ts:13-61, tokenRefresh.ts:64-132, auth.ts:16-30, auth.ts:93-143, auth.ts:180-245
네이티브 앱 세션과 웹뷰 세션은 SessionType.NATIVE와 SessionType.WEBVIEW로 구분됩니다. 네이티브 세션은 JSON 응답으로 access·refresh token을 받고, 웹뷰 세션은 HttpOnly 쿠키를 받습니다.
POST /auth/native/login/exchange는 NativeLoginService.exchange()를 호출합니다.
NativeLoginExchangeRequest를 보냅니다.credentialType이 LOGIN_CODE이면 NativeSessionService.consumeLoginCode()가 일회용 코드와 PKCE verifier를 검증합니다.NativeSocialCredentialVerifier가 소셜 자격 증명을 검증합니다.SocialMemberService.findOrCreate()로 회원을 조회하거나 생성합니다.SessionTokenIssuer.issue(member, SessionType.NATIVE)가 네이티브 JWT를 발급합니다.NativeSessionService.issueWebBootstrapCode()가 웹뷰 부트스트랩용 코드를 발급합니다.NativeLoginExchangeResponse에 네이티브 토큰, 만료 시간, 웹 세션 코드, 프로필 완료 여부를 담아 반환합니다.로그인 교환용 코드는 NativeSessionCodeService가 아니라 NativeSessionService가 생성하며, NativeSessionCodePurpose.LOGIN_EXCHANGE로 저장합니다. 코드는 60초 동안 유효하고 한 번 사용되면 다시 사용할 수 없습니다. PKCE 검증은 SHA-256과 URL-safe Base64 인코딩 결과를 비교합니다.
네이티브 세션으로 인증된 주체는 POST /auth/native/web-session-code를 호출해 WEB_BOOTSTRAP 목적의 코드를 받습니다. 이후 웹뷰가 POST /auth/native/bootstrap에 코드와 검증된 returnUrl을 제출하면 서버는 코드를 소비하고 SessionType.WEBVIEW 토큰을 쿠키로 발급합니다.
returnUrl은 https만 허용하고, WEBVIEW_RETURN_HOSTS에 포함된 호스트만 허용합니다. 경로는 비어 있거나 /이어야 하며 query와 fragment는 허용하지 않습니다. 성공 시 303 SEE_OTHER로 해당 호스트에 이동하고, 실패 시 검증 단계에 따라 HTML 오류 또는 /login?error=...로 이동합니다.
Apple 웹 로그인도 nativeCodeChallenge가 전달되면 즉시 웹 쿠키를 발급하지 않고 NativeSessionService.issueCode()로 일회용 로그인 코드를 발급합니다. 이 코드는 NativeLoginService.exchange()에서 PKCE 검증 후 네이티브 세션으로 교환됩니다.
출처: NativeSessionController.java:38-119, NativeLoginService.java:48-75, NativeSessionService.java:27-113, AppleWebLoginService.java:61-75
| API | 입력 위치 | 세션 유형 | 처리 |
|---|---|---|---|
GET /auth/token | refresh token 쿠키 또는 헤더 | WEBVIEW | 새 토큰을 쿠키로 발급 |
POST /auth/app/token | JSON 또는 refresh token 헤더 | NATIVE | 새 토큰을 JSON으로 반환 |
POST /auth/native/token/refresh | JSON refreshToken | NATIVE | 네이티브 토큰을 JSON으로 반환 |
POST /auth/logout | refresh token 쿠키 또는 헤더 | 웹·공통 | 현재 refresh token 폐기 및 쿠키 삭제 |
POST /auth/native/logout | JSON refreshToken | NATIVE | 회원·세션 유형을 확인하고 토큰 폐기 |
GET /auth/native/web-session-status | access token | WEBVIEW | 웹뷰 세션 유형 확인 |
POST /auth/native/bootstrap | 일회용 code와 returnUrl | 웹뷰 전환 | 쿠키 발급 후 리다이렉트 |
AuthTokenController는 refresh token이 없으면 INVALID_REFRESH_TOKEN을 발생시키고, 재발급 실패 시 JwtTokenProvider.clearTokenCookies()로 쿠키를 삭제합니다. AppAuthController는 요청 본문의 refreshToken을 우선 사용하고, 없으면 요청에서 refresh token을 추출합니다. SessionType.NATIVE를 기대 세션 유형으로 전달해 웹 토큰이 네이티브 API에서 사용되지 않게 합니다.
JWT 파싱 중 발생한 오류는 JwtTokenExceptionFilter가 처리합니다.
| 오류 상황 | HTTP 상태 | 응답 코드 |
|---|---|---|
CustomException | 예외에 지정된 상태 | ErrorCode 코드 |
| 만료된 JWT | 401 | ACCESS_TOKEN_EXPIRED |
| 잘못된 JWT 또는 인자 | 401 | INVALID_JWT |
클라이언트의 로그아웃은 서버 응답을 기다리기 전에 시작됩니다. useLogout.onMutate()가 authStorage.clearTokens(), clearCachedSidebar(), queryClient.cancelQueries(), queryClient.removeQueries(), purgePersistedQueries(), closeChatSocket()을 수행합니다. 이후 authApi.logout()을 호출하고, 완료 여부와 관계없이 onSettled에서 postNativeMessage({ type: 'logout' })를 전송합니다.
서버의 LogoutController는 현재 refresh token을 폐기하고 토큰 쿠키를 삭제하며 ChatSessionTerminator.terminateAllFor()로 해당 회원의 채팅 세션도 종료합니다.
출처: AuthTokenController.java:18-45, AppAuthController.java:22-49, JwtTokenExceptionFilter.java:12-40, useAuthMutations.ts:100-138, LogoutController.java:18-35
GET /login/oauth2/code/:provider를 실제로 호출하는 구글 등 소셜 로그인 콜백 화면과 OAuth2LoginSuccessHandler의 전체 구현은 어디에 있습니까?PUT /signup/info가 받는 SignUpInfoRequest 필드와 회원 프로필 완료 조건은 어떤 코드에서 정의합니까?SocialMemberService.findOrCreate()가 이메일·provider·subject를 어떤 유일성 기준으로 회원과 연결합니까?refreshAccessTokenOnce()가 호출되는 Axios 인터셉터와 네이티브 브리지의 실제 메시지 계약은 어디에 있습니까?NativeSessionCode 테이블의 컬럼, 인덱스, 만료·사용 상태 매핑은 어떤 엔티티·마이그레이션 파일에 정의되어 있습니까?RefreshTokenService.REUSE_GRACE의 60초 유예와 refresh token 만료 기간의 운영 설정값은 어떤 환경별 설정에서 관리합니까?CookieMutationOriginFilter가 쿠키를 변경하는 요청의 Origin을 어떻게 제한하는지 확인할 수 있는 구현이 필요합니다.POST /auth/native/bootstrap에 사용되는 네이티브 WebView의 loadRequest 호출과 returnUrl 생성 규칙은 네이티브 앱 레포에서 확인해야 합니다.출처: NativeSessionController.java:87-119, SecurityConfig.java:42-50, authApi.ts:43-50