docs: B-2 — sharing the stores fixes one problem and exposes three more
Moving the authorized client to JdbcOAuth2AuthorizedClientService makes tokens work across replicas, so the session-in-Redis plus tokens-in-PostgreSQL split holds. The table then shows what sharing cannot fix: the primary key is (client_registration_id, principal_name) with no session in it, so a second login for the same user updates the same row rather than adding one. The refresh token sits in bytea as the raw JWT, readable with convert_from, and logout clears only the Redis session while the plaintext token row and the Keycloak SSO session both survive. The schema itself failed silently first because the default DDL uses blob, which PostgreSQL does not have, and continue-on-error swallowed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f2595f748f
commit
711878379c
@@ -0,0 +1,8 @@
|
||||
deployment.apps/bff configured
|
||||
deployment "bff" successfully rolled out
|
||||
bff-555df79c97-6j86w 1/1 Running 0 44s
|
||||
bff-555df79c97-vgg6g 1/1 Running 0 22s
|
||||
|
||||
=== oauth2_authorized_client 테이블이 생겼는가 ===
|
||||
Did not find any relation named "oauth2_authorized_client".
|
||||
command terminated with exit code 1
|
||||
@@ -0,0 +1,33 @@
|
||||
=== PostgreSQL 전용 스키마 ===
|
||||
CREATE TABLE oauth2_authorized_client (
|
||||
client_registration_id varchar(100) NOT NULL,
|
||||
principal_name varchar(200) NOT NULL,
|
||||
access_token_type varchar(100) NOT NULL,
|
||||
access_token_value bytea NOT NULL,
|
||||
access_token_issued_at timestamp NOT NULL,
|
||||
access_token_expires_at timestamp NOT NULL,
|
||||
access_token_scopes varchar(1000) DEFAULT NULL,
|
||||
refresh_token_value bytea DEFAULT NULL,
|
||||
refresh_token_issued_at timestamp DEFAULT NULL,
|
||||
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (client_registration_id, principal_name)
|
||||
);
|
||||
|
||||
=== 적용 ===
|
||||
CREATE TABLE
|
||||
Table "public.oauth2_authorized_client"
|
||||
Column | Type | Collation | Nullable | Default
|
||||
-------------------------+-----------------------------+-----------+----------+-------------------------
|
||||
client_registration_id | character varying(100) | | not null |
|
||||
principal_name | character varying(200) | | not null |
|
||||
access_token_type | character varying(100) | | not null |
|
||||
access_token_value | bytea | | not null |
|
||||
access_token_issued_at | timestamp without time zone | | not null |
|
||||
access_token_expires_at | timestamp without time zone | | not null |
|
||||
access_token_scopes | character varying(1000) | | | NULL::character varying
|
||||
refresh_token_value | bytea | | |
|
||||
refresh_token_issued_at | timestamp without time zone | | |
|
||||
created_at | timestamp without time zone | | not null | CURRENT_TIMESTAMP
|
||||
Indexes:
|
||||
"oauth2_authorized_client_pkey" PRIMARY KEY, btree (client_registration_id, principal_name)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
=== Q3 검증 2번 — 저장소를 직접 열어 refresh token 이 평문인가 ===
|
||||
eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlMmUzZDZkMy0yNzQyLTRhYWItYjk4Ni02ZDU2ZDM5MDk1ZDEifQ.eyJleHAiOjE3ODg1MDA0NDYsImlhdCI6MTc4ODQ5ODY0NiwianRpIjoiNTQwOTZmYTQtZWRjNi1iZjZkLWE4OGMtZDJhNjEzOGJjNmVlIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmh5ZW9ud29ya3MuY29tL3JlYWxtcy9rZXljbG9hay1wYXR0ZXJucyIsImF1ZCI6I
|
||||
|
||||
=== access token 도 ===
|
||||
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJPWS1jYVlETkdvUDRITUF6LVE5VVBUVS1ETTFpODk2TnV6VVp1NmdmQ3FNIn0.eyJleHAi
|
||||
|
||||
=== 그 문자열이 실제 JWT 인지 — 헤더를 디코드 ===
|
||||
File "<string>", line 3
|
||||
h=open(/tmp/hdr.txt).read().strip()
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
|
||||
=== 저장된 바이트를 그대로 디코드한 결과 ===
|
||||
refresh_token 헤더 : {"alg":"HS512","typ" : "JWT","kid" : "e2e3d6d3-2742-4aab-b986-6d56d39095d1"}
|
||||
refresh_token 페이로드(앞부분):
|
||||
{"exp":1788500446,"iat":1788498646,"jti":"54096fa4-edc6-bf6d-a88c-d2a6138bc6ee","iss":"https://auth.hyeonworks.com/realms/keycloak-patterns"
|
||||
access_token 헤더 : {"alg":"RS256","typ" : "JWT","kid" : "OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM"}
|
||||
|
||||
→ bytea 에 들어 있는 것은 암호화된 덩어리가 아니라 JWT 문자열 그대로다.
|
||||
DB 읽기 권한만 있으면 그 자리에서 쓸 수 있는 토큰을 얻는다.
|
||||
@@ -0,0 +1,25 @@
|
||||
=== [현재] 같은 사용자의 항목 ===
|
||||
client_registration_id | principal_name | access_token_issued_at | at_md5
|
||||
------------------------+----------------+----------------------------+----------------------------------
|
||||
keycloak | labuser | 2026-09-04 05:10:46.927192 | 675af2286bfc2fd9d2bab7bc8f391df7
|
||||
(1 row)
|
||||
|
||||
행 수: 1
|
||||
|
||||
=== [모의 두 번째 브라우저] 세션만 지우고 같은 사용자로 다시 로그인시킨다 ===
|
||||
(브라우저가 달라도 principal 은 같으므로 조회 키가 같다)
|
||||
Redis 세션 삭제 완료 — 다음 요청이 새 로그인을 만든다
|
||||
=== [재로그인 후] 행이 늘었는가, 덮어써졌는가 ===
|
||||
client_registration_id | principal_name | access_token_issued_at | at_md5
|
||||
------------------------+----------------+----------------------------+----------------------------------
|
||||
keycloak | labuser | 2026-09-04 05:12:13.018828 | e19a63fc5aa18bd0a68b3e19dff16b3b
|
||||
(1 row)
|
||||
|
||||
행 수: 1
|
||||
|
||||
★ 행 수가 1 그대로이고 md5 가 바뀌었으면 → 덮어쓰기다
|
||||
|
||||
=== Q1 검증 ④ — 로그아웃하면 두 저장소가 다 정리되는가 ===
|
||||
로그아웃 전
|
||||
Redis: 1 키
|
||||
PostgreSQL: 1 행
|
||||
@@ -0,0 +1,14 @@
|
||||
=== Q1 검증 ④ — 로그아웃 후 두 저장소 상태 ===
|
||||
Redis 세션 : 0 키
|
||||
PostgreSQL 토큰 : 1 행
|
||||
|
||||
principal_name | access_token_issued_at | access_token_expires_at
|
||||
----------------+----------------------------+----------------------------
|
||||
labuser | 2026-09-04 05:12:13.018828 | 2026-09-04 05:13:13.018828
|
||||
(1 row)
|
||||
|
||||
|
||||
★ Redis 는 비었는데 PostgreSQL 에 행이 남아 있으면 → 한쪽만 정리된 것
|
||||
|
||||
=== Keycloak 쪽 SSO 세션은? ===
|
||||
Keycloak 온라인 세션: 2
|
||||
@@ -0,0 +1,21 @@
|
||||
# B-2 — 다중 인스턴스 운영 증거
|
||||
|
||||
2026-09-04 15:05–15:15 KST
|
||||
해설: [`docs/experiment-b2-multi-instance-session.md`](../../experiment-b2-multi-instance-session.md)
|
||||
|
||||
| 파일 | 무엇을 보여주는가 |
|
||||
|---|---|
|
||||
| `01-jdbc-store-deploy.txt` | JDBC 저장소로 배포. **테이블이 조용히 안 만들어졌다** |
|
||||
| `02-schema.txt` | 원인 — 기본 DDL 은 `blob`(PostgreSQL 에 없음), `-postgres.sql` 판본이 따로 있다. **`PRIMARY KEY (client_registration_id, principal_name)`** — 조회 키 문제가 DDL 에 박혀 있다 |
|
||||
| `03-plaintext-tokens.txt` | **Q3 검증 2번** — `bytea` 안이 JWT 문자열 그대로. 디코드하면 `{"alg":"HS512",...}` |
|
||||
| `04-overwrite-test.txt` | **Q1 검증 3번** — 같은 사용자 재로그인 시 행 수 1 그대로, `issued_at` 과 md5 만 바뀜 = **UPDATE(덮어쓰기)** |
|
||||
| `05-logout-cleanup.txt` | **Q1 검증 4번** — Redis 0키 / PostgreSQL **1행 잔존** / Keycloak SSO **2세션 잔존** |
|
||||
| `b2-before-relogin.png` | JDBC 전환 직후, 옛 세션은 여전히 `false` |
|
||||
| `b2-tokens-shared-across-instances.png` | 재로그인 후 **`accessTokenStoredOnServer: true`** — 두 replica 에서 동작 |
|
||||
|
||||
## 핵심 네 줄
|
||||
|
||||
1. **세션 Redis + 토큰 PostgreSQL 분리 저장이 성립한다.** B-1 의 "로그인은 됐는데 토큰이 없는" 상태가 해결됐다.
|
||||
2. **refresh token 은 평문이다.** DB 읽기 권한이면 작동하는 토큰을 얻는다.
|
||||
3. **같은 사용자의 두 번째 로그인이 첫 번째를 덮어쓴다.** 기본키에 session id 가 없어 구조적으로 그렇다.
|
||||
4. **로그아웃은 셋 중 하나만 지운다.** 평문 토큰과 Keycloak SSO 세션이 남는다.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Reference in New Issue
Block a user