Spring Framework/Filter

Spring Filter Security로 인가처리 - Spring Filter Hands On 3

Junuuu 2023. 10. 6. 00:01

개요

Spring Security Hands On 시리즈에 들어가야 할 내용 같다고 생각하지만.. Spring Security를 활용하면 조금 더 편리하게 인가처리가 가능하기 때문에 소개하고자 합니다.

 

Spring Security 의존성 추가

implementation("org.springframework.boot:spring-boot-starter-security")

 

테스트용 Controller 구현

@RestController
class AuthorizationTestController {

    @GetMapping("/admin/admin-path")
    fun onlyAdmin(@RequestHeader role: String): String{
        return "접근 가능"
    }

    @GetMapping("/user-path")
    fun onlyUser(@RequestHeader role: String): String{
        return "접근 가능"
    }
}

 

 

SecurityConfig 설정

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig {
    @Bean
    fun filterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
        httpSecurity
            .csrf {
                it.disable()
            }
            .authorizeHttpRequests {
                it
                    .requestMatchers("/admin/**").authenticated()
                    .requestMatchers("/**").permitAll()

            }
            .addFilterBefore(AuthorizationFilter(), UsernamePasswordAuthenticationFilter::class.java)
        return httpSecurity.build()
    }
}

admin이라는 path가 붙으면 인증을 수행하고, 그렇지 않으면 인증을 수행하지 않도록 구성합니다.

그리고 직접 구현할 AuthroizationFilter를 추가해 둡니다.

@EnableMethodSecurity의 경우에 추후에 @PreAuthorize, @PostAuthorize 등의 어노테이션을 활용하기 위해 선언합니다.

 

AuthorizationFilter 구현

class AuthorizationFilter : OncePerRequestFilter() {
    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        val isFail = runCatching {
            val role = request.getHeader("role").uppercase()
            val authorities: List<GrantedAuthority> = listOf(SimpleGrantedAuthority("$ROLE_PREFIX$role"))
            val authentication: Authentication = UsernamePasswordAuthenticationToken(null, "", authorities)
            SecurityContextHolder.getContext().authentication = authentication

            chain.doFilter(request, response)
        }.isFailure

        if (isFail) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "현재 권한으로는 접근할 수 없는 uri입니다.")
        }
    }

    companion object{
        const val ROLE_PREFIX = "ROLE_"
    }
}

이전에 Filter를 구현했을 때와 동일하게 OncePerRequestFilter를 활용했습니다.

role을 받아오고 GrantedAuthority 객체를 만들어 넘기고 Authentication 객체를 만듭니다.

 

GrantedAuthority객체는 현재 사용자가 가지고 있는 권한을 의미하며 ROLE_ADMIN, ROLE_USER와 같이 ROLE_* 형태로 사용됩니다.

추후에 GrantedAuthority 객체를 활용하여 인가를 수행할 수 있습니다.

 

SecurityContextHolder에서 Context를 가져와 해당 인증객체를 저장해 둡니다.

해당 구현은 Thread Local을 활용하기 때문에 추후에 호출되는 스레드들은 인증객체의 정보를 활용할 수 있게 됩니다.

 

위의 코드를 한 줄로 요약하자면 헤더에서 값을 꺼내와 Role으로 사용자의 인가를 부여하여 인증이 완료된 객체를 생성해 냅니다.

 

MethodSecurity 방식 활용

@RestController
class AuthorizationTestController {

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @GetMapping("/admin/admin-path")
    fun onlyAdmin(@RequestHeader role: String): String{
        return "접근 가능"
    }

    @GetMapping("/user-path")
    fun onlyUser(@RequestHeader role: String): String{
        return "접근 가능"
    }
}

@PreAuthorize 메서드를 활용하여 ROLE_ADMIN인 경우에만 onlyAdmin을 호출할 수 있도록 설정할 수 있습니다.

이제 동료 개발자들이 굳이 SecurityConfig의 필터설정을 보지 않더라도 해당 API는 권한이 필요하다는 것을 알 수 있습니다.

또한 ADMIN 이외에 SUPER_ADMIN, 협력사들의 권한들을 다채롭게 부여하여 다양한 인가를 유연하게 수행할 수 있습니다.

 

 

치명적인 단점

해당 방법은 Security를 잘 이해하지 못하고 사용했기 때문에 모든 Path에 AuthorizationFilter가 적용됩니다.

Security의 아키텍처를 이해하는 시간을 가지고 다음 방법을 통해 해결할 수 있습니다.

https://junuuu.tistory.com/859