Spring Security는 애플리케이션의 인증/인가 관리를 쉽게 처리할 수 있도록 도와준다. 제공하는 기능 중에는 특정 API에 인증없이 요청을 처리할 수 있도록 허용하는 기능(permitAll)도 있다.
필자가 운영하는 '개발 한 스푼' 서비스에서도 Spring Security를 통해 인증을 관리하고 있다. 마찬가지로 인증이 필요없는 API들에는 permitAll을 이용해 인증을 열어주었다.
하지만 API별로 인증을 해제하는 기능은 핸들러 메서드에서 제어하는 것이 아닌 Security 설정 코드에서만 제어할 수 있다. 이는 인증 해제가 필요한 API가 늘어날수록 여러 문제를 초래한다.
이번 글에서는 API별 인증 해제를 효율적으로 처리하기 위해 적용한 방법을 공유해본다.
본 포스트는 다음과 같은 순서로 진행됩니다.
1. 기존 Security 인증해제 방법과 문제점
2. 개선방향과 적용 방법
1. 기존 Security 인증해제 방법과 문제점
1.1 설정코드에서 Path와 Method로 인증해제
Spring Security는 설정코드에서 요청 Path와 Http Method의 조합으로 인증을 허용해줄 수 있다. 다음 코드처럼 requestMatchers 메서드에 Path와 Method를 넣고 permitAll() 메서드를 이용하면 해당 API로의 요청에는 인증을 필요로 하지 않는다.
@Configuration
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity) = http
// ...
.authorizeHttpRequests { matchers ->
matchers.requestMatchers(HttpMethod.GET, "/auth/login").permitAll()
.requestMatchers(HttpMethod.POST, "/auth/register").permitAll()
.requestMatchers(HttpMethod.POST, "/auth/refresh").permitAll()
.anyRequest().authenticated()
}
.build()!!
}
1.2 해당 방식의 문제점
해당 방식은 단순해 보이지만 다음과 같은 문제가 생긴다. 때문에 보다 편리한 사용을 위해 커스텀할 필요성이 느껴졌다.
- 핸들러 메서드에서 인증 해제 정보를 알 수 없음
- API 요청을 처리하는 핸들러 메서드(Controller의 메서드)에서 인증 해제와 관련된 정보를 알 수가 없다. 때문에 설정코드를 찾아보거나 추가적인 주석이 필요하다.
- 반복적인 작업
- API 개발하고 Security 설정코드에도 함께 추가해야한다.
- 휴먼에러 발생 가능성 높음
- 설정코드에서 Path를 잘못쓰거나 Http Method를 잘못 작성하는 경우가 발생할 수 있다.
- 추가될수록 허용 API들을 파악하기 힘듦
- 허용 API가 많아질수록 FilterChain 내 정보가 비대해진다.
2. 개선방향과 적용방법
2.1 개선 방향
설정코드가 아닌 핸들러 메서드에서 인증 해제를 제어할 수 있다면 좋다고 생각했다. API 처리로직에서 정보를 알 수 있고 설정코드를 수정하면서 드는 휴먼에러도 방지할 수 있다.
인증 허용에 대한 정보는 반드시 설정코드에 작성되어야 한다. 따라서, 어노테이션으로 해당 핸들러 메서드가 인증 허용됨을 표시하고 설정코드에서 어노테이션이 적용된 API에 대해서는 인증을 해제시켜주는 일종의 자동화코드를 만들기로 했다.
문제는 어노테이션이 적용된 핸들러 메서드를 Security가 어떻게 인식하냐는 것이다.
2.2 어노테이션을 통한 해결 방향
이 문제를 풀기위해 곰곰히 고민했다. 접근방식을 조금 바꾸니 솔루션이 보였다. 문제가 풀리지 않으면 여러 문제로 나누어 생각해라!
- 모든 핸들러 메서드를 가져온다.
- 어노테이션이 존재하는 핸들러 메서드들만 필터링한다.
- 각 핸들러 메서드의 Path와 Method만 가져온다.
- 각 Path와 Method로 인증 permitAll 시키기
첫 문제만 풀면 모든게 간단하게 풀린다. 그리고 첫 문제는 핸들러메서드의 정보를 모두 가지고 있는 HandlerMapping를 통해 풀 수 있었다.
우리는 인증/인가를 관리하는 SecurityFilterChain을 빈으로 등록하며 REST API의 핸들러 정보를 관리하는 RequestMappingHandlerMapping은 빈을 등록된다. 빈에서는 다른 빈을 사용할 수 있으니 문제는 해결된 셈이다. 이제 구현해보자.
2.3 어노테이션으로 인증 해제 시키기
2.3.1 인증해제용 어노테이션 선언
먼저, 인증 해제가 필요한 핸들러 메서드임을 알리기 위한 어노테이션을 선언하고 필요한 핸들러 메서드에 적용한다. 메타 어노테이션에 대한 설명은 생략한다.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class SecurityIgnored()
// 핸들러 메서드에 적용
@RestController
@RequestMapping("/auth")
class AuthController(
private val authService: AuthService
){
@GetMapping("/login")
@SecurityIgnored
fun auth(): String {
// ...
}
}
2.3.2 어노테이션 적용된 핸들러 메서드 파싱
이제 Security 설정에서 RequestMappingHandlerMapping 을 주입받아 이로부터 핸들러 메서드들의 정보를 가져와보자. 해당 빈은 getHandlerMethods 메서드를 통해서 핸들러메서드의 정보를 제공해준다.
RequestMappingInfo를 key로, HandlerMethod를 value로 하여 Map형태로 제공한다.
value인 HandlerMethod는 말 그대로 핸들러 메서드이다. 이를 이용해서 해당 핸들러 메서드가 어떤 어노테이션을 가지고 있는지 체크할 수 있다.
key인 RequestMappingInfo에는 핸들러 메서드의 정보가 전부 들어가 있다. Path와 Method, 메서드가 어떤 Param을 받는지까지 모든 정보가 들어가 있다. 이 정보를 이용해 Path와 Method만 꺼내서 사용한다.
구현체는 다음과 같다.
@Configuration
class SecurityConfig(
private val handlerMapping: RequestMappingHandlerMapping
) {
@Bean
fun allowedMethodAndPathPattern() = handlerMapping.handlerMethods
// HandlerMethod가 SecurityIgnored 어노테이션을 가지는지
.filter { it.value.hasMethodAnnotation(SecurityIgnored::class.java) }
.map { it.key }
// Method와 PathPattern만 필터링
.filter { it.methodsCondition.methods.isNotEmpty() && it.pathPatternsCondition?.patterns?.first() != null }
.map {
it.methodsCondition.methods.first()!!.asHttpMethod() to it.pathPatternsCondition!!.patterns.first()!!.patternString
}
}
2.3.3 Security에 적용하기
마지막으로 FilterChain에서 만들어둔 Method, Path 조합을 전부 적용시키면 된다.
@Configuration
class SecurityConfig(
private val handlerMapping: RequestMappingHandlerMapping
) {
@Bean
fun allowedMethodAndPathPattern() = // ...
@Bean
fun filterChain(http: HttpSecurity) = http
// ...
.authorizeHttpRequests { matchers ->
allowedMethodAndPathPattern().forEach { (method, path) ->
matchers.requestMatchers(method, path).permitAll()
}
}
.build()!!
}
3. 마치며
어노테이션과 HandlerMapping 빈을 통해 인증 해제를 간편하고 가독성 좋게 처리해봤다. 처리하고 나니 이 두 가지 조합이면 할 수 있는게 무궁무진할 것 같다는 생각이 든다.
전체 코드는 아래 레포에서 확인 가능하다.
'Spring' 카테고리의 다른 글
MySQL 네임드락으로 분산락 구성, AOP로 유연하게 적용하기 (0) | 2024.04.24 |
---|---|
예외 정보를 인터페이스와 infix함수로 확장성, 가독성 높게 관리하기 (0) | 2024.04.18 |
문자열 응답 시 공통 응답형식이 적용되지 않는 문제 해결하기 (0) | 2024.04.05 |
공통 형식의 응답 효율적으로 처리하기 (0) | 2024.04.04 |
Query에 포함된 다수의 Legacy Enum 우아하게 관리하기 (0) | 2024.04.04 |