⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠

Excalidraw Data

Text Elements

Harudle 인증·인가 코드 협력 지도 ^45lnMuHX

develop-backend @ d8d52bf · 현재 구현 기준 · Spring Security가 제공하는 객체와 하루들 객체를 분리해서 읽기 ^IJrclkDG

  1. 먼저 보는 핵심: 요청마다 SecurityFilterChain 하나만 선택된다 ^Pt0agOeF

SecurityConfig가 체인을 만들고, FilterChainProxy가 요청 경로에 맞는 첫 번째 체인을 실행한다. ^Ufuw1Txa

인증(Authentication) “누가 보냈는가?”를 Authentication 객체로 만드는 과정 ^F3sBfkyh

인가(Authorization) 그 Authentication이 이 URL/기능을 실행해도 되는지 결정 ^HWB4w5cK

User Spring Security principal이 아니라 DB에 저장되는 하루들 도메인 엔티티 ^PZSlG1cE

Browser 요청 URL + Cookie/Header ^KDgZkpiw

FilterChainProxy 첫 매칭 체인 선택 ^kZuly3Yz

요청 경로? ^6DkY901r

oauthSecurityFilterChain @Order(1) /oauth2/, /login/oauth2/ IF_REQUIRED Session · OAuth2 Client · permitAll ^1z4nnaKv

