feat: 가상화 문서들 추가

This commit is contained in:
DongHyeonka
2026-09-10 08:54:05 +09:00
parent e9f6a93327
commit 43e1aadef0
695 changed files with 153404 additions and 12754 deletions
@@ -0,0 +1,616 @@
- generic [ref=f9e3]:
- link "본문으로 건너뛰기" [ref=f9e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f9e5]:
- generic [ref=f9e6]:
- link "TechLog Studio" [ref=f9e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f9e8]: Studio
- navigation "Studio 주 탐색" [ref=f9e10]:
- link "작업본" [ref=f9e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f9e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f9e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f9e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f9e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f9e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f9e17]
- main [ref=f9e18]:
- generic [ref=f9e19]:
- generic [ref=f9e20]:
- region [ref=f9e21]:
- generic [ref=f9e22]:
- paragraph [ref=f9e23]: CONCEPT · VERSION 5
- heading "문서 편집" [level=1] [ref=f9e24]
- paragraph [ref=f9e25]: Cookie로 인증하는 요청에서 CSRF token이 하는 일
- region [ref=f9e26]:
- generic [ref=f9e27]:
- paragraph [ref=f9e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f9e29]
- generic [ref=f9e30]:
- generic [ref=f9e31]:
- generic [ref=f9e32]: 제목
- textbox "제목" [ref=f9e33]: Cookie로 인증하는 요청에서 CSRF token이 하는 일
- generic [ref=f9e34]:
- generic [ref=f9e35]: slug
- textbox "slug" [ref=f9e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: cookie-auth-csrf
- generic [ref=f9e37]:
- generic [ref=f9e38]: 요약
- textbox "요약" [ref=f9e39]: session cookie는 브라우저가 요청마다 자동으로 붙인다. 그래서 상태를 바꾸는 요청이 사용자의 의도인지 서버가 따로 확인해야 한다. CSRF token이 그 확인이고, SameSite는 브라우저가 cookie를 언제 보낼지 정하는 별도의 정책이다.
- generic [aria-hidden] [ref=f9e40]: 목록 카드에는 약 90자까지 보입니다 · 143 / 2000
- generic [ref=f9e41]:
- generic [ref=f9e42]: Topic
- combobox "Topic" [ref=f9e43]:
- option "선택하지 않음"
- option "JPA 피드 조회 성능"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f9e44]:
- generic [ref=f9e45]: Project
- combobox "Project" [ref=f9e46]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- status [ref=f9e47]
- group "축 — 고르지 않으면 이 주제의 공통 기록이 됩니다" [ref=f9e48]:
- generic [ref=f9e50] [cursor=pointer]:
- checkbox "SPA" [ref=f9e51]
- generic [ref=f9e52]: SPA
- generic [ref=f9e53] [cursor=pointer]:
- checkbox "Mediator" [ref=f9e54]
- generic [ref=f9e55]: Mediator
- generic [ref=f9e56] [cursor=pointer]:
- checkbox "BFF" [ref=f9e57]
- generic [ref=f9e58]: BFF
- generic [ref=f9e59] [cursor=pointer]:
- checkbox "Forward-Auth" [ref=f9e60]
- generic [ref=f9e61]: Forward-Auth
- group "관계" [ref=f9e62]:
- generic [ref=f9e64]:
- generic [ref=f9e65]:
- generic [ref=f9e66]: 관계 1 대상
- combobox "관계 1 대상" [ref=f9e67]:
- option "대상 선택"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제"
- option "Collection Fetch Join Pagination의 In-memory Paging"
- option "Fetch 타입이 아닌 조회 방식으로 인한 N+1"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Projection 이후에도 1,509행을 읽은 Row Over-fetch"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우"
- option "Visibility OR이 Keyset Index를 깨뜨린 문제"
- option "Authorization Code와 PKCE가 보호하는 구간"
- option "Bearer JWT가 인증된 principal이 되기까지"
- option "Cookie로 인증하는 요청에서 CSRF token이 하는 일"
- option "브라우저가 credential을 보관하는 위치와 그 성질"
- option "Forward-Auth와 Nginx auth_request의 동작"
- option "외부 IdP Brokering의 동작"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [selected]
- option "Feed Visibility Query Pattern"
- option "Fetch Join · Batch · Projection 선택 기준"
- option "Fetch Type과 Fetch Strategy 구분"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP 연동과 Application 인증 구조의 경계"
- option "JPA N+1 정량 진단 기준"
- option "Keyset Pagination 설계 기준"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [disabled]
- option "PostgreSQL Query Plan 측정 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Top-N-per-group 선택 기준"
- option "실제 동시 트래픽에서도 이 구조가 안정적인가"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "feed_visible을 Production CQRS로 승격할 것인가"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Highlight 없는 FeedItem을 허용할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "Round Trip과 Row Volume을 독립 측정할 것인가"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다"
- option "Entity Graph 조회에는 Batch Fetch를 사용한다"
- option "Feed Pagination은 Keyset을 사용한다"
- option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다."
- option "Query Plan은 실제 PostgreSQL에서 측정한다"
- option "Query Strategy는 FeedQueryPort 뒤에서 소유한다"
- option "현재 Read Model은 CQRS-lite로 유지한다"
- option "화면 조회는 Read Projection을 사용한다"
- generic [ref=f9e68]:
- generic [ref=f9e69]: 관계 1 이유
- textbox "관계 1 이유" [ref=f9e70]: 이 확인이 필요한 구조의 설계 항목이다.
- generic [aria-hidden] [ref=f9e71]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다.
- generic [ref=f9e72]:
- button "위로" [disabled] [ref=f9e73]
- button "아래로" [ref=f9e74]
- button "삭제" [ref=f9e75]
- generic [ref=f9e76]:
- generic [ref=f9e77]:
- generic [ref=f9e78]: 관계 2 대상
- combobox "관계 2 대상" [ref=f9e79]:
- option "대상 선택"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected]
- option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제"
- option "Collection Fetch Join Pagination의 In-memory Paging"
- option "Fetch 타입이 아닌 조회 방식으로 인한 N+1"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Projection 이후에도 1,509행을 읽은 Row Over-fetch"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우"
- option "Visibility OR이 Keyset Index를 깨뜨린 문제"
- option "Authorization Code와 PKCE가 보호하는 구간"
- option "Bearer JWT가 인증된 principal이 되기까지"
- option "Cookie로 인증하는 요청에서 CSRF token이 하는 일"
- option "브라우저가 credential을 보관하는 위치와 그 성질"
- option "Forward-Auth와 Nginx auth_request의 동작"
- option "외부 IdP Brokering의 동작"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "Feed Visibility Query Pattern"
- option "Fetch Join · Batch · Projection 선택 기준"
- option "Fetch Type과 Fetch Strategy 구분"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP 연동과 Application 인증 구조의 경계"
- option "JPA N+1 정량 진단 기준"
- option "Keyset Pagination 설계 기준"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [disabled]
- option "PostgreSQL Query Plan 측정 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Top-N-per-group 선택 기준"
- option "실제 동시 트래픽에서도 이 구조가 안정적인가"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "feed_visible을 Production CQRS로 승격할 것인가"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Highlight 없는 FeedItem을 허용할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "Round Trip과 Row Volume을 독립 측정할 것인가"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다"
- option "Entity Graph 조회에는 Batch Fetch를 사용한다"
- option "Feed Pagination은 Keyset을 사용한다"
- option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다."
- option "Query Plan은 실제 PostgreSQL에서 측정한다"
- option "Query Strategy는 FeedQueryPort 뒤에서 소유한다"
- option "현재 Read Model은 CQRS-lite로 유지한다"
- option "화면 조회는 Read Projection을 사용한다"
- generic [ref=f9e80]:
- generic [ref=f9e81]: 관계 2 이유
- textbox "관계 2 이유" [ref=f9e82]: 이 동작을 실제로 재현한 기록이다.
- generic [aria-hidden] [ref=f9e83]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다.
- generic [ref=f9e84]:
- button "위로" [ref=f9e85]
- button "아래로" [ref=f9e86]
- button "삭제" [ref=f9e87]
- generic [ref=f9e88]:
- generic [ref=f9e89]:
- generic [ref=f9e90]: 관계 3 대상
- combobox "관계 3 대상" [ref=f9e91]:
- option "대상 선택"
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled]
- option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제"
- option "Collection Fetch Join Pagination의 In-memory Paging"
- option "Fetch 타입이 아닌 조회 방식으로 인한 N+1"
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
- option "Projection 이후에도 1,509행을 읽은 Row Over-fetch"
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출"
- option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우"
- option "Visibility OR이 Keyset Index를 깨뜨린 문제"
- option "Authorization Code와 PKCE가 보호하는 구간"
- option "Bearer JWT가 인증된 principal이 되기까지"
- option "Cookie로 인증하는 요청에서 CSRF token이 하는 일"
- option "브라우저가 credential을 보관하는 위치와 그 성질"
- option "Forward-Auth와 Nginx auth_request의 동작"
- option "외부 IdP Brokering의 동작"
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "Feed Visibility Query Pattern"
- option "Fetch Join · Batch · Projection 선택 기준"
- option "Fetch Type과 Fetch Strategy 구분"
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
- option "외부 IdP 연동과 Application 인증 구조의 경계"
- option "JPA N+1 정량 진단 기준"
- option "Keyset Pagination 설계 기준"
- option "OAuth/OIDC 인증 패턴 선택 기준"
- option "OAuth Token과 Application Session을 구분하는 기준" [selected]
- option "PostgreSQL Query Plan 측정 기준"
- option "Public Client와 Confidential Client 구분 기준"
- option "Top-N-per-group 선택 기준"
- option "실제 동시 트래픽에서도 이 구조가 안정적인가"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- option "feed_visible을 Production CQRS로 승격할 것인가"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "Highlight 없는 FeedItem을 허용할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- option "Round Trip과 Row Volume을 독립 측정할 것인가"
- option "BFF가 OAuth Token을 관리하는 조건"
- option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다"
- option "Entity Graph 조회에는 Batch Fetch를 사용한다"
- option "Feed Pagination은 Keyset을 사용한다"
- option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다."
- option "Query Plan은 실제 PostgreSQL에서 측정한다"
- option "Query Strategy는 FeedQueryPort 뒤에서 소유한다"
- option "현재 Read Model은 CQRS-lite로 유지한다"
- option "화면 조회는 Read Projection을 사용한다"
- generic [ref=f9e92]:
- generic [ref=f9e93]: 관계 3 이유
- textbox "관계 3 이유" [ref=f9e94]: session cookie와 CSRF token은 서로 다른 값이다.
- generic [aria-hidden] [ref=f9e95]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다.
- generic [ref=f9e96]:
- button "위로" [ref=f9e97]
- button "아래로" [disabled] [ref=f9e98]
- button "삭제" [ref=f9e99]
- button "관계 추가" [ref=f9e100]
- region [ref=f9e101]:
- generic [ref=f9e102]:
- paragraph [ref=f9e103]: CONCEPT
- heading "개념" [level=2] [ref=f9e104]
- generic [ref=f9e105]:
- generic [ref=f9e106]:
- generic [ref=f9e107]: 기준 버전
- textbox "기준 버전 “Kubernetes 1.31” 처럼 무엇을 보고 썼는지. 비워도 됩니다." [ref=f9e108]: Spring Security 6 CSRF · AP3 BFF 구성
- generic [ref=f9e109]: “Kubernetes 1.31” 처럼 무엇을 보고 썼는지. 비워도 됩니다.
- generic [ref=f9e110]:
- generic [ref=f9e111]: 본문 Markdown
- group "Markdown 삽입" [ref=f9e112]:
- button "코드" [ref=f9e113] [cursor=pointer]
- button "표" [ref=f9e114] [cursor=pointer]
- button "목록" [ref=f9e115] [cursor=pointer]
- textbox "본문 Markdown" [ref=f9e116]: "## cookie가 credential이 되면 생기는 일 브라우저가 OAuth token을 받지 않는 구조에서도 인증 상태는 남는다. BFF는 HttpOnly session cookie로 로그인 상태를 찾는다. 이 cookie는 브라우저가 자동으로 붙인다. 다른 사이트가 만든 요청에도 붙을 수 있다는 뜻이다. `GET /bff/api/me`만 보면 이 문제가 드러나지 않으므로 상태를 바꾸는 요청을 따로 봐야 한다. ## token을 받아 오는 요청 브라우저가 먼저 CSRF material을 요청한다. ```http label=\"CSRF token 요청\" GET http://localhost:8083/bff/csrf Accept: application/json Cookie: AP3_SESSION=<opaque-session-id> ``` `CookieCsrfTokenRepository.withHttpOnlyFalse()`는 JavaScript가 읽을 수 있는 `XSRF-TOKEN` cookie를 path `/`에 만든다. controller는 다음 JSON을 반환한다. ```json label=\"CsrfController가 반환하는 JSON\" { \"headerName\": \"X-XSRF-TOKEN\", \"parameterName\": \"_csrf\", \"token\": \"<xor-masked-csrf-token>\" } ``` :::evidence key=\"ap3-csrf-boundary-971df81c\" alt=\"BFF CSRF endpoint가 raw XSRF cookie와 masked JSON token으로 분기하고, SPA가 raw cookie만 실제 POST header 값으로 사용해 Spring CSRF filter에 제출하는 데이터 흐름.\" caption=\" \" zoom=\"true\" ::: ## body의 token과 cookie의 값은 다르다 여기가 이 구조에서 가장 헷갈리는 지점이다. | 위치 | 값 | |---|---| | 응답 body의 `token` | XOR와 Base64로 mask된 값 | | `XSRF-TOKEN` cookie | raw 값 | | POST의 `X-XSRF-TOKEN` 헤더 | cookie와 같은 raw 값 | `XorCsrfTokenRequestAttributeHandler`가 request attribute용 token을 mask하기 때문에 controller JSON에는 masked 값이 보인다. SPA는 JSON에서 `headerName`만 읽고, 실제 값은 `document.cookie`에서 raw `XSRF-TOKEN`을 찾아 쓴다. `SpaCsrfTokenRequestHandler`가 이 조합을 맞춘다. expected 헤더가 있으면 plain resolver로 제출된 raw token을 읽고, 없으면 XOR resolver 경로를 쓴다. 응답 JSON의 `token`을 그대로 헤더에 복사하면 값이 맞지 않아 403이 된다. 노출 값과 제출 값이 다를 수 있다는 것을 클라이언트 코드가 알아야 한다. ## 검증이 controller보다 먼저 일어난다 정상 상태 변경 요청은 다음과 같다. ```http label=\"CSRF 검증을 통과하는 POST\" POST http://localhost:8083/bff/api/preferences Content-Type: application/x-www-form-urlencoded Cookie: AP3_SESSION=<opaque-session-id>; XSRF-TOKEN=<raw-csrf-token> X-XSRF-TOKEN: <same-raw-csrf-token> theme=dark ``` Spring CSRF filter가 repository의 expected token과 제출된 헤더를 비교한다. 헤더가 없거나 값이 맞지 않으면 controller는 실행되지 않고 403이 된다. 검증 지점이 controller 앞이라 endpoint를 추가해도 같은 filter를 지난다. ## SameSite와 CSRF token은 역할이 다르다 | | SameSite | CSRF token | |---|---|---| | 누가 판단하나 | 브라우저 | 서버 | | 무엇을 정하나 | cookie를 보낼지 | 요청을 받아들일지 | | 언제 작동하나 | 요청을 만들 때 | 요청을 처리할 때 | 두 방어선은 서로를 대신하지 못한다. port가 달라도 site 계산상 같은 경우가 있어서, SameSite가 막지 않는 요청에도 CSRF 검증이 필요하다. 네 가지 입력을 나란히 두면 어느 방어선이 작동하는지 갈린다. | 입력 | cookie 동작 | CSRF 동작 | 결과 | |---|---|---|---| | same-origin, CSRF 헤더 없음 | session cookie 붙음 | token 부재로 거부 | 403 | | same-origin, raw cookie와 헤더 일치 | session cookie 붙음 | token 일치 | 200 | | 다른 port지만 same-site, 헤더 없음 | cookie가 붙을 수 있음 | token 부재로 거부 | 403 | | cross-site POST | SameSite=Lax로 cookie 제외 | 이 지점 이후는 고정하지 않음 | cookie omission이 확인 지점 | 마지막 줄에서 확인하는 것은 최종 status가 아니라 cookie가 빠졌는지다. ## CSRF가 XSS를 대신하지 않는다 브라우저에 OAuth token을 주지 않아도 same-origin 악성 script는 피해자 session으로 BFF endpoint를 부를 수 있다. JavaScript가 읽을 수 있는 `XSRF-TOKEN`도 같이 읽을 수 있다. 이 구조가 줄이는 것은 access·refresh token 원문이 script에서 유출되어 다른 client나 직접 API 호출에 재사용되는 범위다. CSP, output encoding, 의존성 무결성, 애플리케이션 인가는 별도 방어선으로 남는다."
- generic [ref=f9e117]: “##” 소제목이 목차가 됩니다. 아키텍처 도식은 아래에서 삽입하세요.
- group [ref=f9e118]:
- paragraph [ref=f9e119]: EVIDENCE
- heading "본문에 Asset 삽입" [level=3] [ref=f9e120]
- paragraph [ref=f9e121]: 목록에서 선택하면 본문 커서 위치에 evidence 구문을 삽입합니다. READY 상태의 Asset만 선택할 수 있습니다.
- generic [ref=f9e122]:
- generic [ref=f9e123]:
- generic [ref=f9e124]: 업로드 종류
- combobox "업로드 종류" [ref=f9e125]:
- option "이미지" [selected]
- option "다이어그램"
- option "첨부파일"
- button "Asset 업로드" [ref=f9e126]
- generic [ref=f9e127]:
- search [ref=f9e128]:
- generic [ref=f9e129]: Asset 검색
- generic [ref=f9e130]:
- searchbox "Asset 검색" [ref=f9e131]
- button "검색" [ref=f9e132]
- generic [ref=f9e133]:
- checkbox "삽입할 때 크게 보기 허용" [checked] [ref=f9e134]
- generic [ref=f9e135]: 삽입할 때 크게 보기 허용
- status [ref=f9e136]: 삽입할 수 있는 Asset 24개
- list [ref=f9e137]:
- listitem [ref=f9e138]:
- button "ap4-edge-trust-architecture-1a916e10" [ref=f9e139]
- button "삭제" [ref=f9e140]
- listitem [ref=f9e141]:
- button "ap3-bff-session-flow-1b005e15" [ref=f9e142]
- button "삭제" [ref=f9e143]
- listitem [ref=f9e144]:
- button "ap3-bff-architecture-a27ea91c" [ref=f9e145]
- button "삭제" [ref=f9e146]
- listitem [ref=f9e147]:
- button "ap2-mediator-handoff-flow-efe7039c" [ref=f9e148]
- button "삭제" [ref=f9e149]
- listitem [ref=f9e150]:
- button "ap2-mediator-architecture-c95ed25f" [ref=f9e151]
- button "삭제" [ref=f9e152]
- listitem [ref=f9e153]:
- button "projection-row-over-fetch-f2b1943b" [ref=f9e154]
- button "삭제" [ref=f9e155]
- listitem [ref=f9e156]:
- button "cartesian-row-multiplication-dce2e166" [ref=f9e157]
- button "삭제" [ref=f9e158]
- listitem [ref=f9e159]:
- button "eager-lazy-query-sequence-47c12bda" [ref=f9e160]
- button "삭제" [ref=f9e161]
- listitem [ref=f9e162]:
- button "ap3-bff-session-flow-a8dfff6f" [ref=f9e163]
- button "삭제" [ref=f9e164]
- listitem [ref=f9e165]:
- button "ap2-mediator-handoff-flow-8c2a6f8f" [ref=f9e166]
- button "삭제" [ref=f9e167]
- listitem [ref=f9e168]:
- button "ap4-edge-forward-auth-flow-a6ec423a" [ref=f9e169]
- button "삭제" [ref=f9e170]
- listitem [ref=f9e171]:
- button "ap3-csrf-boundary-971df81c" [ref=f9e172]
- button "삭제" [ref=f9e173]
- listitem [ref=f9e174]:
- button "login-api-phase-split-3e354274" [ref=f9e175]
- button "삭제" [ref=f9e176]
- listitem [ref=f9e177]:
- button "ap1-browser-bearer-flow-a7f8aa9e" [ref=f9e178]
- button "삭제" [ref=f9e179]
- listitem [ref=f9e180]:
- button "ap1-direct-architecture-0adf4199" [ref=f9e181]
- button "삭제" [ref=f9e182]
- listitem [ref=f9e183]:
- button "nplus1-query-fanout-644febe6" [ref=f9e184]
- button "삭제" [ref=f9e185]
- listitem [ref=f9e186]:
- button "ap4-edge-trust-1cff2399" [ref=f9e187]
- button "삭제" [ref=f9e188]
- listitem [ref=f9e189]:
- button "ap3-csrf-split-501dd1f7" [ref=f9e190]
- button "삭제" [ref=f9e191]
- listitem [ref=f9e192]:
- button "ap3-bff-custody-82fa18bd" [ref=f9e193]
- button "삭제" [ref=f9e194]
- listitem [ref=f9e195]:
- button "ap2-split-custody-779cb791" [ref=f9e196]
- button "삭제" [ref=f9e197]
- listitem [ref=f9e198]:
- button "ap1-custody-v3-6e0376d2" [ref=f9e199]
- button "삭제" [ref=f9e200]
- listitem [ref=f9e201]:
- button "ap1-custody-v2-e110bd98" [ref=f9e202]
- button "삭제" [ref=f9e203]
- listitem [ref=f9e204]:
- button "ap1-credential-custody-f5e0c027" [ref=f9e205]
- button "삭제" [ref=f9e206]
- listitem [ref=f9e207]:
- button "screenshot-from-2026-08-21-18-04-49-72f1f9c6" [ref=f9e208]
- button "삭제" [ref=f9e209]
- region [ref=f9e210]:
- generic [ref=f9e211]:
- paragraph [ref=f9e212]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f9e213]
- generic [ref=f9e216]:
- generic [ref=f9e217]:
- navigation "문서 경로" [ref=f9e218]:
- link "동작 원리" [ref=f9e219] [cursor=pointer]:
- /url: /explore/concepts
- generic [aria-hidden] [ref=f9e220]: /
- generic [ref=f9e221]: OAuth/OIDC 인증 경계
- generic [aria-hidden] [ref=f9e222]: /
- link "KeyCloak Patterns" [ref=f9e223] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "Cookie로 인증하는 요청에서 CSRF token이 하는 일" [level=1] [ref=f9e224]
- paragraph [ref=f9e225]: session cookie는 브라우저가 요청마다 자동으로 붙인다. 그래서 상태를 바꾸는 요청이 사용자의 의도인지 서버가 따로 확인해야 한다. CSRF token이 그 확인이고, SameSite는 브라우저가 cookie를 언제 보낼지 정하는 별도의 정책이다.
- generic [ref=f9e226]:
- generic [ref=f9e227]:
- term [ref=f9e228]: 기준
- definition [ref=f9e229]:
- paragraph [ref=f9e230]: Spring Security 6 CSRF · AP3 BFF 구성
- generic [ref=f9e231]:
- term [ref=f9e232]: 기록
- definition [ref=f9e233]: 게시 게시 전
- group [ref=f9e235]:
- generic "목차 · SameSite와 CSRF token은 역할이 다르다" [ref=f9e236] [cursor=pointer]
- article [ref=f9e238]:
- region [ref=f9e239]:
- heading [level=2] [ref=f9e240]:
- link "cookie가 credential이 되면 생기는 일 바로가기" [ref=f9e241] [cursor=pointer]:
- /url: "#cookie가-credential이-되면-생기는-일"
- text: cookie가 credential이 되면 생기는 일
- generic [aria-hidden] [ref=f9e242]: "#"
- paragraph [ref=f9e243]: 브라우저가 OAuth token을 받지 않는 구조에서도 인증 상태는 남는다. BFF는 HttpOnly session cookie로 로그인 상태를 찾는다.
- paragraph [ref=f9e244]:
- text: 이 cookie는 브라우저가 자동으로 붙인다. 다른 사이트가 만든 요청에도 붙을 수 있다는 뜻이다.
- code [ref=f9e245]: GET /bff/api/me
- text: 만 보면 이 문제가 드러나지 않으므로 상태를 바꾸는 요청을 따로 봐야 한다.
- region [ref=f9e246]:
- heading [level=2] [ref=f9e247]:
- link "token을 받아 오는 요청 바로가기" [ref=f9e248] [cursor=pointer]:
- /url: "#token을-받아-오는-요청"
- text: token을 받아 오는 요청
- generic [aria-hidden] [ref=f9e249]: "#"
- paragraph [ref=f9e250]: 브라우저가 먼저 CSRF material을 요청한다.
- figure "HTTP ·CSRF token 요청 코드 복사" [ref=f9e251]:
- generic [ref=f9e252]:
- generic [ref=f9e253]: HTTP
- generic [ref=f9e254]: ·CSRF token 요청
- button "코드 복사" [ref=f9e255] [cursor=pointer]: 복사
- region "CSRF token 요청 코드" [ref=f9e256]:
- code [ref=f9e257]: "GET http://localhost:8083/bff/csrf Accept: application/json Cookie: AP3_SESSION=<opaque-session-id>"
- paragraph [ref=f9e259]:
- code [ref=f9e260]: CookieCsrfTokenRepository.withHttpOnlyFalse()
- text: 는 JavaScript가 읽을 수 있는
- code [ref=f9e261]: XSRF-TOKEN
- text: cookie를 path
- code [ref=f9e262]: /
- text: 에 만든다. controller는 다음 JSON을 반환한다.
- figure "JSON ·CsrfController가 반환하는 JSON 코드 복사" [ref=f9e263]:
- generic [ref=f9e264]:
- generic [ref=f9e265]: JSON
- generic [ref=f9e266]: ·CsrfController가 반환하는 JSON
- button "코드 복사" [ref=f9e267] [cursor=pointer]: 복사
- region "CsrfController가 반환하는 JSON 코드" [ref=f9e268]:
- code [ref=f9e269]: "{ \"headerName\": \"X-XSRF-TOKEN\", \"parameterName\": \"_csrf\", \"token\": \"<xor-masked-csrf-token>\" }"
- figure [ref=f9e271]:
- button "ap3-csrf-boundary-971df81c 이미지 크게 보기" [ref=f9e272]:
- img "BFF CSRF endpoint가 raw XSRF cookie와 masked JSON token으로 분기하고, SPA가 raw cookie만 실제 POST header 값으로 사용해 Spring CSRF filter에 제출하는 데이터 흐름." [ref=f9e273]
- generic [ref=f9e274]: 크게 보기
- generic [ref=f9e275]: BFF CSRF endpoint가 raw XSRF cookie와 masked JSON token으로 분기하고, SPA가 raw cookie만 실제 POST header 값으로 사용해 Spring CSRF filter에 제출하는 데이터 흐름.
- region [ref=f9e276]:
- heading [level=2] [ref=f9e277]:
- link "body의 token과 cookie의 값은 다르다 바로가기" [ref=f9e278] [cursor=pointer]:
- /url: "#body의-token과-cookie의-값은-다르다"
- text: body의 token과 cookie의 값은 다르다
- generic [aria-hidden] [ref=f9e279]: "#"
- paragraph [ref=f9e280]: 여기가 이 구조에서 가장 헷갈리는 지점이다.
- region "표" [ref=f9e281]:
- table [ref=f9e282]:
- caption [ref=f9e283]
- rowgroup [ref=f9e284]:
- row [ref=f9e285]:
- columnheader "위치" [ref=f9e286]
- columnheader "값" [ref=f9e287]
- rowgroup [ref=f9e288]:
- row [ref=f9e289]:
- cell [ref=f9e290]:
- text: 응답 body의
- code [ref=f9e291]: token
- cell "XOR와 Base64로 mask된 값" [ref=f9e292]
- row [ref=f9e293]:
- cell [ref=f9e294]:
- code [ref=f9e295]: XSRF-TOKEN
- text: cookie
- cell "raw 값" [ref=f9e296]
- row [ref=f9e297]:
- cell [ref=f9e298]:
- text: POST의
- code [ref=f9e299]: X-XSRF-TOKEN
- text: 헤더
- cell "cookie와 같은 raw 값" [ref=f9e300]
- paragraph [ref=f9e301]:
- code [ref=f9e302]: XorCsrfTokenRequestAttributeHandler
- text: 가 request attribute용 token을 mask하기 때문에 controller JSON에는 masked 값이 보인다. SPA는 JSON에서
- code [ref=f9e303]: headerName
- text: 만 읽고, 실제 값은
- code [ref=f9e304]: document.cookie
- text: 에서 raw
- code [ref=f9e305]: XSRF-TOKEN
- text: 을 찾아 쓴다.
- paragraph [ref=f9e306]:
- code [ref=f9e307]: SpaCsrfTokenRequestHandler
- text: 가 이 조합을 맞춘다. expected 헤더가 있으면 plain resolver로 제출된 raw token을 읽고, 없으면 XOR resolver 경로를 쓴다.
- paragraph [ref=f9e308]:
- text: 응답 JSON의
- code [ref=f9e309]: token
- text: 을 그대로 헤더에 복사하면 값이 맞지 않아 403이 된다. 노출 값과 제출 값이 다를 수 있다는 것을 클라이언트 코드가 알아야 한다.
- region [ref=f9e310]:
- heading [level=2] [ref=f9e311]:
- link "검증이 controller보다 먼저 일어난다 바로가기" [ref=f9e312] [cursor=pointer]:
- /url: "#검증이-controller보다-먼저-일어난다"
- text: 검증이 controller보다 먼저 일어난다
- generic [aria-hidden] [ref=f9e313]: "#"
- paragraph [ref=f9e314]: 정상 상태 변경 요청은 다음과 같다.
- figure "HTTP ·CSRF 검증을 통과하는 POST 코드 복사" [ref=f9e315]:
- generic [ref=f9e316]:
- generic [ref=f9e317]: HTTP
- generic [ref=f9e318]: ·CSRF 검증을 통과하는 POST
- button "코드 복사" [ref=f9e319] [cursor=pointer]: 복사
- region "CSRF 검증을 통과하는 POST 코드" [ref=f9e320]:
- code [ref=f9e321]: "POST http://localhost:8083/bff/api/preferences Content-Type: application/x-www-form-urlencoded Cookie: AP3_SESSION=<opaque-session-id>; XSRF-TOKEN=<raw-csrf-token> X-XSRF-TOKEN: <same-raw-csrf-token> theme=dark"
- paragraph [ref=f9e323]: Spring CSRF filter가 repository의 expected token과 제출된 헤더를 비교한다. 헤더가 없거나 값이 맞지 않으면 controller는 실행되지 않고 403이 된다. 검증 지점이 controller 앞이라 endpoint를 추가해도 같은 filter를 지난다.
- region [ref=f9e324]:
- heading [level=2] [ref=f9e325]:
- link "SameSite와 CSRF token은 역할이 다르다 바로가기" [ref=f9e326] [cursor=pointer]:
- /url: "#samesite와-csrf-token은-역할이-다르다"
- text: SameSite와 CSRF token은 역할이 다르다
- generic [aria-hidden] [ref=f9e327]: "#"
- region "표" [ref=f9e328]:
- table [ref=f9e329]:
- caption [ref=f9e330]
- rowgroup [ref=f9e331]:
- row [ref=f9e332]:
- columnheader [ref=f9e333]
- columnheader "SameSite" [ref=f9e334]
- columnheader "CSRF token" [ref=f9e335]
- rowgroup [ref=f9e336]:
- row [ref=f9e337]:
- cell "누가 판단하나" [ref=f9e338]
- cell "브라우저" [ref=f9e339]
- cell "서버" [ref=f9e340]
- row [ref=f9e341]:
- cell "무엇을 정하나" [ref=f9e342]
- cell "cookie를 보낼지" [ref=f9e343]
- cell "요청을 받아들일지" [ref=f9e344]
- row [ref=f9e345]:
- cell "언제 작동하나" [ref=f9e346]
- cell "요청을 만들 때" [ref=f9e347]
- cell "요청을 처리할 때" [ref=f9e348]
- paragraph [ref=f9e349]: 두 방어선은 서로를 대신하지 못한다. port가 달라도 site 계산상 같은 경우가 있어서, SameSite가 막지 않는 요청에도 CSRF 검증이 필요하다.
- paragraph [ref=f9e350]: 네 가지 입력을 나란히 두면 어느 방어선이 작동하는지 갈린다.
- region "표" [ref=f9e351]:
- table [ref=f9e352]:
- caption [ref=f9e353]
- rowgroup [ref=f9e354]:
- row [ref=f9e355]:
- columnheader "입력" [ref=f9e356]
- columnheader "cookie 동작" [ref=f9e357]
- columnheader "CSRF 동작" [ref=f9e358]
- columnheader "결과" [ref=f9e359]
- rowgroup [ref=f9e360]:
- row [ref=f9e361]:
- cell "same-origin, CSRF 헤더 없음" [ref=f9e362]
- cell "session cookie 붙음" [ref=f9e363]
- cell "token 부재로 거부" [ref=f9e364]
- cell "403" [ref=f9e365]
- row [ref=f9e366]:
- cell "same-origin, raw cookie와 헤더 일치" [ref=f9e367]
- cell "session cookie 붙음" [ref=f9e368]
- cell "token 일치" [ref=f9e369]
- cell "200" [ref=f9e370]
- row [ref=f9e371]:
- cell "다른 port지만 same-site, 헤더 없음" [ref=f9e372]
- cell "cookie가 붙을 수 있음" [ref=f9e373]
- cell "token 부재로 거부" [ref=f9e374]
- cell "403" [ref=f9e375]
- row [ref=f9e376]:
- cell "cross-site POST" [ref=f9e377]
- cell "SameSite=Lax로 cookie 제외" [ref=f9e378]
- cell "이 지점 이후는 고정하지 않음" [ref=f9e379]
- cell "cookie omission이 확인 지점" [ref=f9e380]
- paragraph [ref=f9e381]: 마지막 줄에서 확인하는 것은 최종 status가 아니라 cookie가 빠졌는지다.
- region [ref=f9e382]:
- heading [level=2] [ref=f9e383]:
- link "CSRF가 XSS를 대신하지 않는다 바로가기" [ref=f9e384] [cursor=pointer]:
- /url: "#csrf가-xss를-대신하지-않는다"
- text: CSRF가 XSS를 대신하지 않는다
- generic [aria-hidden] [ref=f9e385]: "#"
- paragraph [ref=f9e386]:
- text: 브라우저에 OAuth token을 주지 않아도 same-origin 악성 script는 피해자 session으로 BFF endpoint를 부를 수 있다. JavaScript가 읽을 수 있는
- code [ref=f9e387]: XSRF-TOKEN
- text: 도 같이 읽을 수 있다.
- paragraph [ref=f9e388]: 이 구조가 줄이는 것은 access·refresh token 원문이 script에서 유출되어 다른 client나 직접 API 호출에 재사용되는 범위다. CSP, output encoding, 의존성 무결성, 애플리케이션 인가는 별도 방어선으로 남는다.
- region [ref=f9e389]:
- paragraph [ref=f9e390]: Next
- heading "다음에 읽을 것" [level=2] [ref=f9e391]
- list [ref=f9e392]:
- listitem [ref=f9e393]:
- link "적용 기준 BFF 인증 구조 설계 기준" [ref=f9e394] [cursor=pointer]:
- /url: /references/bff-authentication-design-criteria
- generic [ref=f9e395]: 적용 기준
- generic [ref=f9e396]:
- strong [ref=f9e397]: BFF 인증 구조 설계 기준
- paragraph [aria-hidden] [ref=f9e398]: 이 확인이 필요한 구조의 설계 항목이다.
- generic [aria-hidden] [ref=f9e399]:
- listitem [ref=f9e400]:
- link "검증 기록 Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f9e401] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- generic [ref=f9e402]: 검증 기록
- generic [ref=f9e403]:
- strong [ref=f9e404]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정
- paragraph [aria-hidden] [ref=f9e405]: 이 동작을 실제로 재현한 기록이다.
- generic [aria-hidden] [ref=f9e406]:
- listitem [ref=f9e407]:
- link "적용 기준 OAuth Token과 Application Session을 구분하는 기준" [ref=f9e408] [cursor=pointer]:
- /url: /references/oauth-token-application-session-boundary
- generic [ref=f9e409]: 적용 기준
- generic [ref=f9e410]:
- strong [ref=f9e411]: OAuth Token과 Application Session을 구분하는 기준
- paragraph [aria-hidden] [ref=f9e412]: session cookie와 CSRF token은 서로 다른 값이다.
- generic [aria-hidden] [ref=f9e413]:
- complementary [ref=f9e414]:
- heading "작업 상태" [level=2] [ref=f9e415]
- status "편집 상태" [ref=f9e416]: 저장됨
- generic [ref=f9e417]:
- generic [ref=f9e418]:
- term [ref=f9e419]: 저장 버전
- definition [ref=f9e420]: "5"
- generic [ref=f9e421]:
- term [ref=f9e422]: 종류
- definition [ref=f9e423]: 동작 원리
- paragraph [ref=f9e424]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f9e425]:
- button "저장" [disabled] [ref=f9e426]
- button "게시" [ref=f9e427]
- paragraph [ref=f9e428]: 버전 5으로 저장했습니다.