티스토리 뷰

개요

  • Spring Boot 기반 프로젝트에서 요청 API에 대한 권한 검사를 수행하는 방법은 여러가지가 있을 수 있다. 서블릿 필터를 이용하는 방법, 스프링 인터셉터를 이용한 방법, 스프링 시큐리티를 이용한 방법 등이다. 마지막으로 Spring AOP을 이용한 방법이 있다. Spring AOP를 이용하면, 스프링 시큐리티 대비 비교적 간단하고 직관적인 방법으로 권한 검사가 가능한 장점이 있다. 이번 글에서는 이러한 Spring AOP를 이용한 권한 검사 방법을 설명하고자 한다.

라이브러리 종속성 추가

  • 프로젝트 루트의 /build.gradle 파일에 아래 내용을 추가한다.
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    compile group: 'com.devskiller.friendly-id', name: 'friendly-id', version: '1.1.0'
}

@SpringBootApplication

  • Spring AOP를 활성화하려면, 아래와 같이 @EnableAspectJAutoProxy을 추가해야 한다.
package com.jsonobject.example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.EnableAspectJAutoProxy

@SpringBootApplication
@EnableAspectJAutoProxy
class ExampleApplication

fun main(args: Array<String>) {

    runApplication<ExampleApplication>(*args)
}

ClientAuthInterceptor 작성

  • 가장 먼저 REST API를 호출하는 주체인 클라이언트에 대한 인증부를 구현할 차례이다. 클라이언트는 사전에 발급 받은 고유의 client_id, client_secret를 매 요청마다 헤더에 첨부하면 서버는 아래와 같이 사전에 약속된 클라이언트의 요청인지 유효성 검사를 수행할 수 있다.
package com.jsonobject.example

import com.funnc.shop.api.common.exception.ApiErrorCode
import com.funnc.shop.api.common.exception.ApiException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class ClientAuthInterceptor : HandlerInterceptorAdapter() {

    @Autowired
    private lateinit var clientCredentialService: ClientCredentialService

    @Throws(Exception::class)
    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {

        val clientId: String? = request.getHeader("X-Client-Id")
        val clientSecret: String? = request.getHeader("X-Client-Secret")

        if (clientId.isNullOrEmpty()) {
            throw ApiException(ApiErrorCode.EMPTY_CLIENT_ID, HttpStatus.UNAUTHORIZED)
        }

        val clientCredential: ClientCredential? = clientCredentialService.getClientCredential(clientId)

        if (clientSecret.isNullOrEmpty()) {
            throw ApiException(ApiErrorCode.EMPTY_CLIENT_SECRET, HttpStatus.UNAUTHORIZED)
        }
        clientCredential ?: throw ApiException(ApiErrorCode.INVALID_CLIENT_ID, HttpStatus.UNAUTHORIZED)
        if (clientCredential.secret != clientSecret) {
            throw ApiException(ApiErrorCode.INVALID_CLIENT_SECRET, HttpStatus.UNAUTHORIZED)
        }

        return true
    }
}

Role 작성

  • 클라이언트 인증 후에는 클라이언트를 통해 접근할 사용자에 대한 인증부를 구현할 차례이다. 먼저 사용자 역할에 대한 식별 코드를 아래와 같이 작성한다. 일반 사용자를 가정한 USER와 어드민 사용자를 가정한 ADMIN, 마지막으로 개발자를 가정한 SUPER_ADMIN을 작성하였다.
package com.jsonobject.example

enum class Role {

    USER,
    ADMIN,
    SUPER_ADMIN
}

@RoleMapping 작성

  • 각 요청에 대해 앞서 작성한 요구 권한을 맵핑할 @RoleMapping 어노테이션을 작성할 차례이다. 작명은 @RequestMapping에 영감을 받아 이름 지었다.
package com.jsonobject.example

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RoleMapping(vararg val value: Role = [])

PermissionMappingAspect 작성

  • 앞서 작성한 권한 검사를 위한 @PermissionMapping 어노테이션이 실제 동작할 수 있도록, @Aspect 클래스를 작성할 차례이다. 실제 권한 검사 로직을 작성하면 된다.
package com.jsonobject.example

import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes

@Component
@Aspect
class PermissionMappingAspect {

    @Around("@annotation(permissionMapping)")
    fun aroundPermissionMapping(joinPoint: ProceedingJoinPoint, permissionMapping: PermissionMapping): Any? {

        val request = (RequestContextHolder.currentRequestAttributes() as ServletRequestAttributes).request
        var isPreHandleSucceed = false

        val accessToken = request.getHeader("X-Access-Token")
        if (!accessToken.isNullOrEmpty()) {
            // 액세스 토큰에 대한 유효성 검사 로직 작성
            isPreHandleSucceed = true
        }

        if (!isPreHandleSucceed) {
            throw RuntimeException("Permission Denied")
        }

        return joinPoint.proceed()
    }
}

PermissionMappingInterceptor 작성

  • 권한 검사와 별도로 매 요청시 액세스 토큰이 유효할 경우 만료 일시를 연장하는 로직을 아래와 같이 작성한다.
package com.jsonobject.example

import org.springframework.stereotype.Component
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class PermissionMappingInterceptor : HandlerInterceptorAdapter() {

    @Throws(Exception::class)
    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {

        val accessToken = request.getHeader("X-Access-Token")
        if (!accessToken.isNullOrEmpty()) {
            // 액세스 토큰이 유효할 경우 액세스 토큰 만료일시 연장 로직 작성 
        }

        return true
    }
}

@RestController 작성

  • 아래는 컨트롤러의 요청에 권한 검사를 적용한 예이다. 기존 스프링 문법에 위화감 없이 적용되어 직관적인 것을 확인할 수 있다.
package com.jsonobject.example

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class FooController {

    @GetMapping("/v1/foos")
    @RoleMapping(Role.USER)
    fun getFoos(): ResponseEntity<*> {

        return ResponseEntity.ok("Foos!")
    }
}
댓글
댓글쓰기 폼