Skip to content

도메인: Auth (인증/회원)

개요

카카오 / Google / Apple OAuth로 소셜 로그인을 처리하고, HS256 JWT로 세션을 유지한다. 개인정보 최소 수집 원칙에 따라 소셜 로그인 고유 식별자(oauth_provider + oauth_id)만 저장한다.

데이터 모델 (핵심 테이블)

dartbrief.users

컬럼타입설명
idBIGSERIAL PK내부 회원번호
oauth_providerVARCHAR(10)KAKAO / GOOGLE / APPLE
oauth_idVARCHAR(255)소셜 로그인 고유 식별자 (Google/Apple: JWT sub claim, Kakao: /v2/user/me 응답의 숫자 id)
created_atTIMESTAMP(6)가입 시각
updated_atTIMESTAMP(6)nullable
slot_capacityINT관심종목 슬롯 한도 (기본 3, CHECK >= 3)

UNIQUE: (oauth_provider, oauth_id)

동작 플로우

Google 로그인

mermaid
sequenceDiagram
    participant App as Flutter 앱
    participant Google as Google Sign-In SDK / tokeninfo API
    participant BE as 백엔드

    App->>Google: signIn()
    Google-->>App: GoogleSignInAccount (idToken)

    App->>BE: POST /api/v1/auth/google/token<br/>{ "id_token": "..." }
    BE->>Google: GET oauth2.googleapis.com/tokeninfo?id_token=...
    Google-->>BE: { sub, aud, ... } (Google 서버 검증 위임)
    BE->>BE: aud가 GOOGLE_ALLOWED_CLIENT_IDS 에 있는지 확인

    alt 신규 사용자
        BE->>BE: INSERT INTO users (oauth_provider='GOOGLE', oauth_id=sub)
    else 기존 사용자
        BE->>BE: SELECT users WHERE oauth_provider='GOOGLE' AND oauth_id=sub
    end

    BE-->>App: { "token": "<JWT>" }

Apple 로그인 (iOS 전용)

mermaid
sequenceDiagram
    participant App as Flutter 앱
    participant Apple as Apple Sign-In SDK
    participant BE as 백엔드

    App->>App: rawNonce 생성 (32자 random)<br/>hashedNonce = sha256(rawNonce)
    App->>Apple: getAppleIDCredential(nonce: hashedNonce)
    Apple-->>App: identityToken (nonce claim = hashedNonce 포함)

    App->>BE: POST /api/v1/auth/apple/token<br/>{ "identity_token": "...", "raw_nonce": rawNonce }
    BE->>Apple: GET appleid.apple.com/auth/keys (JWKS, 6h 캐시)
    Apple-->>BE: RSA 공개키 목록
    BE->>BE: RS256 서명 검증 + iss/aud/exp 확인
    BE->>BE: sha256(rawNonce) == token.nonce 검증 (replay 방지)

    alt 신규 사용자
        BE->>BE: INSERT INTO users (oauth_provider='APPLE', oauth_id=sub)
    else 기존 사용자
        BE->>BE: SELECT users WHERE oauth_provider='APPLE' AND oauth_id=sub
    end

    BE-->>App: { "token": "<JWT>" }

카카오 로그인

mermaid
sequenceDiagram
    participant App as Flutter 앱
    participant Kakao as Kakao SDK
    participant BE as 백엔드

    App->>Kakao: 카카오 로그인 요청
    Kakao-->>App: access_token 반환

    App->>BE: POST /api/v1/auth/kakao/token<br/>{ "access_token": "..." }
    BE->>Kakao: GET kapi.kakao.com/v2/user/me<br/>Authorization: Bearer access_token
    Kakao-->>BE: 카카오 사용자 고유 ID

    alt 신규 사용자
        BE->>BE: INSERT INTO users (oauth_provider, oauth_id)
    else 기존 사용자
        BE->>BE: SELECT users WHERE oauth_provider='KAKAO' AND oauth_id=...
    end

    BE-->>App: { "token": "<JWT>" }

JWT 검증 흐름 (미들웨어)

mermaid
flowchart TD
    A[HTTP 요청] --> B{Authorization: Bearer?}
    B -- 없음 --> C[userID = 0 컨텍스트]
    B -- 있음 --> D[JWT 파싱 + HS256 검증]
    D -- 유효 --> E[sub 클레임 → userID 컨텍스트]
    D -- 만료/위변조 --> C
    C --> F{공개 경로?}
    E --> F
    F -- 공개 --> G[핸들러 실행]
    F -- 보호 경로 + userID=0 --> H[401 UNAUTHORIZED]
    F -- 보호 경로 + userID>0 --> G

회원 탈퇴

