본문 바로가기
개인 프로젝트

LLM + RAG 프로젝트 [3.1] 프론트 - 미들웨어 인증 로직 수정

by 포잉띠 2025. 8. 19.

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 스크립트)로 안전한 교환.
  • CustomOAuth2SuccessHandler
    • Access/Refresh 생성 → Refresh 저장 → issueOneTimeCode()(기본 60s) →
      UriComponentsBuilder의 queryParam() 으로 code(= 포함 가능) 안전히 붙여 리다이렉트.
    • 리다이렉트 목적지:
      • 세션 frontRedirect(화이트리스트 통과 시) > app.oauth2.redirect-uri (기본 http://localhost:5173/oauth/success-popup)
  • 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.

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) 테스트 체크리스트

  1. 로그인
    • /login → 팝업 열림 → 구글 로그인 → 팝업 닫힘 → localStorage에 accessToken/refreshToken.
    • 네트워크: GET /api/auth/exchange?code=... 200.
  2. 인증 확인
    • GET /api/users/me 200, 화면에 사용자명 표시.
  3. 리프레시
    • 임의로 access 만료(짧게 발급 or 서버에서 강제 401) → 자동 /api/auth/refresh → 원요청 재시도 200.
  4. 블랙리스트/로그아웃
    • POST /api/auth/logout → 이후 API 호출 401 + {"error":"로그아웃된 토큰"}.
  5. 프록시 → FastAPI
    • GET/POST /api/proxy/... 호출 시 FastAPI에서 Authorization: Bearer <jwt> 수신 확인.
  6. 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가 아닌 경로.