Pangea
⏎
자동 분석 · 1달 전
로그인
데모 · 1D1S 프로젝트를 읽기 전용으로 보고 있습니다. 편집·AI 실행은 할 수 없습니다.내 레포로 시작하기 →
1D1S
홈브리핑진전이슈8일정화면API지도 탐색플로우문서변경

문서
루트/AI 역문서/3. 기술 구현

AI 초안 · 검토 전노근 · 1달 전 수정

3.1 인증과 세션

AI 역문서 · 1D1S-client@ad5edf9 · 1D1S-server-v2@f69a4eb 기준 · 코드에서 거꾸로 만든 초안입니다. 근거 링크로 확인하세요.

관련 소스 파일

  • 1D1S-client/src/app.feature/auth/api/authApi.ts
  • 1D1S-client/src/app.feature/auth/hooks/useAuthMutations.ts
  • 1D1S-client/src/app.module/api/tokenRefresh.ts
  • 1D1S-client/src/app.module/middleware/auth.ts
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/controller/AppAuthController.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/controller/AppleLoginController.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/controller/AuthTokenController.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/controller/LogoutController.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/controller/NativeSessionController.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/jwt/JwtAuthenticationFilter.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/jwt/JwtTokenExceptionFilter.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/jwt/JwtTokenProvider.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/jwt/MemberPrincipal.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/oauth2/OAuth2LoginResponse.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/service/AppleWebLoginService.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/service/NativeLoginService.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/service/NativeSessionService.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/service/RefreshTokenFingerprint.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/service/RefreshTokenService.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/service/SessionTokenIssuer.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/domain/security/service/TokenReissueService.java
  • 1D1S-server-v2/src/main/java/com/odos/odos_server_v2/config/SecurityConfig.java
목적과 범위

이 페이지는 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.socialLoginOAuth 인가 코드 전달GET /login/oauth2/code/:provider
authApi.appleLoginApple identityToken 전달POST /auth/apple/login
authApi.completeSignUpInfo추가 회원가입 정보 저장PUT /signup/info
authApi.refreshTokenaccess token 갱신refreshAccessTokenOnce
authApi.logout로그아웃 요청POST /auth/logout
useAppleLoginApple 인증 결과를 서버 세션으로 교환하고 라우팅authApi.appleLogin 호출
useLogout로컬 인증 상태·캐시·소켓 정리authApi.logout 호출
authMiddlewareNext.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회원 식별자
emailgetUsername()이 반환하는 이메일
roleSpring Security 권한 생성에 사용되는 역할
signupRoute가입 소셜 경로
sessionTypeWEBVIEW 또는 NATIVE 세션 유형
sessionIdJWT의 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 웹 로그인은 별도의 명시적인 경로를 사용합니다.

  1. 사용자가 클라이언트에서 Apple 팝업 인증을 시작합니다.
  2. useAppleLogin의 signInWithApple()이 Apple 인증 결과를 반환합니다.
  3. useAppleLogin이 authApi.appleLogin()을 호출해 POST /auth/apple/login에 identityToken과 선택적 authorizationCode를 보냅니다.
  4. 서버의 AppleLoginController가 AppleWebLoginService.login()을 호출합니다.
  5. AppleWebLoginService가 AppleOidcCredentialVerifier로 자격 증명을 검증하고, SocialMemberService.findOrCreate()로 회원을 찾거나 생성합니다.
  6. nativeCodeChallenge가 없으면 SessionTokenIssuer.issue(member, SessionType.WEBVIEW)로 세션을 만들고 JwtTokenProvider.sendAccessAndRefreshToken()으로 쿠키를 응답합니다.
  7. 서버는 OAuth2LoginResponse로 isProfileComplete를 반환합니다.
  8. 클라이언트는 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

JWT와 리프레시 토큰
토큰 발급과 쿠키

SessionTokenIssuer.issue()는 하나의 세션에 대해 sessionId를 생성하고 access token과 refresh token을 함께 만듭니다. 두 토큰 모두 JWT이며 session_type과 sid를 포함합니다. refresh token은 RefreshTokenService.saveActiveToken()을 통해 저장되므로 서버는 서명이 유효한 JWT인지뿐 아니라 저장된 refresh token인지도 확인합니다.