apiSecurityFilterChain @Order(2) 그 밖의 요청 · /api/v1/** STATELESS · CSRF · JWT Resource Server · URL 인가 ^9Qm73qcE

SecurityConfig @Bean으로 두 체인 조립 ^gY2H4t9S

request ^YcudYOE3

matches ^QYCw3ytk

OAuth 경로 ^FiclBxun

그 외 ^fF2qgI53

configures ^2GuAaqNV

※ 두 체인이 차례대로 모두 실행되는 구조가 아니다. OAuth 시작/콜백과 보호 API는 서로 다른 필터 체인에서 처리된다. ^yImqv84D

A. OAuth 로그인: Kakao 신원 → 하루들 User 연결 ^I34Zznph

Spring의 OAuth2AuthenticationToken이 만들어진 뒤에야 하루들 SuccessHandler와 서비스가 동작한다. ^ZWkiH1cr

Browser / React ^P2XsXp5Q

Spring Security 제공 객체 ^tjv9VLNJ

Harudle 구현 객체 ^xEEgIeqj

Kakao / DB ^DSZDmdjU

LoginPage location.assign(…) ^WJ5sQRM0

OAuth2AuthorizationRequest RedirectFilter ^tsrgVJY9

1 GET /oauth2/authorization/kakao ^sjsAbBlP

HttpSessionOAuth2Authorization RequestRepository ^11tzMYXk

Kakao authorize 로그인 · 동의 ^RnveY8vK

2 state + 요청 저장 ^bGjb731M

3 302 + JSESSIONID → Kakao ^3m3F6F27

Browser callback code + state Cookie: JSESSIONID ^ShYcuqEs

OAuth2Login AuthenticationFilter ^KJ5sEoRi

4 302 /login/oauth2/code/kakao?code&state ^KQlAuOzY

5 callback 요청 ^RpgttiuB

AuthenticationManager → OAuth2LoginAuthenticationProvider ^pU1C0FzS

Kakao token endpoint

  • /v2/user/me Access Token · profile ^tUuKEcr9

6 authenticate ^BDzIERUx

7 code 교환 + 사용자 조회 ^8vFXVDzB

OAuth2AuthenticationToken implements Authentication ^V87KYHMQ

OAuthLoginSuccessHandler .onAuthenticationSuccess(request, response, authentication) ^d3uDMCcU

8 인증 성공 객체 ^4igLr4dL

9 successHandler ^JelTf2G8

이 Authentication은 “Kakao 사용자 조회까지 성공”을 뜻한다. 하루들 Access JWT는 아직 없다. ^yTTb8Jme

KakaoLoginCommandMapper .map(attributes) ^bXSt34Qq

OAuthLoginService .login(command, now) ^SzhwmQVt

OAuthAccountRepository find(provider, subject) UserRepository.save OAuthAccountRepository.save ^qdYcjruT

10 attributes ^cJsimWUH

OAuthLoginCommand ^3lkZwNRU

11 기존/신규 계정 연결 ^8pbNtcVo

RefreshTokenService .issue(user, now) raw 생성 → hash 저장 ^62RQiUDv

RefreshTokenCookieWriter

  • CookieCsrfTokenRepository ^bqCFjtSO

refresh_tokens User 1 ─ N RefreshToken DB에는 hash만 저장 ^Te7SCrz8

Browser가 받음 refresh_token: HttpOnly XSRF-TOKEN: JS readable 302 /auth/callback ^s5rr1VOv

12 User 조회 후 ^bOXgmC3y

IssuedRefreshToken ^bOaqD3lm

hash save ^13r1i2TP

13 Set-Cookie + redirect ^DzzZlNGO

B. Access Token 발급: Refresh Cookie → 하루들 JWT ^pJ3nbFGF

Auth 엔드포인트는 permitAll이지만, /refresh와 /logout의 CSRF 검증은 AuthorizationFilter보다 먼저 수행된다. ^9JqzYPKx

Browser / React ^yaONJ92W

Spring API Filter ^mTqpVreq

Harudle 구현 객체 ^ynDfY8mD

DB / Crypto ^l3hkOs4P

AuthCallbackPage 또는 authFetch.restoreAccessToken() ^5FtY7m3K

AuthController.csrf() CookieCsrfTokenRepository.generateToken + saveToken ^NoJgWbh6

1 GET /api/v1/auth/csrf ^YfbtVdwK

XSRF-TOKEN Cookie

  • 응답 body token ^CbW8DPcv

2 브라우저가 token 확보 ^J7uCR36C

POST /auth/refresh Cookie: refresh_token Header: X-XSRF-TOKEN credentials: include ^pg3Zwobo

CsrfFilter Cookie token == Header token 불일치면 403 ^DfJoMeNS

AuthController.refresh(request, response) RefreshTokenCookieReader.read(request) ^fYKw9O4L

3 ^eou92Ja0

4 CSRF 통과 ^p5r5Vf5y

permitAll = Access JWT 인증을 요구하지 않음 CSRF 검사를 끈다는 뜻은 아님 ^s9MO45Ib

AuthService.refresh 트랜잭션 오케스트레이션 ^TBgvegcr

RefreshTokenService.rotate old revoke + new issue ^u5FhKnGk

5 raw token ^T9aOkHfW

6 ^0ToOaCsW

RefreshTokenHasher.hash(raw) RefreshTokenRepository .findByTokenHash PESSIMISTIC_WRITE ^OZZDVqxo

7 hash + lock ^H0FA36Ds

RefreshToken domain .canUseAt(now) .revoke(now) ^2KD5oeCx

새 raw token 생성 새 hash만 refresh_tokens에 save User 삭제 여부도 확인 ^26r0nijE

8 entity ^iQ5UEBdy

9 새 Refresh Token ^MTMWywfT

AccessTokenService.issue(userId) claims 생성 ^XKdUdrbF

JwtEncoder.encode IssuedAccessToken ^NlW3XkyH

하루들 Access JWT HS256 · sub=User UUID iss · aud · iat · exp role claim 없음 ^WIferZRd

10 userId ^fmttyFNZ

11 ^djRwW2dN

12 encoded ^pyoFgUjO

AuthController → 새 Refresh Cookie + body: accessToken ^jsm92p0j

setAccessToken(jwt) 브라우저 모듈 메모리 ^qnHRIeCU

13 ^dK21LsIi

14 200 + Set-Cookie ^jqgLeBvv

C. 보호 API: Bearer JWT → Authentication → 인가 → Controller/Service ^6QF7H9Dz

핵심은 JWT 문자열이 곧 User가 아니라는 점이다. 검증된 JWT로 매 요청마다 JwtAuthenticationToken을 만든다. ^fuh7QiRr

authFetch / fetchWithAccessToken Authorization: Bearer Access Token은 브라우저 메모리 ^6pGyaxUu

BearerTokenAuthenticationFilter Header에서 token 문자열 추출 ^Fq9aLB5b

BearerTokenAuthenticationToken 아직 미인증 입력 객체 ^ytkD3RZG

AuthenticationManager → JwtAuthenticationProvider ^0SgQtT4J

JwtDecoder HS256 서명 · iss · aud · exp 검증 ^TaqWZXc4

JwtAuthenticationToken principal = Jwt name = jwt.sub = User UUID ^MDWfml1Z

1 ^Pksjt2uX

2 ^EEXomRwj

3 ^n2GfFpnb

4 ^ha1aXy2Y

5 success ^xZHifIce

검증 실패 또는 token 없음 → ApiAuthenticationEntryPoint → 401 Unauthorized ^jYNBGBTy

fail ^6V875czX

SecurityContextHolder 현재 요청의 Authentication 보관 ^TQL2Vn7F

AuthorizationFilter URL 규칙에 따라 인가 ^IjvonlZf

6 SecurityContext 저장 ^x6w7WHWW

7 ^MFX5mat6

/api/v1/** → authenticated() Authentication.isAuthenticated == true면 허용 ^VlJoCexw

AdminAuthorizationManager Authentication.getName() → UUID ^lz7V4lXG

UserRepository.findById(UUID) !user.isDeleted() && user.isAdmin() 현재 DB role을 매 요청 확인 ^8fd3HfbM

일반 API ^Z6i8S3nB

/api/v1/admin/** ^OuQ2vaYX

8 DB 권한 조회 ^i1gVVq9N

Authentication은 유효하지만 URL 권한이 거절됨 → ApiAccessDeniedHandler → 403 Forbidden ^uRAHOsn6

denied ^peSJVt4E

인가 성공 후 Controller가 받는 두 가지 방식 ^Djs0QkHF

CurrentUserController.find @AuthenticationPrincipal Jwt → jwt.getSubject() → UUID ^PYityDCX

DiaryController 등 Authentication 매개변수 → AuthenticatedUserIdResolver ^hexaqqpe

9 granted ^1xUiy1wG

Authentication 주입 ^jNCuz8IR

principal Jwt 주입 ^rwnBpfjQ

CurrentUserService.find(userId) UserRepository.findById OAuthAccountRepository.findByUser_Id ^KNATC5zs

AuthenticatedUserIdResolver.resolve AuthenticationTrustResolver getName()을 canonical UUID로 변환 ^YTrPpyT4

10 ^pUJytwwr

10 ^rdYJGxK7

여기서부터 비즈니스 협력 Controller → Application Service → Repository → User/Diary 등 Domain Entity SecurityContext의 Authentication은 userId를 전달하는 입구 역할만 한다. ^kw08FRce

11 User + OAuthAccount ^qD9uWsM4

11 UUID ^bHC8Ykxy

중요한 경계 • JSESSIONID: OAuth 시작↔callback의 state/인가요청 연결용. 보호 API 인증 수단이 아니다. • apiSecurityFilterChain은 STATELESS: 보호 API마다 Bearer JWT를 다시 검증한다. • Access JWT에는 role이 없다: 관리자 여부는 AdminAuthorizationManager가 DB User.role로 판단한다. ^OjIz6usw

401과 403을 객체 관점으로 구분

401 Unauthorized

  • Bearer token이 없거나 JwtDecoder 검증 실패
  • 신뢰할 Authentication을 만들지 못함
  • ApiAuthenticationEntryPoint

403 Forbidden

  • Authentication은 만들어졌지만 URL 인가가 거절됨
  • CSRF 검증 실패도 이 응답이 될 수 있음
  • ApiAccessDeniedHandler 또는 CsrfFilter ^rnVFJae4

D. 객체 관계를 한 장으로 고정하기 ^40niom6j

같은 “사용자”라는 말을 쓰지만 수명·저장 위치·책임이 완전히 다르다. ^vbzTePcF

Authentication (Spring Security interface) principal · authorities · authenticated · getName() 현재 요청에서 “누구로 인증되었는가”를 표현하며 DB row가 아니다. ^Zh22tWAd

OAuth2AuthenticationToken 수명: OAuth callback 처리 시점 principal: OAuth2User(attributes) SuccessHandler의 입력 ^fzrplLQQ

JwtAuthenticationToken 수명: 보호 API 요청 1회 principal: 검증된 Jwt getName(): JWT sub(User UUID) ^yfpyCCy6

구현체 A ^FORTBnHc

구현체 B ^MWqPK79l

SecurityContextHolder 현재 요청의 Authentication을 Controller까지 전달 · 요청 종료 후 비움 ^ugELwpGO

저장 ^sstzGbrj

User @Entity (users) id: UUID role: USER | ADMIN deletedAt · name · email 하루들 내부 사용자/정책의 기준 ^v68loP6A

OAuthAccount @Entity provider + providerSubject ManyToOne → User “카카오 계정 ↔ 내부 User” 연결 ^C2Sz2JXs

RefreshToken @Entity tokenHash · expiresAt · revokedAt ManyToOne → User raw 값은 DB에 저장하지 않음 ^Fr6zpiu9

N : 1 ^Nd1UKmNo

N : 1 ^11BxpSpj

Access JWT 저장: Browser JavaScript 메모리 DB 저장 안 함 sub로 User.id를 가리킴 · role 없음 ^XGxwCYup

Refresh Cookie 저장: Browser HttpOnly Cookie DB: raw가 아닌 hash row Access JWT 재발급/회전에만 사용 ^Uf8uPRNr

sub = User.id ^N5JXiXha

hash row ^pUCW6EDj

실제 코드 출발점 SecurityConfig · OAuthLoginSuccessHandler · OAuthLoginService · AuthController · AuthService RefreshTokenService · AccessTokenService · AdminAuthorizationManager · CurrentUserController frontend: LoginPage.tsx · AuthCallbackPage.tsx · shared/auth.ts

읽는 순서: 0 → A → B → C → D ^yzUcSIaK

Built with LogoFlowershow