20 KiB
Redis Session 요청은 어디에서 멈추는가: Web 설정과 미완성 Repository
Redis 코드 상세 시리즈 17/20 · 전체 지도 · 이전: Redis Idempotency V2 상태 머신: Claim에서 Replay까지 · 다음: 같은 Redis 장애가 DEGRADED와 DOWN으로 갈리는 코드
이 글이 답하는 코드 질문
ca-skeleton.security.auth-mode=redis-session으로 설정하면 어떤 web/security 객체가 생기며, HTTP session은 실제로 Redis에 저장됩니까? startup validator가 요구하는 redisVersionedSessionRepository는 어디에 구현되어 있습니까?
현행 답은 두 경계에서 멈춥니다. cookie, Spring Session filter activation annotation, primitive security-context repository, stateful session policy branch는 구현되어 있습니다. 그러나 production SessionRepository bean, 이름이 redisVersionedSessionRepository인 bean, Redis session adapter는 source에서 확인되지 않습니다. 별도로, 인증 snapshot이 없는 요청에서 최초 Authentication을 만드는 form login, HTTP Basic, custom authentication filter나 production login endpoint도 확인되지 않습니다. 따라서 Redis Session capability는 미완성이고 semantic capability composition은 cache/rate-limit/lease/idempotency V2의 4/5입니다.
먼저 보는 클래스 지도
| 코드 | 입력 | 출력 | 다음 호출 |
|---|---|---|---|
RedisSessionWebConfig |
auth-mode와 cookie settings | CookieSerializer, Spring Session filter configuration |
필요한 SessionRepository bean |
SecurityConfig.filterChain |
auth mode, error handlers, context repository | JWT stateless 또는 session stateful chain | Spring Security filters |
PrimitiveSessionSecurityContextRepository |
SecurityContext, HttpSession |
bounded byte snapshot 또는 empty context | session attribute |
AuthenticationModeCompositionConfig |
auth-mode, bean registry | startup pass/fail | 없음 |
RedisActivationValidator |
global switch와 role selectors | startup pass/fail | 없음 |
객체 조립에서 먼저 걸리는 두 validator
RedisActivationValidator는 auth mode redis-session을 Redis-selecting role로 등록합니다. app.redis.enabled=false인데 이 mode를 선택하면 startup에 모순으로 거절합니다. role selector가 Redis를 자동 활성화하지는 않습니다. REDIS_SELECTING_VALUES
그 다음 AuthenticationModeCompositionConfig는 bean 이름으로 완성도를 검사합니다.
- JWT mode:
jwtDecoder는 있어야 하고 session repository/filter는 없어야 합니다. - REDIS_SESSION mode:
jwtDecoder는 없어야 하고redisVersionedSessionRepository,springSessionRepositoryFilter가 둘 다 있어야 합니다.
검사는 type이 아니라 containsBean 이름입니다. validate
문제는 production source 전체에서 redisVersionedSessionRepository를 만드는 @Bean이나 SessionRepository 구현이 확인되지 않는다는 점입니다. 검색 결과는 validator와 그 unit test의 fake bean뿐입니다. 따라서 mode를 실제로 선택하면 web 설정이 활성화되더라도 composition validator가 repository와 filter가 갖춰지지 않았다고 판단해 startup을 거절하는 것이 현행 의도에 가까운 결과입니다.
web 설정이 제공하는 것
RedisSessionWebConfig는 auth mode가 redis-session일 때만 활성화됩니다. @EnableSpringHttpSession은 Spring Session filter infrastructure를 import하지만, filter를 만들려면 SessionRepository bean이 필요합니다. 이 configuration 자체는 repository를 만들지 않습니다. RedisSessionWebConfig
이 config의 유일한 explicit bean은 CookieSerializer입니다. 설정에서 cookie name, Secure, HttpOnly, SameSite, path를 읽고 max age -1, Base64 encoding을 적용합니다. domain/domain pattern을 지정하지 않으므로 host-only cookie입니다. cookie가 안전하게 구성됐다는 사실은 session data가 Redis에 저장된다는 증거가 아닙니다.
adapter:inbound:web은 spring-session-core만 의존합니다. Redis store 구현을 제공하는 Spring Data Redis dependency는 이 module에 없습니다. adapter/inbound/web/build.gradle
SecurityFilterChain의 mode 분기
flowchart TD
A[SecurityConfig.filterChain] --> B{authMode}
B -->|JWT| C[CSRF disabled]
C --> D[STATELESS]
D --> E[Bearer filter + JWT converter가 Authentication 생성]
B -->|REDIS_SESSION| F[Cookie CSRF repository]
F --> G[IF_REQUIRED + migrateSession]
G --> H[PrimitiveSecurityContext load/save]
G --> M{최초 Authentication mechanism?}
M -->|production source| N[form/basic/custom filter·login endpoint 미확인]
M -->|test 전용 controller| O[SecurityContext에 직접 설정]
O -.->|저장 대상 제공| H
H --> I[HttpSession primitive byte attribute]
I --> J[springSessionRepositoryFilter]
J --> K{SessionRepository bean?}
K -->|production source에서 없음| L[startup composition incomplete]
K -->|test MapSessionRepository| P[in-memory persistence]
JWT branch는 CSRF를 끄고 SessionCreationPolicy.STATELESS와 resource-server JWT converter를 설정합니다. session branch는 CSRF cookie/header, IF_REQUIRED, session fixation migration을 설정하고 PrimitiveSessionSecurityContextRepository를 Spring Security의 context repository로 지정합니다. SecurityConfig.filterChain
session CSRF cookie는 secure true, httpOnly false, configured SameSite/path입니다. JavaScript가 token을 읽어 header로 돌려보내는 double-submit 형태이므로 session ID cookie의 HttpOnly와 목적이 다릅니다.
PrimitiveSessionSecurityContextRepository bean도 auth mode 조건부입니다. session branch에서 ObjectProvider.getObject()를 호출하므로 mode는 session인데 bean이 없다면 filter chain 생성 자체가 실패합니다. 현행 조건은 같은 property를 쓰므로 정상적으로 함께 활성화됩니다.
session mode의 세 층은 서로 다른 책임입니다
첫째, springSessionRepositoryFilter와 SessionRepository는 HttpSession을 provider storage에 저장하고 다시 읽습니다. 이 filter는 session persistence filter이지 사용자를 인증하는 filter가 아닙니다.
둘째, PrimitiveSessionSecurityContextRepository는 이미 존재하는 authenticated context를 bounded bytes로 저장하고, 다음 요청에서 그 snapshot을 Authentication으로 복원합니다. 기존 snapshot을 복원할 수 있다는 사실은 최초 snapshot을 만들 수 있다는 뜻이 아닙니다. load
셋째, 인증 snapshot이 없는 요청에서는 credential이나 외부 identity를 검증해 최초 Authentication을 만드는 mechanism이 필요합니다. JWT branch는 oauth2ResourceServer와 JWT converter를 설정하지만 Redis-session branch는 CSRF, IF_REQUIRED, fixation migration, context repository만 설정합니다. formLogin, httpBasic, custom authentication filter, production login endpoint는 production source에서 확인되지 않습니다. SecurityConfig의 두 mode 분기
따라서 redisVersionedSessionRepository만 추가해 validator를 통과하더라도 persistence 조립만 채워집니다. 최초 인증 조립은 별도 공백으로 남습니다.
primitive snapshot의 저장 형식
이 repository는 Spring Security의 SecurityContext object graph를 session에 그대로 넣지 않습니다. attribute 이름은 dev.caskeleton.security.PRIMITIVE_SECURITY_CONTEXT_V1이고 값은 byte[]입니다. SNAPSHOT_ATTRIBUTE
binary layout은 다음 순서입니다.
- magic
0x43534543 - version 1
- length-prefixed principal ID
- nullable email
- role count와 정렬된 role strings
- authority count와 정렬된 authority strings
credential은 저장하지 않습니다. principal은 AuthenticatedPrincipal만 허용합니다. 전체 snapshot은 16,384 bytes, principal 256 UTF-8 bytes, email 320 bytes, token 128 bytes, roles 64개, authorities 128개로 제한됩니다. encode
load할 때 magic/version/길이/count/중복/trailing bytes를 검사합니다. 손상되거나 incompatible하면 exception을 밖으로 내보내지 않고 attribute를 삭제한 뒤 empty context를 반환합니다. 즉 corrupt session authentication은 authenticated로 복구되지 않습니다. load
request-time save와 load 순서
sequenceDiagram
participant F as SecurityContext filter
participant P as Primitive repository
participant H as HttpSession
participant S as Spring Session filter
participant X as SessionRepository
F->>P: loadContext(holder)
P->>H: getSession(false), get snapshot
P-->>F: decoded authentication 또는 empty
Note over P,F: response/request wrapper 설치
F->>P: saveContext(final context)
alt authenticated AuthenticatedPrincipal
P->>H: getSession(true), set byte[]
else empty/anonymous
P->>H: remove attribute if session exists
end
H->>S: session mutation
S->>X: save session
Note over X: production Redis repository는 확인되지 않음
loadContext는 response에 CommitSaveResponseWrapper를 씌웁니다. response가 commit될 때 현재 context를 저장하되, 이후 explicit final save가 빈 context면 앞서 저장한 snapshot을 제거합니다. async가 시작되면 commit hook 저장을 끄고 final save까지 미룹니다. CommitSaveResponseWrapper
인증이 없거나 anonymous면 기존 session을 새로 만들지 않고 attribute만 제거합니다. 인증된 context면 getSession(true)로 session을 만들고 bytes를 저장합니다. 이 시점의 HttpSession을 어느 backend에 persist할지는 Spring Session SessionRepository의 책임입니다.
정상과 실패 분기
구현된 web 경계의 정상 분기는 다음과 같습니다.
- JWT mode에는 session cookie serializer/filter가 생기지 않습니다.
- Redis-session mode에서 repository가 제공되면 Spring Session filter와 cookie serializer가 생깁니다. 이것만으로 새 사용자의 최초 인증이 생기지는 않습니다.
- authenticated primitive principal은 credential 없이 round-trip합니다.
- empty/anonymous context는 snapshot을 제거합니다.
- corrupt snapshot은 제거하고 unauthenticated 상태로 처리합니다.
- foreign principal graph, oversized authority count, control character·byte bound 위반은 save 시
IllegalArgumentException입니다.
현재 production 조립 실패는 Redis timeout이나 ambiguous write보다 앞에 있습니다. Redis로 session command를 보내는 repository 자체가 없으므로 Redis 명령, TTL, envelope/version migration, touch/save/delete certainty를 분석할 production code도 없습니다. repository를 보완한 뒤에도 최초 인증 mechanism이 없으면 새 unauthenticated 요청은 anyRequest().authenticated()에서 인증 entry point로 갈 뿐, 저장할 authenticated context를 만들지 못합니다.
PrimitiveSessionSecurityContextRepository의 이름에 Redis가 없다는 점도 중요합니다. 이 객체는 HttpSession attribute의 내용과 lifecycle만 소유하며 provider storage를 소유하지 않습니다.
테스트가 고정하는 계약
RedisSessionWebConfigTest.jwtModeCreatesNoSessionFilterOrCookieSerializer는 JWT에서 web session infrastructure가 비활성임을 검사합니다.- 같은 test의
redisSessionModeWritesSecureHttpOnlySameSiteHostOnlyCookie는 test가 직접MapSessionRepository를 제공한 뒤 cookie flags와 host-only 속성을 확인합니다. Redis repository 검증이 아닙니다. PrimitiveSessionSecurityContextRepositoryTest는 primitive bytes round-trip과 credential/framework-object 배제를 검사합니다.- 같은 test의 commit/final/async cases는 response commit 전에 session 생성이 필요한 경우와 최종 context가 앞선 snapshot을 교체·삭제하는 순서를 고정합니다.
savesThePrimitiveSnapshotBeforeAResponseCommitRequiresANewSession - corrupt/foreign test는 손상 bytes를 empty authentication으로 만들고 attribute를 제거하며 foreign principal save를 거절합니다.
rejectsForeignPrincipalGraphsAndFailsClosedOnCorruptSnapshots SecurityModeWebContractTest.redisSessionSecurityFilterPersistsAndRestoresOnlyThePrimitiveAuthenticationSnapshot는 primitive snapshot round-trip을 검사합니다. 하지만 최초 인증은 test 전용/login-testcontroller가SecurityContextHolder에 authenticated token을 직접 넣어 만듭니다.loginForContractAuthenticationModeCompositionConfigTest는 이름만 가진 fake repository/filter bean으로 exclusive composition rule을 검사합니다. repository 기능을 입증하지 않습니다.
현재 구현 공백과 잘못 읽기 쉬운 지점
redisVersionedSessionRepositoryproduction bean 또는 구현은 확인되지 않습니다.- Redis session record의 key, value envelope, session TTL, save/touch/delete command나 Lua도 production source에 없습니다.
- 인증 snapshot이 없는 요청에서 최초
Authentication을 만드는 production mechanism도 확인되지 않습니다. repository를 추가하는 것만으로 Redis Session 인증 mode가 완성되지 않습니다. - 따라서 Redis failure의 unavailable/indeterminate 분기와 session fail-closed 정책을 실행 코드 수준에서 확인할 수 없습니다.
@EnableSpringHttpSession은 repository 구현이 아닙니다. test는MapSessionRepository를 주입해 filter/cookie 조립만 확인합니다.PrimitiveSessionSecurityContextRepository는 이미 만들어진 security snapshot의 serializer/load-save 경계이며 provider repository나 최초 인증 mechanism이 아닙니다.- composition validator가 요구하는 bean 이름은 contract 역할을 하지만 type, 기능, 최초 인증 경로를 검사하지는 않습니다.
- semantic capability 5개 중 production adapter가 조립되는 것은 cache, rate-limit, lease, idempotency V2의 4개입니다. Session은 미완성입니다.
- real-server topology tests에는 Session repository flow가 없습니다. 이번 작성에서도 real-server lane을 실행하지 않았습니다.
다음에 열어볼 source 순서
RedisSessionWebConfig → SecurityConfig의 두 authentication branch → primitive repository → SecurityModeWebContractTest의 test-only login → composition validator 순으로 읽으면 “최초 인증”, “security-context snapshot”, “Redis persistence”를 섞지 않을 수 있습니다.