,

[로컬 서버 운영기] 한 번 로그인으로 다 쓰기 — 커스텀 JWT에서 Keycloak으로, 무중단으로

이 글의 위치: 기술 블로그 → 백엔드 · 인프라. 작성·검증 방식은 Editorial Policy 참조.
집 PC 한 대로 여러 서비스를 굴리는 로컬 서버 운영기 2편입니다. 서비스를 터널로 공개한 뒤, 흩어진 로그인을 하나로 합친 이야기입니다. 로그인 뒤의 권한 나누기는 3편 역할 기반 접근제어로 이어집니다.


"SSO처럼 보이지만 사실은 비밀키 하나"

제 서비스들은 처음엔 모두 같은 방식으로 로그인을 검증했습니다 — HS256 JWT. 로그인 서비스가 토큰을 발급하면, 다른 서비스들은 똑같은 비밀키(JWT_SECRET)로 서명을 검증합니다. 키가 같으니 한 서비스에서 받은 토큰이 다른 서비스에서도 통했습니다. SSO처럼 보였습니다.

문제는 이게 SSO의 결과만 흉내 낸다는 것입니다. 사용자·권한을 중앙에서 관리할 곳이 없고, 비밀키를 회전하려면 모든 서비스를 동시에 바꿔야 하고, 표준 로그인 화면·비밀번호 정책·소셜 로그인 같은 건 직접 만들어야 합니다. 그래서 중앙 인증 서버(Keycloak)로 옮기기로 했습니다. 단, 지금 잘 도는 로그인을 멈추지 않고 말입니다.


두 서명 방식의 근본 차이

HS256과 Keycloak(RS256)의 차이는 "누가 무엇으로 검증하느냐"에 있습니다.

  • HS256(대칭키): 발급자와 검증자가 같은 비밀키를 공유합니다. 검증하려면 비밀을 알아야 하고, 그 비밀을 아는 쪽은 위조도 가능합니다.
  • RS256(비대칭키): 발급자는 개인키로 서명하고, 검증자는 공개키(JWKS)로 검증합니다. 검증자는 공개키만 있으면 되고, 위조는 불가능합니다. 공개키는 Keycloak이 /protocol/openid-connect/certs(JWKS 엔드포인트)로 공개합니다.
항목 공유키 HS256(커스텀) Keycloak OIDC(RS256/JWKS)
키 형태 대칭(비밀 공유) 비대칭(개인키 서명 / 공개키 검증)
검증자가 아는 것 비밀키(=위조 가능) 공개키만(위조 불가)
사용자 저장소 각 서비스 DB Keycloak(realm) 중앙
로그인 화면 직접 구현 Keycloak 표준 제공
키 회전 전 서비스 동시 교체 JWKS 자동 배포(kid로 구분)
발급자(iss) 임의 문자열 realm URL(검증에 사용)
로그인 흐름 폼 → 토큰 리다이렉트(Authorization Code + PKCE)

정리하면, HS256은 "비밀을 나눠 갖는" 모델이고 Keycloak은 "공개키로 검증하는 표준(OIDC)" 모델입니다.


핵심 아이디어: dual-verify (신·구 토큰 동시 수용)

여기서 무중단의 열쇠가 나옵니다. 한 번에 전부 바꾸면 실패 시 전 서비스가 로그인 불능이 됩니다. 그래서 백엔드가 두 종류의 토큰을 모두 받게 했습니다. JWT 헤더의 alg 필드로 분기합니다.

Bearer 토큰 도착
   │  헤더의 alg 는?
   ├─ HS256 → 기존 공유키로 검증 (그대로)
   └─ RS256 → Keycloak JWKS(공개키)로 검증 (신규)
헤더의 alg로 분기하는 dual-verify 흐름
그림 1. 백엔드가 헤더의 alg로 분기해 옛(HS256)·새(RS256) 토큰을 함께 받는 dual-verify.

Go 표준 라이브러리만으로 구현했습니다. JWKS에서 받은 공개키(모듈러스 n, 지수 e)로 RSA 공개키를 만들어 rsa.VerifyPKCS1v15로 서명을 확인하고, 발급자(iss)와 만료(exp)를 검사한 뒤, 역할(role)은 Keycloak 토큰의 realm_access.roles 배열에서 뽑습니다.

