세션 설계에서 먼저 정할 것은 JWT인지 opaque ID인지가 아니라 요청을 허용하는 현재 권위 상태를 어디서 확인할지입니다. 서명 검증은 토큰이 정책에 맞게 발급되고 변조되지 않았음을 확인하고, 현재 세션·계정 상태 조회는 회수와 권한 변경을 반영합니다. 이미 시작한 외부 효과는 자격을 거절하는 것만으로 취소되지 않으므로 새 요청 차단과 진행 중 작업 제어를 별도로 설계합니다.
로그아웃 토큰의 서명 유효성과 현재 상태
10시에 5분짜리 JWT를 발급하고 10시 1분에 계정을 정지했다고 합시다. 서비스가 서명과 exp만 검사하면 10시 5분까지 토큰을 허용할 수 있습니다. 서명은 발급 당시 주장에 대한 무결성 근거이지 계정의 현재 상태를 실시간으로 바꾸는 장치가 아닙니다.
서버 세션은 클라이언트에 의미 없는 무작위 ID를 주고 현재 사용자·만료·회수 상태를 서버에서 찾는 방식입니다. 그렇다고 항상 즉시 회수되는 것도 아닙니다. 각 서비스가 세션 조회 성공을 1분 캐시하면 그동안 오래된 판단을 사용할 수 있습니다. 비교해야 할 것은 JWT라는 형식과 세션이라는 이름보다 무엇을 어디서 얼마나 오래 신뢰하는가입니다.
자격 형식·보관·검증·인가
| 축 | 선택 예 | 자동으로 해결되지 않는 것 |
|---|---|---|
| 자격 표현 | opaque ID, 서명 JWT | 현재 계정 정지 |
| 브라우저 보관 | HttpOnly 쿠키, 메모리 bearer | XSS·CSRF 전체 방어 |
| 상태 확인 | 원본 조회, 캐시, 로컬 서명 | 자원별 소유권 인가 |
| 회수 단위 | 토큰, 기기 세션, 계정 | 기존 실행의 중단 |
서명된 JWT payload는 일반적으로 읽을 수 있으므로 민감한 원문을 넣지 않습니다. HttpOnly는 스크립트의 쿠키 값 읽기를 줄이지만 XSS가 같은 앱에서 요청을 수행하는 것을 막지는 않습니다. 쿠키의 자동 첨부에는 CSRF 방어가 필요합니다. 저장 위치와 토큰 형식을 같은 선택으로 묶지 않습니다.
서버 검증 정책과 토큰 입력
서버 설정에 용도별 허용 issuer·audience·알고리즘·키 집합·필수 claim을 둡니다. 헤더의 alg는 그 정책에 맞는지 확인할 입력이고, kid는 이미 신뢰한 키 집합에서 키를 찾는 힌트입니다. 임의 파일 경로·URL로 해석하지 않습니다. 공개키를 대칭 비밀키처럼 사용하는 알고리즘 혼동을 라이브러리 설정과 키 타입 검사로 막습니다.
policy = configured_policy_for_this_endpoint()
key = select_from_trusted_keys(token.kid, policy)
claims = verify_signature_and_allowed_algorithm(token, key, policy)
validate_exact_issuer_audience_purpose_and_time(claims, policy)
principal = validate_current_session_if_required(claims)
authorize(principal, server_loaded_resource, requested_action)
unverified claim으로 등록된 정책 후보를 찾을 수는 있지만 검증 전에는 주체나 권한으로 사용하지 않습니다. exp·nbf·발급 시각의 요구와 제한된 clock skew를 프로파일에 맞게 확인합니다. 같은 키로 서명한 다른 목적의 JWT도 용도·audience·필수 claim이 다르면 거절해야 합니다. 모르는 kid는 제한된 키 갱신 후 거절할 수 있지만 검증 생략으로 바꾸지는 않습니다.
기기별·계정별 회수 버전
그림을 글로 읽기
- 계정 A: security_version = 8
- 휴대전화 세션 S1: active · family F1
- 노트북 세션 S2: revoked · family F2
- API 현재 상태 검사: 계정 버전 + 세션 상태
- 계정 A → 휴대전화 세션 S1: 소속
- 계정 A → 노트북 세션 S2: 소속
- 휴대전화 세션 S1 → API 현재 상태 검사: 허용 후보
- 노트북 세션 S2 → API 현재 상태 검사: 회수 거절
각 access token에 account version과 session ID를 담는 설계라면, 버전이 현재값과 맞고 해당 세션이 활성인지 확인합니다. S2를 회수하면 F2의 갱신을 막고, 계정 버전을 올리면 모든 옛 버전 자격을 거절하는 정책이 가능합니다. 그러나 이 비교를 로컬 서명만 하는 서비스에는 적용할 수 없습니다. 회수 상태 조회를 어디서 할지 명시해야 합니다.
로그인·갱신과 계정 전체 회수가 동시에 일어나는 상황을 보겠습니다. 회수 작업이 현재 세션 목록을 읽은 뒤 로그인 요청이 S3를 만들면, 회수 작업이 목록에 있던 S1·S2만 하나씩 삭제하고 S3를 놓칠 수 있습니다. 따라서 회수 이후 옛 버전의 세션이 새로 생기지 않게 거래나 조건부 버전 갱신으로 두 경로를 직렬화해야 합니다. 계정 버전은 이 전체 범위의 경계가 될 수 있습니다.
캐시 TTL과 회수 지연 상한
원본에서 회수한 시점을 t0라고 하겠습니다. replica가 t0 뒤 20초 동안 옛 상태를 돌려주고 서비스가 그 마지막 응답을 60초 캐시하면, t0+20초에 생긴 마지막 오래된 응답이 t0+80초까지 캐시에 남을 수 있어 원본 변경 뒤 약 80초까지 오래된 판단이 이어질 수 있습니다. 이는 지연 상한의 예시일 뿐이며 replica lag가 실제로 무한히 늘면 같은 상한을 보장할 수 없습니다. push invalidation도 전달 유실과 재시작을 고려해 TTL·버전 확인과 조합합니다.
민감한 쓰기는 권위 상태를 확인한 뒤 최종 변경과 회수의 순서를 정합니다. 확인 직후 실행 전에 권한이 바뀌는 TOCTOU(확인 시점과 사용 시점 사이의 변경)까지 막으려면 같은 거래·버전 조건·실행 권한 검증 경계를 사용해야 합니다. 이미 실행 중인 송금이나 열린 스트림은 세션 레코드 삭제만으로 되돌릴 수 없으므로, 새 요청 차단과 진행 중 작업의 취소·종결을 별도 계약으로 정합니다.
상태 저장소 장애를 권한 없음과 구별해 관측하되 민감 변경을 무조건 허용하지 않습니다. 저위험 조회에 마지막 정상 상태를 제한적으로 허용하는 경우 원래 만료·최대 stale 기간·새 권한 부여 금지를 명시합니다. 무기한 fail-open은 회수 요구와 양립하지 않습니다.
회수 실험의 핵심 결과는 “토큰이 유효한가” 하나가 아니라 회수 커밋 시각부터 각 서비스가 마지막으로 허용한 요청까지의 간격입니다. S2만 회수한 뒤 S1이 계속 허용되는지, 계정 버전 상승 뒤 두 기기 모두 차단되는지, 이미 열린 작업이 별도 취소 정책을 따르는지를 각각 측정해야 범위와 지연을 설명할 수 있습니다.
회수 시각과 마지막 성공 시각 측정
테스트 계정의 두 기기와 두 서비스에서 cache hit·miss·replica 지연·장기 연결을 나눠 실행합니다. S2 회수 뒤 S1은 정책대로 유지되고 S2의 갱신과 API 사용은 각각 정한 시점에 거절되어야 합니다. 계정 전체 회수와 동시 갱신도 장벽으로 순서를 제어합니다.
서명이 변조된 토큰뿐 아니라 올바르게 서명된 다른 audience·용도 토큰도 넣습니다. 지표는 서명 성공률만이 아니라 회수 커밋 시각, 각 서비스의 마지막 허용 시각, 조회 장애 때 적용한 정책입니다. 이 노트는 검증 설계이며 실제 인증 서버에서 측정한 회수 지연을 제시하는 것은 아닙니다.