하루들 인증·인가 코드 협력 지도.excalidraw
⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠
Excalidraw Data
Text Elements
Harudle 인증·인가 코드 협력 지도 ^45lnMuHX
develop-backend @ d8d52bf · 현재 구현 기준 · Spring Security가 제공하는 객체와 하루들 객체를 분리해서 읽기 ^IJrclkDG
- 먼저 보는 핵심: 요청마다 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
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