도메인: Auth (인증/회원)
개요
카카오 / Google / Apple OAuth로 소셜 로그인을 처리하고, HS256 JWT로 세션을 유지한다. 개인정보 최소 수집 원칙에 따라 소셜 로그인 고유 식별자(oauth_provider + oauth_id)만 저장한다.
데이터 모델 (핵심 테이블)
dartbrief.users
| 컬럼 | 타입 | 설명 |
|---|---|---|
id | BIGSERIAL PK | 내부 회원번호 |
oauth_provider | VARCHAR(10) | KAKAO / GOOGLE / APPLE |
oauth_id | VARCHAR(255) | 소셜 로그인 고유 식별자 (Google/Apple: JWT sub claim, Kakao: /v2/user/me 응답의 숫자 id) |
created_at | TIMESTAMP(6) | 가입 시각 |
updated_at | TIMESTAMP(6) | nullable |
slot_capacity | INT | 관심종목 슬롯 한도 (기본 3, CHECK >= 3) |
UNIQUE: (oauth_provider, oauth_id)
동작 플로우
Google 로그인
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 전용)
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>" }카카오 로그인
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 검증 흐름 (미들웨어)
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회원 탈퇴
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 캐시 TTL | 6시간 | — |
| 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/logoutAdmin:/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/token | Google ID Token → JWT 발급 |
POST /api/v1/auth/apple/token | Apple Identity Token + raw_nonce → JWT 발급 (iOS 전용) |
GET /api/v1/auth/me | JWT → 내부 user_id 반환 |
POST /api/v1/auth/logout | access_token 쿠키 삭제 (네이티브 앱은 Bearer 사용이라 실질적 효과 없음) |
DELETE /api/v1/auth/withdraw | 회원 탈퇴 (X-Confirm-Withdraw: yes 필수) |