GPT에게 오늘 작업 내용 요약해달라고 했다. 트러블슈팅 과정에서 이해가 애매한 부분들은 딥다이브 들어갈 예정이다.
어우 드디어 일단 Front - middle (인증, 인증 정보 전송) - backend 플로우 완성한 느낌이다.
내일 다시 middle - backend 인증 정보 확인하고 다음 진행해야겠다.
오늘 작업 요약
1) 전체 아키텍처 & 흐름 (최종)
- 프론트(Vite, 5173) → 스프링 미들웨어(8080) → FastAPI(8000).
- 로그인은 스프링이 전담(Google OAuth2) → 스프링이 HS256 JWT 발급(Access/Refresh) → 1회용 코드(OTC) 로 프론트에 전달 → 프론트가 /api/auth/exchange 로 교환하여 localStorage 저장.
- 프론트는 모든 API 호출에 Authorization: Bearer <access> 자동 첨부.
401 시 /api/auth/refresh 한 번만 시도 → 실패하면 로그아웃/초기화. - 스프링은 리소스 서버로 동작(JWT 검증) + 블랙리스트 필터 선차단 + 프록시에서 FastAPI로 검증된 JWT 그대로 전달.
- FastAPI는 경량 전역 미들웨어(선택)로 HS256 재검증만 수행(Authorization 헤더 유효성만).
2) 스프링 Boot 변경 포인트
보안/인증
- SecurityConfig
- oauth2Login() + CustomOAuth2SuccessHandler 연결.
- oauth2ResourceServer().jwt(...) + CompositeJwtAuthConverter(roles → authorities 매핑).
- JwtBlacklistFilter 를 BearerTokenAuthenticationFilter 앞에 배치(블랙리스트 선차단).
- 401/403 JSON 응답 통일.
- JwtDecoderConfig
- HS256용 NimbusJwtDecoder를 **jwt.secret**으로 구성.
- JwtProvider
- HS256 서명, sub(userId), roles(대문자), authorities(ROLE_*), 만료 포함.
- TokenService
- Redis 사용
- blacklist: SHA-256(access) → TTL=남은 만료시간
- refresh: userId → refresh token
- otc: 1회용 코드(JSON: {access, refresh}) + TTL
- 원자적 GET+DEL(Lua 스크립트)로 안전한 교환.
- Redis 사용
- CustomOAuth2SuccessHandler
- Access/Refresh 생성 → Refresh 저장 → issueOneTimeCode()(기본 60s) →
UriComponentsBuilder의 queryParam() 으로 code(= 포함 가능) 안전히 붙여 리다이렉트. - 리다이렉트 목적지:
- 세션 frontRedirect(화이트리스트 통과 시) > app.oauth2.redirect-uri (기본 http://localhost:5173/oauth/success-popup)
- Access/Refresh 생성 → Refresh 저장 → issueOneTimeCode()(기본 60s) →
- OAuthTokenController
- GET /api/auth/exchange?code=: 한 번만 교환(성공 시 즉시 삭제).
- (구버전 세션 교환 엔드포인트는 Deprecated)
- WebConfig
- CORS에 http://localhost:5173 허용.
- RestTemplate 빈.
프록시
- ProxyController + ProxyService
- /api/proxy/llm 는 서비스로 위임(DTO 변환/헤더 부착 등 비즈니스 전용).
- /api/proxy/** 는 일반 프록시: hop-by-hop 헤더 제거, X-Forwarded-* 추가, 검증된 JWT로 Authorization 덮어쓰기.
- 메서드 바디 여부는 문자열 비교(GET/HEAD/OPTIONS/TRACE 제외)로 안정 처리.
3) FastAPI (선택 적용)
- security/auth_middleware.py – AuthOnlyMiddleware
- 보호 프리픽스(/rag, /search, /admin, /api) 에서만 HS256 재검증(만료 포함).
- JWT_SECRET 환경변수 사용(미설정 시 dev-secret).
- 통과 시 request.state.claims/roles 세팅.
- 라우터는 Authorization 의존만 하고, 별도 인증로직 없음.
4) 프론트 변경 포인트
라우팅
- /login(공개), /oauth/success-popup(공개), /(보호)
- RequireAuth: /api/users/me로 유저 조회 성공하면 통과, 아니면 /login.
OAuth 팝업 흐름
- 로그인 버튼 → window.open(VITE_OAUTH_URL + '/oauth2/authorization/google?state=popup', ...)
- 팝업 페이지 /oauth/success-popup:
- 쿼리 code 획득 → fetch(axios 아님)로 /api/auth/exchange 호출 →
성공 시 window.opener.postMessage({ type:'oauth-success', accessToken, refreshToken }) → 닫기. - code 없음 등 실패 시 oauth-fail.
- 쿼리 code 획득 → fetch(axios 아님)로 /api/auth/exchange 호출 →
axios 인터셉터 (libs/axios.ts)
- 요청: /api/auth/(exchange|refresh|logout) 제외하고 access 자동 첨부.
- 응답: 401이면 동시성 큐로 /api/auth/refresh 1회만 시도.
- 블랙리스트 메시지(“로그아웃된 토큰”)면 즉시 초기화/로그인 이동.
- 실패 브로드캐스트로 대기 요청 일괄 실패/리다이렉트.
- 타입스크립트 경고(S6671 등) 해결: toError() 래퍼 등.
AuthContext / MainLayout
- 마운트 시 /api/users/me 호출 → user 상태 세팅.
- logout()은 토큰 삭제 후 /api/auth/logout 호출(존재 시) → 초기화.
5) 설정/환경변수
application.yml
- spring.security.oauth2.client.registration.google (client-id/secret/redirect-uri)
- jwt.secret(32바이트↑), jwt.expiration, jwt.refresh-expiration
- app.oauth2.redirect-uri: http://localhost:5173/oauth/success-popup
- app.oauth2.allowed-origins: ["http://localhost:5173"]
- proxy.prefix: /api/proxy, proxy.upstream: http://fastapi:8000
- DB/Redis 설정
Front
.env
VITE_API_URL=http://localhost:8080 VITE_OAUTH_URL=http://localhost:8080
Google OAuth 콘솔
- 승인된 리디렉션 URI: http://localhost:8080/login/oauth2/code/google
6) 테스트 체크리스트
- 로그인
- /login → 팝업 열림 → 구글 로그인 → 팝업 닫힘 → localStorage에 accessToken/refreshToken.
- 네트워크: GET /api/auth/exchange?code=... 200.
- 인증 확인
- GET /api/users/me 200, 화면에 사용자명 표시.
- 리프레시
- 임의로 access 만료(짧게 발급 or 서버에서 강제 401) → 자동 /api/auth/refresh → 원요청 재시도 200.
- 블랙리스트/로그아웃
- POST /api/auth/logout → 이후 API 호출 401 + {"error":"로그아웃된 토큰"}.
- 프록시 → FastAPI
- GET/POST /api/proxy/... 호출 시 FastAPI에서 Authorization: Bearer <jwt> 수신 확인.
- CORS
- 브라우저 콘솔에 CORS 에러 없어야 함.
7) 트러블슈팅 메모
- 무한 /login 리다이렉트:
/api/users/me가 401 → 원인: 잘못된 jwt.secret, JwtDecoderConfig 누락, axios가 토큰을 auth 엔드포인트에만 붙이지 못하는 경우, VITE_API_URL 오타. - 팝업 “code 없음”:
app.oauth2.redirect-uri 가 /oauth/success-popup와 불일치, 또는 success handler가 다른 곳으로 리다이렉트.
팝업 URL과 오리진 화이트리스트 확인. - Invalid character '=' for QUERY_PARAM:
?code= 값을 문자열로 이어붙이면 생기는 문제 → UriComponentsBuilder.queryParam("code", code) 사용. - 블랙리스트가 안 먹음:
JwtBlacklistFilter 위치 확인(반드시 BearerTokenAuthenticationFilter 앞).
Redis key는 access 토큰의 SHA-256 해시로 저장되는 점 확인. - FastAPI 401:
JWT_SECRET 불일치 / 전역 미들웨어에서 보호 prefix가 아닌 경로.
'개인 프로젝트' 카테고리의 다른 글
| LLM + RAG 프로젝트 [6] End to End MVP 완성 (4) | 2025.09.01 |
|---|---|
| LLM + RAG 프로젝트 [5] 로컬 LLM 사용하면서 깨달은점 (5) | 2025.08.22 |
| LLM + RAG 프로젝트 [4] 데이터 수집 (0) | 2025.08.08 |
| LLM + RAG 프로젝트 [3] Reverse Proxy 구성 (7) | 2025.07.29 |
| LLM + RAG 프로젝트 [2] 아키텍쳐 설계 (0) | 2025.06.23 |