본문으로 건너뛰기

인증과 테넌시

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

이 페이지는 요청 컨텍스트 계약을 다루는 레퍼런스 문서입니다.

소스 경로

경로역할
app/src/main/java/com/lumie/app/config/SecurityConfig.java전역 라우트 보호와 필터 순서
modules/auth/src/main/java/com/lumie/auth/adapter/in/security/JwtAuthenticationFilter.java최종 사용자 인증 필터
modules/auth/src/main/java/com/lumie/auth/adapter/out/security/JwtTokenProvider.javaJWT 클레임 형식과 발급
modules/auth/src/main/java/com/lumie/auth/adapter/in/web/AuthController.java등록, 로그인, 갱신, 로그아웃, 프로필 엔드포인트
modules/auth/src/main/java/com/lumie/auth/adapter/in/web/CookieUtils.java공통 access/refresh 쿠키 계약
app/src/main/java/com/lumie/app/config/internal/InternalHmacAuthFilter.java/internal/** 인증 계약
app/src/main/resources/{application.yaml,application-dev.yml}auth 쿠키 기본값과 dev 전용 SameSite 재정의
libs/common/src/main/java/com/lumie/common/tenant/RequestContextFilter.java요청 상관관계, MDC, 대체 헤더 채움
modules/tenant/src/main/java/com/lumie/tenant/adapter/in/web/TenantController.java익명 공개 테넌트 조회
modules/homepage/src/main/java/com/lumie/homepage/application/service/HomepageQueryService.java테넌트 해석 이후의 공개 홈페이지 조회
app/src/main/resources/db/migration/public/{V2__create_users_table,V4__federated_identities,V18__rls_baseline}.sql업그레이드된 데이터베이스에 남아 있을 수 있는 과거 auth/federated-login 스키마 아티팩트

요청 컨텍스트 흐름

최종 사용자 인증 계약

주요 HTTP 인터페이스

흐름엔드포인트비고
등록과 로그인POST /v1/register, POST /v1/register/owner, POST /v1/loginaccess 토큰, refresh 토큰, 사용자 payload를 발급
세션 수명주기POST /v1/refresh, POST /v1/logout, POST /v1/logout-all, GET/DELETE /v1/me/sessions...refresh는 쿠키에서 refresh 토큰을 읽음
프로필GET/PATCH /v1/me, POST /v1/me/password, PATCH /v1/me/avatar인증된 사용자 컨텍스트 필요

AuthControllerGET /v1/oauth2/{provider}/... route를 노출하지 않습니다. Kakao, Google, Naver 로그인은 지원되는 런타임 요청 컨텍스트 계약 밖에 있습니다.

JWT에 담기는 내용

JwtTokenProvider는 access 토큰과 refresh 토큰 모두에 다음과 같은 핵심 클레임을 담아 발급합니다.

클레임의미
sub사용자 ID
name표시 이름
tenant_slug제품 측면 테넌트 식별자
tenant_idRLS에 사용되는 데이터베이스 측 테넌트 식별자
roleRole에서 가져오는 거친 수준의 역할
sidaccess 토큰과 refresh 토큰 쌍이 공유하는 세션 식별자
jti토큰 식별자
typeaccess 또는 refresh

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

  • Authorization: Bearer <token>
  • lumie_access_token 쿠키

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

  • SecurityContextHolder
  • slug와 ID를 모두 담은 TenantContextHolder
  • user ID, name, 역할, 세션 ID를 담은 UserContextHolder

쿠키 계약

CookieUtils는 다음을 발급합니다.

  • lumie_access_token
  • lumie_refresh_token

application.yamlCookieConfig 기준 기본값:

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

브라우저 세션 계약은 등록, 로그인, refresh, 인증된 프로필 조회 전반에서 동일합니다. 지원되는 런타임 플로우에는 제공자 전용 세션 저장소가 따로 없습니다.

내부 인증 계약

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

필수 header:

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

서명 공식:

Source anchor: lumie-backend/app/src/main/java/com/lumie/app/config/internal/InternalHmacAuthFilter.java#computeSignature

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

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

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

이 계약은 내부 chatbot callback과 워커 대상 내부 HTTP 인터페이스에서 사용됩니다.

공개 테넌트 확인 경로

일부 route는 주변 테넌트 컨텍스트 없이 시작하고 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(...)가 테넌트 모듈에 customId -> {slug, tenantId} 조회를 요청합니다.
  2. TenantContextHolder.withinContext(...)로 두 값을 모두 복원합니다.
  3. 트랜잭션 경계를 만들기 위해 프록시된 내부 빈 HomepageQueryService.Tx로 진입합니다.
  4. RlsTenantContextAspectapp.tenant_id를 바인딩하고, 그제야 read가 데이터베이스에서 보이게 됩니다.

테넌트 ID와 프록시된 트랜잭션이 없으면 홈페이지 행은 RLS 아래에서 계속 보이지 않습니다.

라우트 보호 요약

SecurityConfig는 다음과 같은 주요 비인증 인터페이스를 허용합니다.

  • 등록, 로그인, refresh
  • actuator/**
  • /v3/api-docs/**, /swagger-ui/**, /swagger-ui.html
  • 공개 테넌트, 홈페이지, 파일 라우트
  • /internal/**, 단 HMAC 필터가 ROLE_INTERNAL을 부여한 이후에만 허용

그 외 모든 것은 인증된 사용자 컨텍스트가 필요합니다.

다른 모듈로 이어지는 흐름

  • auth의 OwnerRegisteredEvent는 staff가 OWNER staff record를 부트스트랩하는 데 소비합니다.
  • StudentSelfRegisteredEvent는 별도의 student self-등록 경로를 시작합니다.
  • OWNER 로그인과 refresh는 일반적인 요청 시점 테넌트 컨텍스트가 존재하기 전에 테넌트 상태를 해석할 수 있으며, 그래서 auth가 테넌트 조회 데이터에 의존합니다.

실패 모드와 드리프트

  • tenantSlug만 있고 tenantId가 없으면 RLS가 적용되는 데이터에 접근할 수 없습니다.
  • 적용된 데이터베이스에는 federated_identities나 과거 users.oauth_provider 컬럼 같은 역사적 federated-login 스키마가 남아 있을 수 있습니다. 이 아티팩트는 지원되는 런타임 로그인 계약의 일부가 아니며, 활성 요청 컨텍스트 의존성으로 취급하면 안 됩니다.
  • 런타임은 이런 역사적 아티팩트를 더 이상 읽거나 쓰지 않습니다. 코드 제거 배포 중에는 그대로 두고, 비밀번호 기반 로그인과 refresh 경로가 정상인지 배포 검증이 끝난 뒤 나중의 forward-only contract migration으로 보관 또는 삭제해야 합니다.
  • 알 수 없거나 비활성 tenant는 /internal/** 요청이 컨트롤러 코드에 도달하기 전에 실패하게 만듭니다.
  • dev 쿠키 동작이 prod와 다른 것은 의도된 차이이며, 표준 dev 설정이 프론트엔드 로컬 + 백엔드 클러스터이기 때문입니다.
  • homepage public-read 경로에는 여전히 contract drift가 있습니다. 컨트롤러 comment는 unpublished homepage가 404를 반환해야 한다고 말하지만, HomepageQueryService.Tx.findCurrent()와 관련 테스트는 현재 published=false인 저장된 config도 반환합니다.

검증 명령어

cd lumie-backend
./gradlew :modules:auth:test
./gradlew :modules:auth:test --tests '*AuthControllerMeTest'

유용한 테스트:

  • modules/auth/src/test/java/com/lumie/auth/adapter/in/web/AuthControllerMeTest.java
  • libs/common/src/test/java/com/lumie/common/tenant/RequestContextFilterTest.java

관련 페이지