Skip to content

인증 인가 전략

Cupcakes33 edited this page Feb 17, 2026 · 3 revisions

인증/인가 전략

프로젝트명: MARU
작성일: 2026-01-19
최종 수정일: 2026-01-22

수정 이력
수정일 수정 내용
2026-01-19 권한 전략 초안 작성
2026-01-21 실제 구현 기반 전면 개정
2026-01-22 AOP 기반 권한 검증 상세화, 캐싱 아키텍처 추가

전체 인증 및 인가 아키텍처

모든 API 요청은 4단계 인증/인가 파이프라인을 거칩니다.

flowchart TD
    Request([HTTP Request<br/>Cookie: accessToken]) --> Step1

    subgraph Step1 ["1단계: JWT Filter"]
        A1[토큰 검증]
        A2[TenantContextHolder 설정]
        A3[SecurityContext 설정]
        A1 --> A2 --> A3
    end

    Step1 --> Step2

    subgraph Step2 ["2단계: Dojang Access AOP"]
        B1[테넌트/도장 활성 확인]
        B2[OWNER → 즉시 통과]
        B3[INSTRUCTOR → 소속 확인]
    end

    Step2 --> Step3

    subgraph Step3 ["3단계: Permission Check AOP"]
        C1[OWNER/SYSTEM → 즉시 통과]
        C2[INSTRUCTOR → 권한 확인]
    end

    Step3 --> Step4

    subgraph Step4 ["4단계: Query Filtering"]
        D1["WHERE tenant_id = ?<br/>AND dojang_id = ?"]
    end

    Step4 --> Logic([비즈니스 로직 실행])

    style Request fill:#f9f9f9,stroke:#333
    style Logic fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style Step1 fill:#fff3e0,stroke:#ff9800
    style Step2 fill:#f1f8e9,stroke:#4caf50
    style Step3 fill:#e8eaf6,stroke:#3f51b5
    style Step4 fill:#fce4ec,stroke:#e91e63
Loading

1. 멀티테넌시 및 컨텍스트 관리

MARU는 멀티테넌시 환경에서 데이터 격리와 보안을 보장하기 위해 ThreadLocal 기반의 컨텍스트를 활용합니다.

1.1 Tenant & Dojang 구조

MARU의 멀티테넌시는 사업자(Tenant)물리적 지점(Dojang) 으로 계층화되어 있습니다.

계층 설명 역할
Tenant 관장(Owner) 소유의 최상위 조직 데이터 격리의 기준
Dojang 실제 수련이 이루어지는 단위 권한 및 운영의 기준

1.2 TenantContextHolder

요청이 들어오는 시점부터 나가는 시점까지, 현재 사용자의 정보를 공유하는 ThreadLocal 기반 저장소입니다.

주요 메서드:

  • getTenantId(), getUserId(), getDojangId(), getRole() - 컨텍스트 조회
  • isOwner() - 관장 여부 확인
  • withSystemContext() - 배치/스케줄러용 시스템 컨텍스트

1.3 JwtAuthenticationFilter

flowchart TD
    A[HTTP Request] --> B{Cookie에<br/>토큰 존재?}

    B -- No --> C[인증 없이<br/>다음 필터로]
    B -- Yes --> D{토큰 유효성 검증}

    D -- 만료됨 --> E[TOKEN_EXPIRED<br/>에러 응답]
    D -- 변조됨 --> F[인증 실패<br/>Context 비움]
    D -- 유효함 --> G[JwtClaims 파싱]

    G --> H[TenantContextHolder 설정]
    H --> I[SecurityContext 설정]
    I --> J[다음 필터 / Controller]

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style J fill:#bbf,stroke:#333,stroke-width:2px
    style E fill:#ffebee,stroke:#c62828
    style F fill:#ffebee,stroke:#c62828
Loading

Spring Security Filter 체인의 시작 지점입니다. 모든 API 요청이 컨트롤러에 매핑하기 전에 "해당 사용자가 누구인가" 확인하고, 해당 정보를 시스템 전체가 사용할 수 있도록 세팅합니다.

저장소 용도 사용처
TenantContextHolder 비즈니스 로직용 (tenantId, dojangId 등) Service, Repository, AOP
SecurityContextHolder Spring Security용 (인증 객체) Filter Chain

2. 도장 접근 검증 (AOP)

