[Spring Boot] Filter와 Interceptor를 이용한 로그인 확인

Spring Boot

Language :

전제: 로그인 시 발급되는 세션 키를 header에 가지고 있는 사용자만 사이트 이용 가능

 Annotation

로그인이 필요한 서비스는 Controller Method에 Annotation을 붙인다.

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LoginCheck()

Exception

세션 키가 없으면 401 에러를 발생시킨다.

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus

@ResponseStatus(HttpStatus.UNAUTHORIZED, reason = "Unauthorized")
class UnauthorizedException : RuntimeException()

Filter

Header에 유효한 세션 키가 있는 지 확인하여 Request에 저장한다.

import com.dev.service.SessionService
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
import org.springframework.util.Base64Utils
import org.springframework.web.filter.OncePerRequestFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class SessionInjectionFilter(
    private val sessionService: SessionService,
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) {
        sessionInjection(request)
        filterChain.doFilter(request, response)
    }

    private fun sessionInjection(request: HttpServletRequest) {
        val header = request.getHeader(HttpHeaders.AUTHORIZATION) ?: return
        val authType = "Bearer "
        if (!header.startsWith(authType)) return

        val encryptedSessionKey = header.removePrefix(authType)
        val sessionKey = String(Base64Utils.decodeFromString(encryptedSessionKey))

        val session = sessionService.getSession(sessionKey) ?: return
        request.setAttribute("session", session)
    }

}

Interceptor

Controller로 요청이 넘겨지기 전에 Method에 Annotation LoginCheck가 붙어 있으면 로그인을 확인한다.

import com.dev.annotation.LoginCheck
import com.dev.exception.UnauthorizedException
import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class LoginCheckInterceptor : HandlerInterceptor {

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        if (handler !is HandlerMethod) return true
        handler.getMethodAnnotation(LoginCheck::class.java) ?: return true
        if (request.getAttribute("session") == null) throw UnauthorizedException()
        return true
    }

}

WebMvcConfigurer

Custom Interceptor를 등록한다.

@Configuration
class WebMvcConfig : WebMvcConfigurer {

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(LoginCheckInterceptor())
            .addPathPatterns("/api/v1/session")
    }

}

Spring 2.x 에서 정적 리소스를 요청할 때에도 Interceptor 가 호출되기 때문에 HandlerMethod가 전달되는 Url을 지정해 주어야 오류가 발생하지 않는다.

java.lang.ClassCastException: class org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to class 

Controller

@LoginCheck
@GetMapping("/session")
fun getSession(request: HttpServletRequest): ResponseEntity<Any> {
    val session = request.getAttribute("session") as Session

    val result = sessionService.sessionKeyValidation(session)
        ?: return ResponseEntity.ok("The session has expired")

    return ResponseEntity.ok(result)
}

민갤

Back-End Developer

백엔드 개발자입니다.