티스토리 뷰

썸네일을 이걸써야되는게 맞는지 모르겠다.

 

이번 프로젝트를 진행하면서, 스프링 시큐리티를 도입하기로 했다.

 

현재 서비스는 그래도 제법 덩치가 있어서 바로 도입이 어렵기 때문에, 작은 서비스에 먼저 적용해보자는 취지였다. 

 

결론부터 이야기하면, 현재 서비스 구조는 스프링 시큐리티에 필요한 데이터 구조를 가지고 있지 않았다.

 

이 과정에서 겪은 일들과 필터체인에 JWT를 엮는 일까지가 이번 포스팅이 될 것 같다.

 

우선 스프링 시큐리티가 뭔지부터 정리해보자.

 

스프링 시큐리티란?

스프링 시큐리티 공식 사이트에선 아래와 같이 스프링 시큐리티를 소개하고 있다.

Spring Security는 강력하고 사용자 정의가 가능한 인증 및 액세스 제어 프레임워크입니다. 이는 Spring 기반 애플리케이션 보안을 위한 사실상의 표준입니다.
Spring Security는 Java 애플리케이션에 인증과 권한 부여를 모두 제공하는 데 중점을 둔 프레임워크입니다. 모든 Spring 프로젝트와 마찬가지로 Spring Security의 진정한 힘은 사용자 정의 요구 사항을 충족하기 위해 얼마나 쉽게 확장할 수 있는지에 있습니다.

 

공식사이트에서 설명했듯이, 애플리케이션 보안을 위해서 인증과 권한 부여를 확장성 있게 지원한다.

 

선언적/명시적으로 웹 사이트의 보안 기능을 정의할 수 있어서 확장성이 뛰어나다는 점이 가장 큰 장점이라 생각한다.

 

스프링 시큐리티는 필터 체인(Filter Chain)을 통해서 인증/인가 과정을 거친다.

 

스프링 시큐리티의 필터 체인 구조

 

뭔가 많은데, 각각의 필터에 대한 소개는 https://gngsn.tistory.com/160 이 블로그에서 잘 정리해줬다.

 

필터 이름 그대로의 역할을 하고 있는 것 같다.

 

서두가 길었는데, 앞서 언급했듯이 이번 포스팅은 스프링 시큐리티에 JWT 인증 적용하는 법에 대해 알아보려 한다.

JWT 필터 적용하기

새로 만든 서비스는 기존에 있는 서비스의 JWT 토큰에서 필요한 정보를 추출해서 쓴다.

 

그래서 별도의 JWT Provider는 필요가 없고, JWT 값을 추출하는 부분만 필요하다.

 

그래서 JWT Filter는 이전 글에서 사용한 JWT Filter를 거의 그대로 사용하면 된다.

 

이 필터를 스프링 시큐리티의 필터체인에 등록해줘야한다.

@EnableWebSecurity
@Configuration
class SecurityConfig(
        private val jwtFilter: JwtFilter
) {
    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http.httpBasic { it.disable() }
                .csrf { it.disable() }
                .authorizeHttpRequests {
                    it.requestMatchers([인증이 필요한 URL]).authenticated()
                            .anyRequest().permitAll()
                }
                .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
        return http.build()
    }
}

필요없는 부분은 전부 disable 처리 했다.

 

일반적으로 JWT 필터는 UserNamePasswordAuthenticationFtiler 앞에 놓는다.

 

자 여기서 문제가 생긴다. 아래의 인증 과정을 확인해보자.

스프링 시큐리티의 인증 과정

 

인증 과정에서 UserNamePasswordAuthenticationFtiler를 통과하기 위해서는 UserNamePasswordAuthenticationToken이 필요하다.

 

그런데 문제는, 기존 서비스가 스프링 시큐리티를 쓰지 않고 있기 때문에 UsernamePasswordAuthenticationToken을 갖고 있지 않다는 점이다.

 

전달 할 수 있는 정보가 JWT 토큰 밖에 없다는 것이다.

 

결국 이 필터를 통과하기 위해서는 어쩔 수 없이 임의로 토큰을 생성해야 한다.

(어쩌면 스프링 시큐리티의 최대 장점 중 하나를 포기한 것이긴 하다.)

 

UsernamePasswordAuthenticationToken을 생성하려면 principals, credential, authorities 정보가 필요하다.

 

principals는 보통 사용자 정보를 의미하고, credential 인증에 필요한 자격증명(비밀번호)를 의미한다.

 