특정 도장의 데이터에 접근할 때, "해당 도장이 활성 상태인지", "사용자가 해당 도장에 접근 권한이 있는지" 검증합니다.

2.1 AOP 실행 순서

public final class AopOrder {
    public static final int SECURITY_VALIDATION = Ordered.HIGHEST_PRECEDENCE + 100;  // 1순위: 도장 접근
    public static final int PERMISSION_CHECK = Ordered.HIGHEST_PRECEDENCE + 200;     // 2순위: 권한 검증
}
flowchart LR
    A[Service 메서드 호출] --> B["@ValidateDojangAccess<br/>(Order: HIGHEST_PRECEDENCE + 100)"]
    B --> C["@RequirePermission<br/>(Order: HIGHEST_PRECEDENCE + 200)"]
    C --> D[비즈니스 로직]

    style B fill:#f1f8e9,stroke:#4caf50
    style C fill:#e8eaf6,stroke:#3f51b5
Loading

2.2 검증 프로세스

@ValidateDojangAccess 어노테이션이 붙은 Service는 메서드 실행 전 단계별 검증을 거칩니다.

flowchart TD
    Start([요청 시작]) --> TenantCheck{테넌트<br/>활성 상태?}

    TenantCheck -- No --> TenantInactive[TENANT_INACTIVE]
    TenantCheck -- Yes --> DojangCheck{도장<br/>활성 상태?}

    DojangCheck -- No --> DojangInactive[DOJANG_INACTIVE]
    DojangCheck -- Yes --> RoleCheck{관장<br/>OWNER?}

    RoleCheck -- Yes --> Pass([즉시 통과])
    RoleCheck -- No --> EmploymentCheck{해당 도장 소속?<br/>Employment 확인}

    EmploymentCheck -- No --> Unauthorized[UNAUTHORIZED_ACCESS]
    EmploymentCheck -- Yes --> Allow([접근 허용])

    style Start fill:#f3f3f3,stroke:#333
    style Pass fill:#e1f5fe,stroke:#01579b
    style Allow fill:#e1f5fe,stroke:#01579b
    style TenantInactive fill:#ffebee,stroke:#c62828
    style DojangInactive fill:#ffebee,stroke:#c62828
    style Unauthorized fill:#ffebee,stroke:#c62828
Loading

핵심 원칙:

  • 관장님(OWNER): 테넌트 내 모든 도장에 자유롭게 접근
  • 사범님(INSTRUCTOR): ACTIVE 상태의 Employment가 있는 도장만 접근

2.3 사용 방법

@ValidateDojangAccess           // 클래스에 선언하면 모든 public 메서드에 적용
@Service
public class StudentService {

    public void getStudent(String dojangId, ...) { }      // 자동 검증

    @SkipDojangValidation                                  // 검증 제외
    public void getStatistics() { }
}

3. 권한 기반 인가

사용자의 역할(Role) 과, 사범님 개별에게 부여된 세부 권한(Permission) 을 분리하여 제어합니다.

3.1 역할(Role) vs 권한(Permission)

구분 설명 예시
역할 (Role) 시스템 접근 수준 OWNER, INSTRUCTOR
권한 (Permission) 기능별 세부 권한 STUDENT_UPDATE, PAYMENT_VIEW

3.2 @RequirePermission 사용법

메서드 레벨에서 세부 권한을 검증합니다.

@ValidateDojangAccess  // 클래스: 도장 접근 검증
@Service
public class StudentService {

    @RequirePermission(PermissionType.STUDENT_CREATE)  // 메서드: 권한 검증
    @Transactional
    public StudentRes createStudent(String dojangId, StudentCreateReq req) { }

    @RequirePermission(PermissionType.STUDENT_UPDATE)
    @Transactional
    public StudentRes updateStudent(String dojangId, String studentId, StudentUpdateReq req) { }

    @RequirePermission(PermissionType.STUDENT_DELETE)
    @Transactional
    public void deleteStudent(String dojangId, String studentId) { }
}

복합 권한 검증:

// OR: 하나라도 있으면 통과 (기본값)
@RequirePermission({PermissionType.STUDENT_VIEW, PermissionType.ATTENDANCE_VIEW})

// AND: 모두 있어야 통과
@RequirePermission(
    value = {PermissionType.STUDENT_VIEW, PermissionType.PAYMENT_VIEW},
    operator = LogicalOperator.AND
)

