@PreAuthorize Trailing Lambdas로 대체해보기

오지석·2024년 6월 13일
0

@PreAuthorize

@PreAuthorize@PostAuthorize 어노테이션은 Spring Security에서 지원되는 기능으로 해당 principal의 authorities를 확인해 Authorization을 지원합니다.
@PreAuthorize는 함수가 호출되기 전에 @PostAuthorize는 함수가 호출되고난 후에 SpEl 표현식에 대한 검증을 진행합니다.

@PreAuthorize("#user.name == principal.name")
fun doSomething1(user: User, request: SomeRequest): Unit { ... }

@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
fun doSomething2(user: User): Unit { ... }

이런 식으로 쉽게 methods와 properties에 접근할 수 있고 method의 argument에 접근할 수 있어 많이 활용됩니다.
하지만 SpEL의 내용은 문자열이고 표현식과 JoinPoint 인자명이 불일치하더라도 컴파일 예외가 발생하지 않습니다.
예를 들어,

@PreAuthorize("#user.name == principal.name")
fun doSomething1(arg: User, request: SomeRequest): Unit { ... }

이런 식으로 작성하더라도 컴파일은 문제없이 되고 Runtime에 가서야 버그가 발생합니다.

Traling Lambdas

그래서 이를 해결해보고자 Trailing Lambdas를 활용해서 커스텀 PreAuthorize를 만들어 보기로 했습니다.

Trailing Lambdas는 함수의 마지막 인자가 함수라면 마지막 함수는 ()에서 빠져나와 람다식으로 표현할 수 있는 문법입니다.

흔히 사용하는 Kotlin의 고차함수인 map이나 fold도 이런 Traling Lambdas의 대표적인 예시입니다.

numbers.map { it -> it + 1 }

numbers.fold (0) { acc, it -> acc + it }

map 의 정의를 보면

inline fun <T, R> Array<out T>.map(
    transform: (T) -> R
): List<R>

이렇게 transform 이라는 함수를 인자로 받는 함수이고

fold의 경우에는

inline fun <T, R> Array<out T>.fold(
    initial: R,
    operation: (acc: R, T) -> R
): R

이렇게 마지막 인자인 operation이 함수이기 때문에 위와 같이 사용할 수 있습니다.

CustomPreAuthorize

지금 제가 진행하고 있는 프로젝트에서 PreAuthorize가 해야할 일은 매우 간단합니다.
해당 principal이 Roles에 속하는 Role 중 하나를 갖고 있는지 확인하는 것입니다.

@Component
class CustomPreAuthorize {
    fun <T> hasAnyRole(principal: MemberPrincipal, roles: Set<MemberRole>, function: () -> T): T {
        val validAuthorities = roles.map { role -> SimpleGrantedAuthority("ROLE_$role") }
        if (principal.authorities.none { validAuthorities.contains(it) })
            throw AccessDeniedException("Not allowed to this API")
        return function.invoke()
    }
}

principal의 authroities에 hasAnyRole이 적용된 api에 접근할 수 있는 Role이 있는지를 확인하고 하나라도 있다면 함수를 실행시킵니다.

적용된 모습은 다음과 같습니다.

@DeleteMapping("/{productId}")
fun deletePost(
    @AuthenticationPrincipal principal: MemberPrincipal,
    @PathVariable("productId") productId: Long,
): ResponseEntity<Unit> = preAuthorize.hasAnyRole(principal, setOf(MemberRole.ADMIN)) {
    ResponseEntity.status(HttpStatus.OK).body(productService.deleteProduct(productId))
}

이렇게 하면 trailing lambdas를 사용해서 함수의 인자에도 접근할 수 있고 Authorization 과정을 Spring에 모두 맡기는 것이 아니라 직접 컨트롤할 수 있습니다.

참고

0개의 댓글