이 중에서 역할을 의미하는 authorities가 필요한데, authorities는 따로 커스터마이즈해야 한다.

CustomAuthority 생성하기

class CustomAuthority(
    private val id: String, 
    private val maker: String) : GrantedAuthority {
    override fun getAuthority(): String {
        return "CUSTOM_AUTHORITY_${id}_${maker}"
    }
}

GrantedAuthority를 상속받고 JWT 토큰으로부터 받을 수 있는 정보 생성자에 넣고 Authority를 생성하게 했다.

 

예를 들기 위해서 아무 값이나 넣고 만든 것이므로 필요한 정보를 넣고 만들면 된다.

 

그리고 jwtFilter에 이 인증 정보를 가져올 수 있는 매서드를 생성한다.

private fun getAuthentication(id: String, maker: String): Authentication {
    // 커스텀 인증 생성(임의의 값을 생성함)
    val customAuthority = CustomAuthority(id, maker)
    val authorities: Collection<GrantedAuthority> = listOf(customAuthority)

    return UsernamePasswordAuthenticationToken("", "", authorities)
}

마지막으로 SecurityContextHolder의 context에 생성한 인증 정보를 등록해서 사용하면 된다.

override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {

    val token = extractJwtTokenFromHeader(request)

    if (token != null && validateToken(token)) {
        val authentication = getAuthentication(token)
        SecurityContextHolder.getContext().authentication = authentication
    }

    filterChain.doFilter(request, response)
}

 

결과적으로 JwtFilter는 아래와 같이 구성된다.

@Component
class JwtFilter : OncePerRequestFilter() {
    @Value("\${custom.jwt.secret}")
    private val jwtSecret: String? = null

    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {

        val token = extractJwtTokenFromHeader(request)

        if (token != null && validateToken(token)) {
            val authentication = getAuthentication(token)
            SecurityContextHolder.getContext().authentication = authentication
        }

        filterChain.doFilter(request, response)
    }
    
        private fun getAuthentication(): Authentication {
        // 커스텀 인증 생성(임의의 값을 생성함)
        val customAuthority = CustomAuthority("", "polarishare")
        val authorities: Collection<GrantedAuthority> = listOf(customAuthority)

        return UsernamePasswordAuthenticationToken("", "", authorities)
    }

    fun extractJwtTokenFromHeader(request: HttpServletRequest): String? {
        val authorization = request.getHeader(HttpHeaders.AUTHORIZATION) ?: return null
        val parts = authorization.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
        return if (parts.size != 2) {
            null
        } else parts[1]
    }

    private fun validateToken(token: String): Boolean {
        try {
            getClaims(token)
            return true
        } catch (e: Exception) {
            when (e) {
                is SecurityException -> {}  // Invalid JWT Token
                is MalformedJwtException -> {}  // Invalid JWT Token
                is ExpiredJwtException -> {}    // Expired JWT Token
                is UnsupportedJwtException -> {}    // Unsupported JWT Token
                is IllegalArgumentException -> {}   // JWT claims string is empty
                else -> {}  // else
            }
            println(e.message)
        }
        return false
    }

    fun getClaims(token: String): Claims =
            Jwts.parser()
                    .setSigningKey(jwtSecret)
                    .parseClaimsJws(token)
                    .body

}

github

https://github.com/imsosleepy/spring-security-demo.git

마치며

사실 스프링 시큐리티에 대해 100% 알고 있는게 아니라 별로 포스팅하고 싶지 않았다.

 

그리고 스프링 시큐리티에서 제공하는 보안 관련 설정들을 대부분 disable하고 쓰고 있고,

 

인증 과정에서 사용해볼만한 토큰 검증도 임의 생성해서 써버렸기 때문에

 

결과적으로는 왜 적용했지?가 되버렸다.

 

그래도 스프링시큐리티의 인증 구조를 아예 몰랐는데(특히 role 관련 부분이 잘 이해 안됐었다),

 

찍먹이라도 해봤던 좋은? 경험이었다.

 

참고사이트

- https://gngsn.tistory.com/160

- https://mangkyu.tistory.com/77

이전 포스팅

스프링부트에서 JWT 적용하기 - 1. JWT란?

스프링부트에서 JWT 적용하기 - 2. 인터셉터(Interceptor)에 적용

스프링부트에서 JWT 적용하기 - 3. 필터(filter)에 적용

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함