Skip to main content

인증 & 멀티테넌시

JWT 인증 플로우

토큰 추출 순서

JwtAuthenticationFilter는 다음 순서로 JWT를 찾습니다:

  1. Authorization: Bearer <token> 헤더
  2. lumie_access_token 쿠키 (폴백)

JWT Claims 구조

{
"sub": "28",
"tenant_slug": "inst-c704d223",
"tenant_id": 18,
"role": "OWNER",
"type": "access",
"iss": "lumie-auth-svc",
"iat": 1775313347,
"exp": 1775316947,
"jti": "a022ead2-a80e-4091-82ea-1023b9c51560"
}

토큰 검증 단계

  1. 서명 확인: HMAC-SHA256 (JWT_SECRET_KEY)
  2. 만료 확인: exp claim
  3. 블랙리스트 확인: Redis에서 JTI 조회 (로그아웃된 토큰 차단)
  4. 컨텍스트 설정: TenantContextHolder.setTenant + setTenantId, UserContextHolder, SecurityContext

중요: setTenant(slug)만 설정하면 RLS 정책이 행을 숨깁니다. setTenantId도 반드시 호출해야 합니다.

Redis 토큰 저장 구조

refresh_token:{userId}:{jti} → {tokenData} (TTL: 7일)
blacklist:{jti} → "1" (TTL: 액세스 토큰 만료까지)

Spring Security 설정

// app/src/main/java/com/lumie/app/config/SecurityConfig.java
http
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v1/register", "/v1/register/owner").permitAll()
.requestMatchers("/v1/login", "/v1/refresh").permitAll()
.requestMatchers("/v1/oauth2/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()
.requestMatchers("/internal/**").hasRole("INTERNAL")
.requestMatchers("/v1/payments/paddle/webhook").permitAll()
.requestMatchers("/v1/tenants/public/**").permitAll()
.requestMatchers("/v1/homepage/public/**").permitAll()
.requestMatchers("/v1/files/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(internalHmacAuthFilter, JwtAuthenticationFilter.class);

내부 엔드포인트 인증 (HMAC)

/internal/** 경로는 InternalHmacAuthFilter가 먼저 처리합니다. lumie-worker(chatbot-svc 등)가 X-Tenant-Slug, X-Timestamp, X-Signature 헤더로 HMAC-SHA256 서명을 제출해야 합니다. 서명 페이로드: timestamp + "\n" + tenantSlug + "\n" + body. 검증 성공 시 ROLE_INTERNAL 권한이 부여됩니다.

CORS 설정

// CorsConfig.java
config.setAllowedOrigins(List.of(
"https://lumie-edu.com",
"https://dev.lumie-edu.com",
"http://localhost:3000"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);

멀티테넌시

RLS 기반 격리

Lumie는 단일 public 스키마를 사용합니다. 테넌트별 스키마를 분리하지 않습니다. 각 테넌트 스코프 테이블에는 tenant_id BIGINT NOT NULL 컬럼과 RLS 정책이 적용됩니다.

테이블 유형격리 방식
플랫폼 테이블 (tenants, plans, ...)RLS 없음
테넌트 스코프 테이블 (students, exams, ...)RLS (app.tenant_id GUC 기반)

컨텍스트 전파

JwtAuthenticationFilter
→ TenantContextHolder.setTenant("inst-c704d223")
→ TenantContextHolder.setTenantId(18)
→ UserContextHolder.setUserId(28)
→ UserContextHolder.setUserRole("OWNER")

@Transactional 진입 → RlsTenantContextAspect
→ SELECT set_config('app.tenant_id', '18', true)

JPA 쿼리 → RLS 자동 필터

TenantContextHolder

// 현재 테넌트 가져오기
String slug = TenantContextHolder.getRequiredTenant(); // "inst-c704d223"
Long tenantId = TenantContextHolder.getRequiredTenantId(); // 18

// 현재 사용자 가져오기
Long userId = UserContextHolder.getRequiredUserId(); // 28
String role = UserContextHolder.getUserRole(); // "OWNER"

OAuth2 지원

auth 모듈은 2개의 OAuth2 프로바이더를 지원합니다:

프로바이더용도
Google소셜 로그인
Kakao소셜 로그인

플로우: 클라이언트 → /v1/oauth2/\{provider\} → 프로바이더 인증 → 콜백 → JWT 발급


관련 문서