기록

[Spring] URI 기반 권한제어 : Spring Boot 필터 및 Swagger 설정 본문

Web/Spring

[Spring] URI 기반 권한제어 : Spring Boot 필터 및 Swagger 설정

youngyin 2024. 11. 4. 00:00

1. 시작하면서

Spring Boot를 사용하여 API 서버를 구축하는 과정에서, 사용자의 유형에 따라 권한을 제어하는 것이 필요했습니다. 이 프로젝트에서는 다음과 같은 사용자 유형을 정의했습니다:

  • BOSS: 관리자 역할로, 시스템 전반에 대한 접근 권한을 가집니다. 이 사용자는 주요 관리 기능을 수행하는 데 필요한 모든 권한을 갖습니다.
  • CUSTOMER: 일반 고객으로, 특정 서비스에만 접근할 수 있습니다. 고객은 자신의 계정 정보나 주문 상태 등을 조회할 수 있습니다.
  • ADMIN: 시스템 관리자로, 사용자 관리 및 시스템 설정을 담당합니다. 이 사용자는 시스템의 운영과 관련된 여러 작업을 수행할 수 있는 권한을 가집니다.
  • OPEN: 권한이 필요 없는 요청으로, 모든 사용자에게 허용됩니다. 이 카테고리는 일반적인 정보 제공이나 공지사항과 같은 공개된 리소스에 해당합니다.

각 사용자 유형에 따라 서비스는 다르게 존재하였으며, "여러 종류의 클라이언트에서 API서버에 요청한다"를 전제로 서버를 설계하였습니다. 또한 "서비스별로 필요한 API를 모아서 보고 싶다"는 클라이언트 개발자들의 요청이 있었습니다.

  • BOSS는 관리자 모바일 앱을 통해 BOSS 권한 API와 OPEN 권한 API를 사용합니다.
  • CUSTOMER는 일반 고객용 모바일 앱을 통해 CUSTOMER 권한 API와 OPEN 권한 API를 사용합니다.
  • ADMIN은 별도의 웹 애플리케이션을 통해 ADMIN 권한 API와 OPEN 권한 API를 사용합니다.

이 프로젝트에서는 API 문서를 위해 Swagger를 사용하고 있었고, Swagger에서 엔드포인트의 URI를 기반으로 그룹을 나눌 수 있다는 점에 주목했습니다. 여기서 아이디어를 얻어 URI에 사용자 권한을 포함하고 검증하는 방법을 채택했습니다.

2. Filter: URI 기반 권한 체크

(1) Concept

Controller의 엔드포인트에 접근하기 전에 필터에서 권한을 체크하도록 구성합니다.

(2) RoleBasedUrilFilter Source

URI 기반으로 사용자 권한을 체크하는 필터를 구현합니다. 이 필터는 사용자의 요청 URI를 분석하여 해당 사용자의 권한에 맞는 접근인지 확인합니다. 사용자가 권한이 없는 URI에 접근하려 할 경우, 적절한 오류 메시지를 반환하여 사용자에게 접근이 거부되었음을 알립니다.

더보기

import com.meokq.api.auth.enums.UserType
import com.meokq.api.auth.request.AuthReq
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class RoleBasedUriFilter : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val userType = getUserTypeFromSecurityContext()

        if (!isAuthorized(request.requestURI, userType)) {
            response.sendError(HttpStatus.FORBIDDEN.value(), "접근이 거부되었습니다.")
            return
        }

        filterChain.doFilter(request, response)
    }

    private fun isAuthorized(requestUri: String, userType: UserType?): Boolean {
        return when (userType) {
            UserType.BOSS -> requestUri.startsWith("/api/boss/")
            UserType.CUSTOMER -> requestUri.startsWith("/api/customer/")
            UserType.ADMIN -> requestUri.startsWith("/api/admin/")
            UserType.UNKNOWN -> false
            else -> true // OPEN 리소스 접근 허용
        }
    }

    private fun getUserTypeFromSecurityContext(): UserType? {
        val authentication = SecurityContextHolder.getContext().authentication
        return (authentication?.principal as? AuthReq)?.userType
    }
}

주요 기능 설명

  • URI 기반 권한 확인: 요청 URI가 사용자의 역할에 맞는지 확인합니다. 예를 들어, BOSS 타입 사용자는 /api/boss로 시작하는 URI에만 접근할 수 있습니다. 이러한 구조는 역할에 따른 접근 제어를 명확하게 하여 보안성을 높입니다.
  • 권한 거부 처리: 사용자가 권한이 없는 URI에 접근하려 할 경우, 403 Forbidden 오류를 반환합니다. 이는 클라이언트에게 정확한 오류 메시지를 제공하여 문제를 이해하는 데 도움을 줍니다.

4. Security Configuration

Spring Security 설정을 통해 URI 기반 권한 필터를 적용합니다. 이 설정은 보안 관리의 핵심이 되며, 필터를 통해 요청이 들어올 때마다 권한 검증이 이루어집니다.

@Configuration
@EnableWebSecurity
class SecureConfig(
    private val roleBasedUriFilter: RoleBasedUriFilter,
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http.csrf().disable()
            .authorizeHttpRequests()
            .anyRequest().permitAll() // 기본적으로 모든 요청 허용
            .and()
            .addFilterBefore(roleBasedUriFilter, UsernamePasswordAuthenticationFilter::class.java)

        return http.build()
    }
}

설정 내용

  • CSRF 보호 비활성화: REST API에서는 CSRF 공격을 고려하지 않기 때문에 CSRF 보호를 비활성화합니다. 그러나 웹 애플리케이션에서는 이 기능이 필요할 수 있으므로, 상황에 맞게 조정해야 합니다.
  • URI 기반 권한 필터 추가: 모든 요청을 기본적으로 허용하되, 필터에서 권한을 검증하여 적절한 접근 제어를 수행합니다. 이를 통해 특정 URI에 대한 접근 권한을 효과적으로 관리할 수 있습니다.