mermaid
sequenceDiagram
    participant App as Flutter 앱
    participant BE as 백엔드

    App->>BE: DELETE /api/v1/auth/withdraw<br/>Authorization: Bearer JWT<br/>X-Confirm-Withdraw: yes
    Note over BE: 헤더 미전송 시 400 반환
    BE->>BE: 트랜잭션 시작
    BE->>BE: DELETE watch_stocks WHERE user_id
    BE->>BE: DELETE push_subscriptions WHERE user_id
    BE->>BE: DELETE analysis_feedback WHERE user_id
    BE->>BE: DELETE users WHERE id
    BE->>BE: COMMIT
    BE-->>App: 204 No Content

룰 / 정책

항목환경변수
JWT 알고리즘HS256
JWT 만료설정값 (기본 7일)JWT_EXPIRATION_MS (밀리초 단위)
JWT 시크릿환경변수JWT_SECRET
카카오 Client ID환경변수KAKAO_CLIENT_ID
카카오 Client Secret환경변수KAKAO_CLIENT_SECRET
Google 허용 Client IDs환경변수 (필수, 쉼표 구분)GOOGLE_ALLOWED_CLIENT_IDS
Apple 허용 Bundle IDs환경변수 (필수, 쉼표 구분)APPLE_ALLOWED_CLIENT_IDS
Apple JWKS 캐시 TTL6시간
Rate Limit (auth 경로)0.5 RPS, burst 5 (30/분)
가입 보너스 기간30일마이그레이션 DEFAULT

공개 경로 (JWT 면제)

ACL 은 endpoint 등록 시점에 Registry helper 로 결정된다 (path-prefix 매처 아님). 카테고리별 helper 와 적용 룰은 backend-structure.md "Route ACL 매트릭스" 참조.

JWT 면제 (helper = Anonymous / AuthIssue / Admin / AdMobSSV / Actuator):

  • Anonymous: GET /api/v1/disclosures/*, GET /api/v1/companies/{corpCode}, GET /api/v1/stocks/search, GET /api/v1/ai/analyze/{rceptNo} (lazy auth, 2026-05-07~)
  • AuthIssue: POST /api/v1/auth/{kakao,google,apple}/token, POST /api/v1/auth/logout
  • Admin: /api/v1/admin/* 전체 (X-Admin-Key 검증)
  • AdMobSSV: GET /api/v1/admob/ssv (ECDSA)
  • Actuator: GET /actuator/health

JWT 필수 (Authenticated): 그 외 모든 endpoint. GET /api/v1/auth/me, DELETE /api/v1/auth/withdraw 도 포함 (/auth/ 경로지만 토큰 보유자만).

lazy auth (2026-05-07~)

GET /api/v1/ai/analyze/{rceptNo} 만 비로그인 허용 (Inspect 메타). sub-path (/feedback, /reveal) 는 Authenticated 로 별도 등록되어 자동 보호 — Go 1.22 mux 가 specific path (/{rceptNo}/feedback) 를 wildcard (/{rceptNo}) 보다 우선 매칭. result 필드는 비로그인 시 항상 null (핸들러 레벨에서 omit). 정책 결정 맥락은 ../policies/lazy-auth-launch.md.

탈퇴 이중 확인

JWT 탈취로 탈퇴가 즉시 실행되는 것을 막기 위해 X-Confirm-Withdraw: yes 헤더를 별도로 요구한다. 헤더 없으면 400 반환.

Edge case

케이스동작
카카오 서버 장애카카오 userinfo 조회 실패 → 401
JWT 만료ValidateAndGetUserID 에러 → userID=0 → 보호 경로 접근 시 401
동일 사용자 재가입FindByOAuth로 기존 row 조회 → 기존 user 그대로 반환 (upsert 아님, 신규 가입 미발생)
탈퇴 후 재가입oauth_id 가 삭제됐으므로 신규 row INSERT → 새 userID 부여
updated_at현재 Create/탈퇴 시 미갱신 (nullable, 명시적 업데이트 없음)

관련 API

엔드포인트설명
POST /api/v1/auth/kakao/token카카오 access_token → JWT 발급
POST /api/v1/auth/google/tokenGoogle ID Token → JWT 발급
POST /api/v1/auth/apple/tokenApple Identity Token + raw_nonce → JWT 발급 (iOS 전용)
GET /api/v1/auth/meJWT → 내부 user_id 반환
POST /api/v1/auth/logoutaccess_token 쿠키 삭제 (네이티브 앱은 Bearer 사용이라 실질적 효과 없음)
DELETE /api/v1/auth/withdraw회원 탈퇴 (X-Confirm-Withdraw: yes 필수)