--- kind: CONCEPT slug: two-stores-two-lookup-keys title: 세션과 인가된 클라이언트는 조회 키가 다르다 topic: where-application-state-lives topicName: 세션과 토큰을 Redis 와 PostgreSQL 에 나눠 두기 project: keycloak-session-store status: 게시 전 basisVersion: Spring Boot 3 · Spring Security OAuth2 Client · Spring Session Redis sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af source: - final/document.md#선택이-코드와-흐름에-반영되는-방식-b0 assets: - key: bff-store-lookup-keys file: ../../../final/assets/bff-store-lookup-keys/bff-store-lookup-keys.svg evidence: - ../../../final/evidence/raw/b0-bff-redis-deploy__03-beans-analysis.txt - ../../../final/evidence/raw/b1-redis-session-store__02-autoconfig-after.txt - ../../../final/evidence/raw/b1-redis-session-store__03-redis-contents.txt - ../../../final/evidence/raw/b2-multi-instance-session__02-schema.txt - ../../../final/evidence/raw/b2-multi-instance-session__04-overwrite-test.txt --- # 세션과 인가된 클라이언트는 조회 키가 다르다 BFF 가 서버에 두는 상태는 둘이고, 세션은 세션 id 로 토큰은 principal 이름으로 찾는다. 조회 키가 이렇게 갈라져 있어서 세션 저장소만 Redis 로 바꿔도 OAuth2AuthorizedClient 는 따라오지 않는다. ## 관계 - **세션만 Redis 로 옮기자 토큰이 따라오지 않았다** 여기서 설명하는 조회 키 차이가 실제 배포에서 어떤 결과를 냈는지 잰 기록이다. - **기본키에 세션 id 가 없어서 두 번째 로그인이 첫 토큰을 덮어썼다** principal 이름으로 찾는다는 것이 테이블 기본키 한 줄로 드러난 대목을 이어서 다룬다. - **세션과 토큰의 저장소를 나눠 각각 설계한다** 두 저장소를 따로 옮긴 뒤 무엇이 풀리고 무엇이 남았는지를 적은 기록이다. ## 본문 ## 자동 구성이 고르는 네 가지 Redis 도 Spring Session 도 붙이지 않은 Spring Boot BFF(Backend for Frontend, 프런트엔드 하나를 위해 두는 백엔드) 에서 자동 구성이 골라 둔 것은 넷이다. ``` authorizedClientService → InMemoryOAuth2AuthorizedClientService authorizedClientRepository → AuthenticatedPrincipalOAuth2AuthorizedClientRepository SessionRepository → 없음 (서블릿 컨테이너 in-memory) Redis / Spring Session → 없음 ``` 둘째 줄의 `AuthenticatedPrincipalOAuth2AuthorizedClientRepository` 는 인증된 principal 을 기준으로 인가된 클라이언트를 꺼내 오는 구현이고, principal 이름으로 찾기 때문에 조회 키에 세션 id 가 없다. 첫째 줄의 `InMemoryOAuth2AuthorizedClientService` 는 이름 그대로 인스턴스 메모리에 두고, `SessionRepository` 는 아예 없어서 세션도 서블릿 컨테이너 메모리에 남는다. 이 목록이 아무것도 안 줬을 때의 값인 것은 배포 순서를 그렇게 짜 두었기 때문이다. 배포 매니페스트의 머리 주석이 그 순서를 적어 놓았다. ```text # The BFF is deployed FIRST WITHOUT any session store wiring. That is deliberate: # B-0 asks what Spring Boot's autoconfiguration actually picks when nothing is # configured, and the only honest way to answer is to look at a running instance # that has been given nothing. Redis is deployed alongside but left unused until # B-1 turns it on. ``` Redis 는 같은 매니페스트로 함께 올라가 있었고 연결만 안 했다. ## 한 요청이 두 갈래로 조회된다 이름은 비슷해도 담는 것이 다른 두 가지가 따로 굴러간다. | | 무엇을 담나 | 조회 키 | |---|---|---| | Application Session | 누가 로그인했는지 | 세션 id | | OAuth2AuthorizedClient | access · refresh token | principal 이름 | 같은 요청이 세션은 세션 id 로, 토큰은 principal 이름으로 두 갈래로 조회된다. ![세션 id 로 찾는 Application Session 과 principal 이름으로 찾는 OAuth2AuthorizedClient 가 각각 다른 저장소에 놓인 구성](../../../final/assets/bff-store-lookup-keys/bff-store-lookup-keys.svg) ## 세션만 Redis 로 옮겼을 때 남은 것 `SPRING_SESSION_STORE_TYPE=redis` 로 Application Session 을 Redis 로 옮겨도 토큰은 같이 살아남지 못한다. 조회 키가 다르므로 세션 저장소를 바꿔도 `OAuth2AuthorizedClient` 는 따라오지 않고, 옮기기 전과 뒤의 빈 목록에서도 authorizedClientService 와 authorizedClientRepository, authorizedClientManager 셋이 그대로였다. 옮긴 뒤 Redis 에는 키가 하나 있었고 타입은 hash 였다. ``` bff:session:sessions:8963b6de-3564-4775-9ccd-1ee9616b83ae 총 키 수: 1 타입: hash 필드: sessionAttr:SPRING_SECURITY_CONTEXT 필드: sessionAttr:SPRING_SECURITY_SAVED_REQUEST 필드: sessionAttr:SPRING_SECURITY_LAST_EXCEPTION 필드: sessionAttr:org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository.AUTHORIZATION_REQUEST 필드: lastAccessedTime 필드: maxInactiveInterval 필드: creationTime TTL: 1772 초 ``` 일곱 필드 어느 이름도 access token 이나 refresh token 을 가리키지 않는다. 값까지 꺼내 본 필드는 `sessionAttr:SPRING_SECURITY_CONTEXT` 하나이고, 그 값은 `\xac\xed` 두 바이트로 시작한다 — Java 기본 직렬화의 매직 넘버다. 이름에 OAuth2 가 들어간 `…AUTHORIZATION_REQUEST` 는 값 안을 열어 보지 않았으므로, 그 필드에 무엇이 들어 있는지는 이 실험대가 답하지 않는다. ## 조회 키가 기본키로 드러나는 곳 토큰은 `JdbcOAuth2AuthorizedClientService` 로 PostgreSQL 에 옮겼고, 그때 만들어진 테이블의 기본키가 조회 키를 그대로 적어 놓는다. ```sql PRIMARY KEY (client_registration_id, principal_name) ``` 두 컬럼 어디에도 세션 id 가 없다. 그래서 같은 사용자의 두 세션이 같은 행을 쓰게 되고, 나중 로그인이 앞의 토큰을 덮어쓴다. ## 지금 확인한 범위 조회 키가 principal 이름이라는 것은 두 곳에서 나온다. 아무것도 설정하지 않았을 때 자동 구성이 고른 구현체 이름과, 토큰을 PostgreSQL 로 옮겼을 때 만들어진 테이블의 기본키다. Spring Security 소스를 열어 조회 경로를 따라가지는 않았다. 여기 적은 것은 인스턴스가 둘 이상인 구성의 이야기다. 단일 인스턴스에서는 이 질문 자체가 생기지 않는다 — 저장소를 붙이기 전에는 세션도 인가된 클라이언트도 같은 프로세스 메모리에 있다.