3.3 권한 검증 프로세스 (PermissionCheckAspect)

flowchart TD
    Start([메서드 호출]) --> OwnerCheck{OWNER 또는<br/>SYSTEM 사용자?}

    OwnerCheck -- Yes --> Pass([즉시 통과])
    OwnerCheck -- No --> ContextCheck{컨텍스트 정보<br/>존재?}

    ContextCheck -- No --> Denied1[AccessDeniedException<br/>인증 정보 없음]
    ContextCheck -- Yes --> CacheCheck[PermissionCache에서<br/>권한 확인]

    CacheCheck --> HasPerm{권한 보유?}
    HasPerm -- Yes --> Allow([접근 허용])
    HasPerm -- No --> Denied2[AccessDeniedException<br/>권한 없음]

    style Pass fill:#e1f5fe,stroke:#01579b
    style Allow fill:#e1f5fe,stroke:#01579b
    style Denied1 fill:#ffebee,stroke:#c62828
    style Denied2 fill:#ffebee,stroke:#c62828
Loading

3.4 권한 타입 (PermissionType)

권한 코드 설명 기본 부여
STUDENT_VIEW student:view 원생 정보 조회 ✅ (필수)
STUDENT_CREATE student:create 원생 등록
STUDENT_UPDATE student:update 원생 정보 수정
STUDENT_DELETE student:delete 원생 삭제
ATTENDANCE_VIEW attendance:view 출석 조회
ATTENDANCE_UPDATE attendance:update 출석 체크/수정
PAYMENT_VIEW payment:view 수납 조회
PAYMENT_UPDATE payment:update 청구서/수납 관리
DOJANG_UPDATE_INFO dojang:updateInfo 도장 정보 수정
DOJANG_MANAGE_CLASS dojang:manageClass 수련반 관리
STATS_VIEW_DASHBOARD stats:viewDashboard 대시보드 조회
  • 기본 부여 (✅): 사범 등록 시 자동 부여
  • 선택 부여 (❌): 관장이 개별 부여
  • 필수 권한: STUDENT_VIEW는 해제 불가

4. 캐싱 아키텍처

4.1 캐시 구조

권한 검증은 모든 API 요청마다 발생하므로, 인터페이스 기반 캐시로 DB 부하를 최소화합니다.

flowchart TB
    subgraph "권한 검증 흐름"
        AOP[PermissionCheckAspect] --> Cache[PermissionCache<br/>인터페이스]
    end

    subgraph "구현체 - 교체 가능"
        Cache --> Local[LocalPermissionCache<br/>Caffeine]
        Cache -.-> Redis[RedisPermissionCache<br/>미구현]
    end

    Local --> DB[(Employment 테이블)]
    Redis -.-> RedisServer[(Redis Server)]

    style Cache fill:#fff3e0,stroke:#ff9800
    style Local fill:#e8f5e9,stroke:#4caf50
    style Redis fill:#f5f5f5,stroke:#9e9e9e,stroke-dasharray: 5 5
Loading

4.2 도장 접근 검증 캐시

캐시 용도
tenant-active tenantId 테넌트 활성화 상태
dojang-active dojangId 도장 활성화 상태
tenant-owner tenantId:userId 테넌트 오너 여부
employment userId:dojangId 도장 소속 여부

4.3 권한 캐시 (PermissionCache)

// 인터페이스
public interface PermissionCache {
    boolean hasPermission(String userId, String tenantId, String dojangId,
                          String resource, String action);
    void invalidate(String userId, String tenantId, String dojangId);
}

캐시 키 형식:

tenant:{tenantId}:user:{userId}:dojang:{dojangId}

현재 구현 (LocalPermissionCache):

@Component
public class LocalPermissionCache implements PermissionCache {

    private final Cache cache;  // Caffeine Cache

    @Override
    public boolean hasPermission(...) {
        String key = buildKey(tenantId, userId, dojangId);

        // 캐시 미스 시 DB 조회
        Set<PermissionType> permissions = cache.get(key, () -> {
            return employmentRepository
                    .findByUserIdAndTenantIdAndDojangIdAndStatus(...)
                    .map(Employment::getPermissions)
                    .orElse(Set.of());
        });

        return permissions.contains(required);
    }
}

Clone this wiki locally