본문 바로가기

Back-End/Spring-MVC

AOP를 활용한 권한 검증 (feat. 커스텀 어노테이션)


1. 커스텀 어노테이션 소개

AOP와 함께 사용할 @AuthCheck 어노테이션은 메서드 수준에서 권한을 정의한다. 이 어노테이션에 필요한 권한을 설정하고, AOP를 통해 권한 검증 로직을 실행한다.

AuthCheck 어노테이션 코드

  • String[] value() : 메서드에 접근 가능한 권한을 배열로 정의한다. 
    배열로 정의한 이유 : OWNER OR CUSTOMER 처럼 각각 접근 가능한 메서드가 따로 있지만 OWNER AND CUSTOMER 처럼 공통으로 접근 가능한 페이지가 있기 때문이다. 

"OWNER" 만 접근 가능한 메서드
"OWNER, CUSTOMER" 공통적으로 접근 가능한 메서드


2. 커스텀 어노테이션을 활용하여 AOP 권한 검증

  • 클래스 선언부
    • @Slf4j : 로깅 기능을 제공하는 Lombok 어노테이션, log 객체를 자동으로 생성
    • @Component : Spring Bean으로 등록. Spring 컨텍스트에서 AOP를 사용할 수 있도록 설정.
    • @RequiredArgsConstructor : final로 선언된 필드에 대해 생성자를 자동으로 생성한다. HttpServletRequest를 주입받기 위해 사용.
    • @Aspect : 이 클래스가 AOP를 구현한다는 것을 Spring에 알림.

  • 필드 선언
    • HttpServletRequest : 현재 요청의 정보를 담고 있는 객체. AOP에서 요청의 헤더나 속성(role, Id 등)을 가져오기 위해 사용
    • final : @RequiredArgsConstructor와 함께 사용하여 의존성을 주입 받는다.

  • 메서드 선언
    • @Around: AOP 어드바이스로, 특정 시점(메서드 실행 전, 후)에 동작한다. 여기서는 @AuthCheck 어노테이션이 선언된 메서드의 실행을 가로챔.
    • "@annotation(team11.team11project.common.aspect.AuthCheck)" : @AuthCheck 어노테이션이 붙은 메서드만 이 AOP의 대상이 된다.
    • ProceedingJoinPoint joinPoint : AOP에서 현재 실행 중인 메서드에 대한 정보를 제공한다. 원래 메서드를 실행하거나 실행을 중단할 수 있다.

  • 요청의 권한 가져오기
    • 현재 요청에서 role 속성을 가져온다.
    • role은 JWT 필터에서 설정한 사용자 권한(ex: OWNER, CUSTOMER) 이다.
    • 이 값을 기반으로 권한 검증을 진행

  • 어노테이션 정보 가져오기
    • joinPoint.getSignature() : 현재 실행 중인 메서드의 정보를 가져온다.
    • (MethodSignature) : 메서드의 시그니처를 가져와 타입 캐스팅한다.
    • getMethod() : 실제 메서드 객체를 가져온다.
    • getAnnotation(AuthCheck.class) : 메서드 선언된 @AuthCheck 어노테이션을 가져온다.
      • 어노테이션에 설정된 권한 정보를 authCheck.value()로 확인할 수 있다.

  • 요청에서 role 정보를 찾지 못한 경우 UserRoleNotFoundException을 발생시킨다.
  • 이는 사용자 권한이 설정되지 않은 잘못된 요청임을 나타낸다.

  • authCheck.value()
    • @AuthCheck 애노테이션에 정의된 필요한 권한 목록이다.
    • ex: {"OWNER", "CUSTOMER"}
  • Arrays.stream(authCheck.value())
    • 권한 목록을 스트림으로 변환
  • noneMatch(role::equals)
    • 현재 사용자 권한이 권한 목록(authCheck.value())에 포함되지 않은 경우 true를 반환한다.

  • 권한 검증이 통과된 경우, 원래 메서드를 실행한다.
  • 반환값은 원래 메서드의 결과이다.

2-1 JoinPoint 부분 AOP에서 현재 실행 중인 메서드에 대한 정보를 추출하는 과정을 더 자세히 설명