JwtTokenProvider는 다음 설정을 사용합니다.

설정 키사용 위치기본값 또는 의미
jwt.secret-keyJWT 서명 키Base64 디코딩 후 HMAC 키로 사용
jwt.access-token-exp-timeaccess token 만료 기간코드에서 기간 값만 주입
jwt.refresh-token-exp-timerefresh token 만료 기간코드에서 기간 값만 주입
jwt.cookie.access-token-nameaccess 쿠키 이름기본값 accessToken
jwt.cookie.refresh-token-namerefresh 쿠키 이름기본값 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를 반환합니다.

  1. refreshAccessTokenOnce()가 호출됩니다.
  2. 네이티브 브리지가 있으면 requestNativeTokenRefresh()를 먼저 시도합니다.
  3. 네이티브 갱신이 NativeTokenRefreshError로 실패하면 웹 쿠키 방식의 GET /auth/token으로 대체합니다.
  4. 웹 방식은 API_BASE_URL/auth/token에 withCredentials: true로 요청합니다.
  5. 성공하면 authStorage.markAuthenticated()로 로그인 힌트를 복구합니다.
  6. 요청 완료 후 inFlight를 null로 되돌립니다.

앱 부팅 시 runAuthBootProbe()가 같은 갱신 경로를 사용합니다. 네이티브 환경에서 첫 요청이 실패하면 native:auth_ready 신호를 기다리고, 다시 실패하면 2.5초 후 한 번 더 시도합니다. 최종 실패 시 authStorage.settleGuest()를 호출합니다.

Next.js 미들웨어

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/createreturnTo를 포함해 로그인 이동
알림/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()를 호출합니다.

  1. 네이티브 클라이언트가 NativeLoginExchangeRequest를 보냅니다.
  2. credentialType이 LOGIN_CODE이면 NativeSessionService.consumeLoginCode()가 일회용 코드와 PKCE verifier를 검증합니다.
  3. 그 외에는 provider별 NativeSocialCredentialVerifier가 소셜 자격 증명을 검증합니다.
  4. SocialMemberService.findOrCreate()로 회원을 조회하거나 생성합니다.
  5. SessionTokenIssuer.issue(member, SessionType.NATIVE)가 네이티브 JWT를 발급합니다.
  6. NativeSessionService.issueWebBootstrapCode()가 웹뷰 부트스트랩용 코드를 발급합니다.
  7. 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와 오류 처리
API입력 위치세션 유형처리
GET /auth/tokenrefresh token 쿠키 또는 헤더WEBVIEW새 토큰을 쿠키로 발급
POST /auth/app/tokenJSON 또는 refresh token 헤더NATIVE새 토큰을 JSON으로 반환
POST /auth/native/token/refreshJSON refreshTokenNATIVE네이티브 토큰을 JSON으로 반환
POST /auth/logoutrefresh token 쿠키 또는 헤더웹·공통현재 refresh token 폐기 및 쿠키 삭제
POST /auth/native/logoutJSON refreshTokenNATIVE회원·세션 유형을 확인하고 토큰 폐기
GET /auth/native/web-session-statusaccess tokenWEBVIEW웹뷰 세션 유형 확인
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 코드
만료된 JWT401ACCESS_TOKEN_EXPIRED
잘못된 JWT 또는 인자401INVALID_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

속성
분류스펙
폴더AI 역문서 / 3. 기술 구현
#AI 역문서
연결된 화면·API10
GET/auth/token
POST/auth/logout
GET/auth/native/web-session-status
POST/auth/app/token
POST/auth/native/web-session-code
POST/auth/native/login/exchange
POST/auth/apple/login
POST/auth/native/logout
POST/auth/native/token/refresh
POST/auth/native/bootstrap
이 문서를 언급한 문서5
3.2 API와 응답 구조스펙2.5 채팅과 신고스펙0 목차스펙2.2 챌린지 만들기스펙1.3 시스템 아키텍처스펙
#AI 역문서
연결된 일정1
10/8→10/14검토v2.4 출시 전 QA