Auth 모듈
Auth 모듈은 Lumie 플랫폼의 인증/인가를 담당합니다. JWT 기반 인증, OAuth2 소셜 로그인, 사용자 관리, 토큰 관리를 제공합니다.
모듈 개요
- 배포: lumie-backend 모놀리스의
modules/auth - 데이터베이스: PostgreSQL RLS 테넌트 스코프
users,public.owner_directory(pre-auth OWNER 라우팅), Redis (토큰 저장) - 주요 의존성:
TenantService(internal-api)
주요 기능
1. 인증 (Authentication)
- JWT 기반 액세스/리프레시 토큰
- 사용자 등록 및 로그인
- OAuth2 소셜 로그인 (Google, Kakao, Keycloak)
- 원장 등록 (테넌트 생성 포함)
2. 인가 (Authorization)
- Spring Security 필터 체인 기반 Role 접근 제어
- 테넌트별 사용자 격리
JwtAuthenticationFilter— 모든 요청에서 JWT를 추출하고SecurityContext설정
3. 토큰 관리
- Redis 기반 리프레시 토큰 저장
- 토큰 블랙리스트 관리 (JTI 기반)
- 쿠키 폴백 (
lumie_access_token)
4. 사용자 관리
- RLS 테넌트 스코프
users테이블 관리 public.owner_directory를 통한 로그인 전 OWNER → 테넌트 라우팅- 사용자 생성/삭제 (
AuthService— 다른 모듈이 호출)
헥사고날 아키텍처
modules/auth/src/main/java/com/lumie/auth/
├── adapter/
│ ├── in/web/ # REST 컨트롤러 (AuthController, OAuth2Controller)
│ ├── in/security/ # JwtAuthenticationFilter, JwtTokenProvider
│ ├── in/internal/ # AuthService 구현체 (StaffServiceAdapter 등 타 모듈 호출용)
│ └── out/{persistence,external,config}/
├── application/
│ ├── service/ # AuthCommandService, TokenService, OAuth2Service
│ └── port/out/ # LoadUserPort, SaveUserPort, TokenStorePort
└── domain/
├── entity/User.java
└── vo/TokenClaims.java
JwtAuthenticationFilter
모든 보호된 요청에 실행되는 필터입니다. app 모듈의 SecurityConfig에 등록됩니다.
// modules/auth/.../adapter/in/security/JwtAuthenticationFilter.java
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final ValidateTokenUseCase validateTokenUseCase;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null) {
Optional<TokenClaims> claimsOpt = validateTokenUseCase.validate(token);
if (claimsOpt.isPresent()) {
TokenClaims claims = claimsOpt.get();
AuthenticatedUser principal = new AuthenticatedUser(
claims.getUserId(), claims.tenantSlug(),
claims.tenantId(), claims.role());
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
principal, null,
List.of(new SimpleGrantedAuthority("ROLE_" + claims.role().name()))));
// TenantContextHolder / UserContextHolder 설정
TenantContextHolder.setTenant(claims.tenantSlug());
TenantContextHolder.setTenantId(claims.tenantId());
UserContextHolder.setUserId(claims.getUserId());
UserContextHolder.setUserRole(claims.role().name());
}
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
// 1. Authorization: Bearer <token> 헤더
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
// 2. lumie_access_token 쿠키 폴백
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("lumie_access_token".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
return path.startsWith("/v1/login") ||
path.startsWith("/v1/register") ||
path.startsWith("/v1/refresh") ||
path.startsWith("/v1/oauth2") ||
path.startsWith("/actuator");
}
}
인증 플로우
일반 로그인
OAuth2 소셜 로그인
원장 등록 플로우
API 명세
사용자 등록
POST /v1/register
Content-Type: application/json
{
"tenantSlug": "awesome-academy",
"userLoginId": "student001",
"password": "password123",
"name": "김학생",
"phone": "010-1234-5678"
}
로그인
POST /v1/login
Content-Type: application/json
{
"userLoginId": "student001",
"password": "password123"
}
응답:
{
"accessExpiresIn": 3600,
"refreshExpiresIn": 604800,
"user": {
"id": 1,
"userLoginId": "student001",
"name": "김학생",
"role": "STUDENT",
"tenantSlug": "awesome-academy"
}
}
원장 등록
POST /v1/register/owner
Content-Type: application/json
{
"instituteName": "어썸 학원",
"businessRegistrationNumber": "123-45-67890",
"userLoginId": "owner@awesome.com",
"password": "password123",
"name": "김원장"
}
OAuth2 인증
# 인증 URL 생성
GET /v1/oauth2/google/authorize?tenant_slug=awesome-academy
# 콜백 처리
GET /v1/oauth2/google/callback?code=xxx&state=yyy
토큰 갱신
POST /v1/refresh
Cookie: lumie_refresh_token=eyJ...
로그아웃
POST /v1/logout
Authorization: Bearer eyJ...
internal-api
AuthService 인터페이스를 구현하여 다른 모듈이 사용자 정보를 조회하거나 계정을 생성/삭제할 수 있도록 합니다.
// libs/internal-api/src/main/java/com/lumie/auth/api/AuthService.java
public interface AuthService {
TokenValidationResult validateToken(String token);
Optional<UserData> getUserInfo(String token);
CreateUserResult createUser(String name, String phone, String role, Long tenantId);
DeleteUserResult deleteUser(Long userId);
ResetPasswordResult resetPassword(Long userId);
ChangeLoginIdResult changeLoginId(Long userId, String newLoginId);
Map<Long, String> getAvatarSeeds(Collection<Long> userIds);
}
JWT 토큰 구조
{
"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시간 만료, Stateless
- 리프레시 토큰: 7일 만료, Redis 저장
- 토큰 블랙리스트: 로그아웃 시 JTI를 Redis에 저장
Redis 토큰 저장 구조
refresh_token:{userId}:{jti} → {tokenData} (TTL: 7일)
blacklist:{jti} → "1" (TTL: 액세스 토큰 만료까지)
보안 설계
비밀번호 보안
- BCrypt 해싱 (
BCryptPasswordEncoder) - 최소 8자, 복잡성 정책 적용
OAuth2 보안
- State 파라미터로 CSRF 방지
- 필요한 scope만 요청 (최소 권한 원칙)