1) joinPoint.getSignature()

  • ProceedingJoinPoint는 AOP에서 현재 실행 중인 메서드에 대한 모든 정보를 담고 있다.
  • getSignature()는 현재 메서드의 시그니처(이름, 반환 타입, 파라미터 등)를 가져온다!
@Around("@annotation(AuthCheck)")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    // 현재 실행 중인 메서드의 시그니처 가져오기
    System.out.println("Signature: " + joinPoint.getSignature());
    return joinPoint.proceed();
}

// 출력 값
Signature: void com.example.MyService.myMethod(java.lang.String)

2)(MethodSignature)

  • getSignature()의 반환값은 Signature 타입으로, 메서드의 기본 정보를 제공한다.
  • MethodSignature는 Signature를 확장한 인터페이스로 메서드와 관련된 추가 정보를 제공한다.
  • MethodSignature로 타입 캐스팅하면 메서드의 반환 타입, 이름, 파라미터 타입 등을 더 구체적으로 가져올 수 있다.
@Around("@annotation(AuthCheck)")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    
    // 메서드 이름 가져오기
    System.out.println("Method Name: " + methodSignature.getMethod().getName());

    // 반환 타입 가져오기
    System.out.println("Return Type: " + methodSignature.getReturnType());

    return joinPoint.proceed();
}

//출력
Method Name: myMethod
Return Type: void

3) getMethod()

  • MethodSignature의 getMethod()는 현재 실행 중인 메서드 객체(java.lang.reflect.Method)를 반환한다.
  • 이 메서드 객체를 사용하면 리플렉션을 통해 메서드에 선언된 어노테이션 등 추가 정보를 가져올 수 있다.
@Around("@annotation(AuthCheck)")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod(); // 메서드 객체 가져오기
    
    System.out.println("Method: " + method);
    return joinPoint.proceed();
}

// 출력
Method: public void com.example.MyService.myMethod(java.lang.String)

4) getAnnotation(AuthCheck.class)

  • Method 객체에서 getAnnotation()을 사용하면 메서드에 선언된 특정 어노테이션을 가져올 수 있다.
  • 여기서는 @AuthCheck 어노테이션을 가져오며, 어노테이션에 정의된 값(필요한 권한)을 확인한다.
@AuthCheck({"OWNER", "CUSTOMER"})
public void myMethod() {}
"메서드에 위와 같은 어노테이션이 선언되어 있다면"

@Around("@annotation(AuthCheck)")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    Method method = ((MethodSignature)joinPoint.getSignature)).getMethod();
    
    // @AuthCheck 애노테이션 가져오기
    AuthCheck authCheck = method.getAnnotation(AuthCheck.class);
    if (authCheck != null) {
        System.out.println("Required Roles: " + Arrays.toString(authCheck.value()));
    }

    return joinPoint.proceed();
}

//출력
Required Roles: [OWNER, CUSTOMER]

5) 동작 흐름 요약

  • joinPoint.getSignature()
    • 현재 실행 중인 메서드의 기본 정보를 가져온다.
    • 반환값: Signature 타입
  • (MethodSignature)
    • Signature를 MethodSignature로 캐스팅하여 메서드와 관련된 추가 정보를 사용한다.
    • ex : 반환 타입, 파라미터 타입 등
  • getMethod()
    • 현재 메서드의 Method 객체를 가져온다.
    • 이 객체를 사용하면 리플렉션으로 어노테이션 정보를 확인할 수 있다.
  • getAnnotation(AuthCheck.class)
    • 현재 메서드에 선언된 @AuthCheck 어노테이션을 가져온다.
    • 어노테이션에 설정된 권한 정보(value())를 확인할 수 있다.

3. 동작 흐름

  1. 클라이언트 요청
    • 클라이언트가 API를 호출한다. 예를 들어 /api/store/create 요청(OWNER만 접근 가능)
  2. AOP가 메서드 실행을 가로챔
    • AOP는 @AuthCheck가 선언된 메서드를 가로채서 실행한다.
  3. 권한 검증
    • HttpServletRequest에서 사용자 권한을 가져온다.
    • 어노테이션에 설정된 권한(AuthCheck.value())과 요청 권한을 비교한다.
  4. 권한 확인 후 처리
    • 권한이 유효하면 원래 메서드를 실행(joinPoint.proceed())
    • 권한이 유효하지 않으면 예외를 발생