본문으로 건너뛰기

인증과 테넌시

인증과 테넌시는 의도적으로 결합되어 있습니다. Lumie에서는 대부분의 유의미한 백엔드 작업이 인증된 호출자와 PostgreSQL RLS에 바인딩할 수 있는 tenant ID를 둘 다 필요로 합니다. tenant 범위가 없는 identity만으로는 일반적인 read와 write를 수행할 수 없습니다.

이 페이지는 요청 context 계약을 다루는 reference 문서입니다.

소스 경로

Path역할
app/src/main/java/com/lumie/app/config/SecurityConfig.java전역 route 보호와 filter 순서
modules/auth/src/main/java/com/lumie/auth/adapter/in/security/JwtAuthenticationFilter.javaend-user authentication filter
modules/auth/src/main/java/com/lumie/auth/adapter/out/security/JwtTokenProvider.javaJWT claim 형식과 발급
modules/auth/src/main/java/com/lumie/auth/adapter/in/web/AuthController.javaregistration, login, refresh, logout, profile endpoint
app/src/main/java/com/lumie/app/config/internal/InternalHmacAuthFilter.java/internal/** authentication 계약
libs/common/src/main/java/com/lumie/common/tenant/RequestContextFilter.javarequest correlation, MDC, fallback header population
modules/tenant/src/main/java/com/lumie/tenant/adapter/in/web/TenantController.javaanonymous public tenant lookup
modules/homepage/src/main/java/com/lumie/homepage/application/service/HomepageQueryService.javatenant 해석 이후의 public homepage lookup

요청 컨텍스트 흐름

최종 사용자 인증 계약

주요 HTTP 인터페이스

FlowEndpointsNotes
Registration and loginPOST /v1/register, POST /v1/register/owner, POST /v1/loginaccess token, refresh token, user payload를 발급
Session lifecyclePOST /v1/refresh, POST /v1/logout, POST /v1/logout-all, GET/DELETE /v1/me/sessions...refresh는 cookie에서 refresh token을 읽음
ProfileGET/PATCH /v1/me, POST /v1/me/password, PATCH /v1/me/avatar인증된 user context 필요
OAuth2GET /v1/oauth2/{provider}/authorize, GET /v1/oauth2/{provider}/callbackGoogle, Kakao, Naver

JWT에 담기는 내용

JwtTokenProvider는 access token과 refresh token 모두에 다음과 같은 핵심 claim을 담아 발급합니다.

Claim의미
sub사용자 ID
name표시 이름
tenant_slug제품 측면 tenant 식별자
tenant_idRLS에 사용되는 데이터베이스 측 tenant 식별자
roleRole에서 가져오는 거친 수준의 역할
sidaccess token과 refresh token 쌍이 공유하는 session 식별자
jtitoken 식별자
typeaccess 또는 refresh

JWT filter는 다음 둘 중 하나를 받아들입니다.

  • Authorization: Bearer <token>
  • lumie_access_token cookie

성공 시 다음을 설정합니다.

  • SecurityContextHolder
  • slug와 ID를 모두 담은 TenantContextHolder
  • user ID, name, role, session ID를 담은 UserContextHolder

쿠키 계약

CookieUtils는 다음을 발급합니다.

  • lumie_access_token
  • lumie_refresh_token

application.yamlCookieConfig 기준 기본값:

  • HttpOnly=true
  • Secure=true
  • 기본 SameSite=Lax
  • dev override: application-dev.ymlcookie.sameSite=None

내부 인증 계약

/internal/** route는 user JWT가 아니라 InternalHmacAuthFilter로 보호됩니다.

필수 header:

  • X-Tenant-Slug
  • X-Timestamp
  • X-Signature

서명 공식:

HMAC-SHA256(timestamp + "\n" + tenantSlug + "\n" + body)

필터에서 강제하는 다른 규칙:

  • 최대 timestamp skew: 300
  • 최대 buffered body size: 1 MiB
  • tenant는 존재하고 활성 상태여야 함
  • 검증에 성공하면 synthetic ROLE_INTERNAL을 부여

이 계약은 내부 chatbot callback과 worker 대상 내부 HTTP surface에서 사용됩니다.

공개 테넌트 확인 경로

일부 route는 주변 tenant context 없이 시작하고 tenant를 먼저 발견합니다.

  • GET /v1/tenants/public/by-custom-id/{customId}
  • GET /v1/tenants/public/by-domain?host=...
  • GET /v1/homepage/public/by-custom-id/{customId}

homepage 경로가 가장 중요한 경계 예시입니다.

  1. HomepageQueryService.getPublicByCustomId(...)가 tenant 모듈에 customId -> {slug, tenantId} 조회를 요청합니다.
  2. TenantContextHolder.withinContext(...)로 두 값을 모두 복원합니다.
  3. 트랜잭션 경계를 만들기 위해 proxy된 내부 bean HomepageQueryService.Tx로 진입합니다.
  4. RlsTenantContextAspectapp.tenant_id를 바인딩하고, 그제야 read가 데이터베이스에서 보이게 됩니다.

tenant ID와 proxy된 트랜잭션이 없으면 homepage row는 RLS 아래에서 계속 보이지 않습니다.

라우트 보호 요약

SecurityConfig는 다음과 같은 주요 비인증 surface를 허용합니다.

  • registration, login, refresh, OAuth2 callback
  • actuator/**
  • /v3/api-docs/**, /swagger-ui/**, /swagger-ui.html
  • public tenant, homepage, file route
  • /internal/**, 단 HMAC filter가 ROLE_INTERNAL을 부여한 이후에만 허용

그 외 모든 것은 인증된 user context가 필요합니다.

다른 모듈로 이어지는 흐름

  • auth의 OwnerRegisteredEvent는 staff가 OWNER staff record를 bootstrap하는 데 소비합니다.
  • StudentSelfRegisteredEvent는 별도의 student self-registration 경로를 시작합니다.
  • owner login과 refresh는 일반적인 요청 시점 tenant context가 존재하기 전에 tenant state를 해석할 수 있으며, 그래서 auth가 tenant lookup data에 의존합니다.

실패 모드와 드리프트

  • tenantSlug만 있고 tenantId가 없으면 RLS가 적용되는 데이터에 접근할 수 없습니다.
  • 알 수 없거나 비활성 tenant는 /internal/** 요청이 컨트롤러 코드에 도달하기 전에 실패하게 만듭니다.
  • 표준 dev 환경이 frontend-local, backend-cluster 구조이므로 dev cookie 동작은 prod와 의도적으로 다릅니다.
  • homepage public-read 경로에는 contract drift가 있습니다. 컨트롤러 주석은 unpublished homepage가 404를 반환해야 한다고 말하지만, HomepageQueryService.Tx.findCurrent()와 그 테스트는 현재 published=false를 포함한 저장된 config를 그대로 반환합니다.

검증 명령어

cd /Users/bluemayne/Projects/Lumie/lumie-backend
./gradlew test
./gradlew :modules:auth:test
./gradlew :modules:homepage:test
./gradlew :libs:common:test

유용한 테스트:

  • libs/common/src/test/java/com/lumie/common/tenant/RequestContextFilterTest.java
  • modules/homepage/src/test/java/com/lumie/homepage/application/service/HomepageQueryServiceTest.java

관련 페이지