// alg 로 분기 — 한 함수가 두 방식을 모두 처리
if header.Alg == "RS256" {
    return validateKeycloakToken(token, cfg, header) // JWKS 검증
}
if header.Alg != "HS256" {
    return errUnsupported
}
// ... 이하 기존 HS256 검증 그대로

이 한 조각 덕분에, 프런트를 Keycloak 로그인으로 바꿔도 기존 계정으로 발급된 HS256 토큰이 그대로 통했습니다. 백엔드와 프런트를 따로, 순서대로 옮길 수 있었습니다.


실제 전환 순서와 관찰

  1. Keycloak을 옆에 띄우고(기존 인증은 그대로) realm·클라이언트·역할을 만듭니다.
  2. 백엔드에 dual-verify를 넣습니다 → 이 시점부터 두 토큰 모두 받습니다.
  3. 프런트 로그인을 Keycloak 리다이렉트(Authorization Code + PKCE)로 교체합니다.
  4. 나머지 서비스도 순차적으로 dual-verify → 최종적으로 HS256 제거.
Keycloak 관리 콘솔의 realm 사용자 목록
그림. 커스텀 계정에서 옮겨온 Keycloak realm 사용자 — admin·user 역할 계정이 한곳에서 관리됩니다.
Keycloak 관리 콘솔의 realm 클라이언트 목록
그림. 같은 realm의 OIDC 클라이언트 — 프런트·관리 콘솔이 표준 OpenID Connect로 등록됩니다.
Keycloak Admin — realm의 Clients 목록 (OpenID Connect)
Keycloak Admin 콘솔 — realm의 Clients(OpenID Connect). 각 서비스가 Keycloak 클라이언트로 등록된다 (realm·도메인은 example로 익명화)

전환이 실제로 무중단인지는 역시 응답 코드로 확인했습니다. 같은 관리 API를 여러 종류의 토큰으로 두드려 봤습니다.

토큰 관리 API 응답
기존 HS256(옛 로그인) 200 (그대로 통함)
Keycloak RS256, 관리자 200 (새로 수용)
Keycloak RS256, 일반 사용자 403 (역할 경계)
위조 토큰 401

옛 토큰 200이 유지된다는 것이 곧 "기존 사용자가 안 끊겼다"는 증거였습니다. 그리고 일반 사용자 403은 다음 편의 주제 — 역할 기반 접근제어로 이어집니다.

한 가지 현실적인 걸림돌이 있었습니다. 기존 사용자의 비밀번호는 bcrypt 해시로 저장돼 있는데, Keycloak은 bcrypt를 기본 지원하지 않아 비밀번호를 무손실로 그대로 옮기기가 어렵습니다. 그래서 기존 계정은 "기존 계정으로 로그인" 폴백(옛 HS256 경로)을 남겨, 원래 비밀번호를 그대로 쓰게 했습니다. 백엔드가 두 토큰을 다 받으니 가능한 선택이었습니다. (이 사용자 계정을 Keycloak으로 온전히 옮기는 건 마지막 편의 향후 계획에서 다룹니다.)


정리

  • 공유키 HS256은 SSO의 결과만 흉내 냅니다 — 중앙 관리·키 회전·표준 로그인이 없습니다.
  • Keycloak(OIDC/RS256)은 공개키로 검증하는 표준 모델입니다. 검증자는 공개키(JWKS)만 있으면 되고 위조가 불가능합니다.
  • 무중단의 핵심은 dual-verify — 백엔드가 alg로 분기해 옛·새 토큰을 동시에 받게 하면, 프런트·백엔드를 따로 옮길 수 있습니다.
  • 검증은 "됐겠지"가 아니라 신·구·위조 토큰으로 응답 코드를 직접 확인합니다.

다음 편에서는 로그인 뒤의 문제 — 관리자와 사용자를 어떻게 가르는가를 다룹니다. UI에서 탭을 숨기는 것과 백엔드가 403으로 막는 것은 전혀 다른 층위라는 이야기입니다: 3편 역할 기반 접근제어.


참고

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다