5. Swagger Group

Swagger를 통해 API 문서를 자동으로 생성하고, 사용자 유형에 따라 그룹화된 API를 제공할 수 있습니다. 이를 통해 개발자들은 각 그룹에 대한 API 문서를 쉽게 확인하고 사용할 수 있습니다.

(1) Swagger Config Source

더보기

import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.security.SecurityRequirement
import org.springdoc.core.models.GroupedOpenApi
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@SecurityScheme(
    type = SecuritySchemeType.APIKEY, `in` = SecuritySchemeIn.HEADER,
    name = "authorization", description = "Auth Token"
)
class SwaggerConfig {

    @Value("\${spring.profiles.active:local}")
    private lateinit var profile: String

    @Value("\${apiProject.version:V.0.0.0}")
    private lateinit var version: String

    @Bean
    fun openResourceApi(): GroupedOpenApi =
        GroupedOpenApi.builder()
            .group("open-resource") // 그룹 이름
            .pathsToMatch("/api/open/**") // 해당 경로의 API를 포함
            .build()

    @Bean
    fun bossResourceApi(): GroupedOpenApi =
        GroupedOpenApi.builder()
            .group("boss-resource") // 그룹 이름
            .pathsToMatch("/api/boss/**") // 해당 경로의 API를 포함
            .build()

    @Bean
    fun adminResourceApi(): GroupedOpenApi =
        GroupedOpenApi.builder()
            .group("admin-resource") // 그룹 이름
            .pathsToMatch("/api/admin/**") // 해당 경로의 API를 포함
            .build()

    @Bean
    fun customerResourceApi(): GroupedOpenApi =
        GroupedOpenApi.builder()
            .group("customer-resource") // 그룹 이름
            .pathsToMatch("/api/customer/**") // 해당 경로의 API를 포함
            .build()

    @Bean
    fun openApi(): OpenAPI =
        OpenAPI()
            .info(
                Info()
                    .title("[$profile] Meok-q Api Document")
                    .description("$profile 환경에서의 API 문서입니다.")
                    .version("$version")
            )
            .security(
                listOf(
                    SecurityRequirement()
                        .addList("authorization") // 보안 요구 사항 설정
                )
            )
}

(2) 결과

6. 장단점

구분 장점 단점
1 명확한 권한 관리: URI에 역할 정보를 포함시켜 각 리소스에 대한 접근 권한을 명확하게 정의할 수 있습니다. URI 길이 제한: 너무 많은 역할 정보를 URI에 포함시키면 URI가 길어질 수 있으며, 이는 가독성을 떨어뜨릴 수 있습니다. 최대한 간결한 URI 설계를 고려해야 합니다.
2 유지보수 용이: URI 패턴을 통해 권한을 관리하므로, 새로운 역할이나 리소스가 추가될 때 코드 수정을 최소화할 수 있습니다. 이는 개발 효율성을 높입니다. 보안 취약점: URI를 통해 권한을 제어할 경우, URI를 조작하여 접근할 수 있는 가능성이 존재하므로 추가적인 보안 검토가 필요합니다.
3 직관적인 접근 제어: 개발자나 운영자가 어떤 URI가 어떤 역할에 의해 접근 가능한지 쉽게 이해할 수 있습니다. 복잡성 증가: 역할과 URI 매핑이 복잡해질 경우, 관리가 어려워질 수 있으며, 실수로 잘못된 권한 설정이 발생할 수 있습니다.

7. 마무리하면서

Filter, Interceptor, AOP

마지막으로, Spring Framework에서 Filter, Interceptor, AOP(Aspect-Oriented Programming)는 요청 처리 과정에서 특정 작업을 수행하는 메커니즘입니다. Filterjavax.servlet.Filter를 구현하여 HTTP 요청/응답을 가로채고, 인증 및 로깅 등의 작업을 수행합니다. InterceptorHandlerInterceptor를 구현하여 컨트롤러 호출 전후에 처리를 하며, 주로 세션 관리나 권한 체크에 사용됩니다. AOP는 비즈니스 로직과 분리된 공통 관심사를 모듈화하여 트랜잭션 관리나 로깅을 수행하며, 포인트컷을 통해 적용됩니다. 이 세 가지는 각각의 목적과 사용 사례가 다르므로 필요에 따라 적절히 선택하여 사용할 수 있습니다.

역할기반 권한 제어방법 : 어노테이션 활용/URI 활용

URI에 역할을 명시하는 것은 권한 제어를 위한 한 가지 접근 방식이지만, 일반적인 방법이라고 보기는 어렵습니다.
어노테이션 기반 권한 관리를 사용하면 각 엔드포인트에 권한을 명시하여 접근 제어를 구현할 수 있습니다. Spring Security에서 주로 사용하는 어노테이션은 @PreAuthorize, @PostAuthorize, @Secured 등이 있습니다. Api 서버를 리뉴얼 하게 된다면, 어노테이션 기반으로 변경하는 것을 고려하려고 합니다.

(1) @PreAuthorize

@RestController
@RequestMapping("/api")
class NoticeController {

    @PreAuthorize("hasRole('ADMIN')") // ADMIN 역할을 가진 사용자만 접근 가능
    @GetMapping("/settings")
    fun getSettings(): ResponseEntity<String> {
        return ResponseEntity.ok("Admin Settings")
    }
}

(2) URI 활용

@RestController
@RequestMapping("/api")
class NoticeController {

    @GetMapping("/admin/settings")
    fun getSettings(): ResponseEntity<String> {
        return ResponseEntity.ok("Admin Settings")
    }
}
Comments