From c5e21156115d76b9d939a57fb245d472808aad38 Mon Sep 17 00:00:00 2001 From: Hanbin Lee Date: Mon, 21 Nov 2022 00:55:12 +0900 Subject: [PATCH] =?UTF-8?q?[#24]=20feat:=20JWT=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20=EC=9D=B8=EA=B0=80=20=ED=94=84=EB=A1=9C=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인가 프로세스용 Security Filter 적용 - jwt token 유효성 체크 및 claim 조회를 통한 authentication 반환 메소드 추가 --- .../common/config/SecurityConfig.kt | 1 - .../dongnecafe/controller/TestController.kt | 13 ----- .../src/main/resources/application-local.yml | 2 +- .../dongnecafe/security/JwtTokenUtils.kt | 49 +++++++++++++--- .../common/config/SecurityConfig.kt | 16 +++++- .../security/JwtAuthenticationConfigurer.kt | 25 +++++++++ .../filter/JwtAuthenticationFilter.kt | 56 +++++++++++++++++++ .../src/main/resources/application-local.yml | 2 +- .../src/test/resources/application.yml | 2 +- 9 files changed, 140 insertions(+), 26 deletions(-) delete mode 100644 dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/controller/TestController.kt create mode 100644 dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtAuthenticationConfigurer.kt create mode 100644 dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/filter/JwtAuthenticationFilter.kt diff --git a/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt b/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt index 8ecd601..ca04bc2 100644 --- a/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt +++ b/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt @@ -22,7 +22,6 @@ class SecurityConfig { .authorizeRequests() .antMatchers("/auth/members/sign-up").permitAll() .antMatchers("/auth/authenticate").permitAll() - .antMatchers("/test").hasRole("USER") // 임시 인가 테스트용 .anyRequest().authenticated() .and() diff --git a/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/controller/TestController.kt b/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/controller/TestController.kt deleted file mode 100644 index b171393..0000000 --- a/dongne-account-api/src/main/kotlin/io/beaniejoy/dongnecafe/controller/TestController.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.beaniejoy.dongnecafe.controller - -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController - -@RestController -class TestController { - - @GetMapping("/test") - fun test(): String { - return "authorize OK!" - } -} \ No newline at end of file diff --git a/dongne-account-api/src/main/resources/application-local.yml b/dongne-account-api/src/main/resources/application-local.yml index 58a0493..eafd9db 100644 --- a/dongne-account-api/src/main/resources/application-local.yml +++ b/dongne-account-api/src/main/resources/application-local.yml @@ -5,4 +5,4 @@ spring: password: beaniejoy # TODO 추후 보안에 대해 생각해보기 jwt: secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo= - expiration_time: 86400 \ No newline at end of file + validity_time_in_sec: 86400 \ No newline at end of file diff --git a/dongne-common/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtTokenUtils.kt b/dongne-common/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtTokenUtils.kt index 69cf1e6..08bb800 100644 --- a/dongne-common/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtTokenUtils.kt +++ b/dongne-common/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtTokenUtils.kt @@ -1,11 +1,15 @@ package io.beaniejoy.dongnecafe.security +import io.jsonwebtoken.Claims +import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.security.Keys import mu.KLogging import org.springframework.beans.factory.annotation.Value +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.stereotype.Component import java.security.Key import java.util.* @@ -14,25 +18,56 @@ import java.util.* class JwtTokenUtils( @Value("\${jwt.secret_key}") private val secretKey: String, - @Value("\${jwt.expiration_time}") - private val expirationTime: Long + @Value("\${jwt.validity_time_in_sec}") + private val validityTimeSec: Long ) { private val key: Key = Keys.hmacShaKeyFor(secretKey.toByteArray()) + private val validityTimeMilliSec: Long = validityTimeSec * 1000 - companion object: KLogging() + companion object : KLogging() { + const val AUTHORITIES_KEY = "authorities" + } fun createToken(authentication: Authentication): String { + logger.info { "test = ${authentication.name}" } val authenticatedMember = (authentication.principal as SecurityUser).member + val authorities = authentication.authorities.joinToString(",") { it.authority } + val nowTime = Date().time - val expirationDate = Date(nowTime + this.expirationTime) + val expirationDate = Date(nowTime + this.validityTimeMilliSec) return Jwts.builder() .setSubject(authenticatedMember.email) - .claim("memberId", authenticatedMember.id) - .claim("email", authenticatedMember.email) - .claim("roles", authentication.authorities.joinToString(",") { it.authority }) + .claim(AUTHORITIES_KEY, authorities) .signWith(key, SignatureAlgorithm.HS256) .setExpiration(expirationDate) .compact() } + + fun getAuthentication(accessToken: String): Authentication? { + val claims = getValidTokenBody(accessToken) + ?: return null + + val authorities = claims[AUTHORITIES_KEY].toString().split(",") + .map { SimpleGrantedAuthority(it) } + + return UsernamePasswordAuthenticationToken(claims.subject, accessToken, authorities) + } + + // jwt access token 유효성 검증 및 claims 획득 + private fun getValidTokenBody(accessToken: String): Claims? { + return try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(accessToken) + .body + } catch (e: ExpiredJwtException) { + logger.error { "JWT access token expired. > Error: ${e.message}" } + null + } catch (e: Exception) { + logger.error { "JWT access token invalid. > Error: ${e.message}" } + null + } + } } \ No newline at end of file diff --git a/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt b/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt index 9dcf6be..1019ad6 100644 --- a/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt +++ b/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/common/config/SecurityConfig.kt @@ -1,5 +1,8 @@ package io.beaniejoy.dongnecafe.common.config +import io.beaniejoy.dongnecafe.security.JwtAuthenticationConfigurer +import io.beaniejoy.dongnecafe.security.JwtTokenUtils +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.security.servlet.PathRequest import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -11,19 +14,28 @@ import org.springframework.security.web.SecurityFilterChain @Configuration @EnableWebSecurity class SecurityConfig { + @Autowired + lateinit var jwtTokenUtils: JwtTokenUtils + @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { return http .csrf().disable() - .formLogin().disable() .authorizeRequests() - .anyRequest().authenticated() // 임시 허용 + .anyRequest().authenticated() .and() + .also { jwtAuthenticationConfigurer(it) } .build() } + private fun jwtAuthenticationConfigurer(http: HttpSecurity) { + http + .apply(JwtAuthenticationConfigurer()) + .jwtTokenUtils(jwtTokenUtils) + } + @Bean fun webSecurityCustomizer(): WebSecurityCustomizer { return WebSecurityCustomizer { web -> diff --git a/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtAuthenticationConfigurer.kt b/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtAuthenticationConfigurer.kt new file mode 100644 index 0000000..cbaa79a --- /dev/null +++ b/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/JwtAuthenticationConfigurer.kt @@ -0,0 +1,25 @@ +package io.beaniejoy.dongnecafe.security + +import io.beaniejoy.dongnecafe.security.filter.JwtAuthenticationFilter +import org.springframework.security.config.annotation.SecurityConfigurerAdapter +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.DefaultSecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +class JwtAuthenticationConfigurer : + SecurityConfigurerAdapter() { + private var jwtTokenUtils: JwtTokenUtils? = null + + override fun configure(http: HttpSecurity) { + http + .addFilterBefore( + JwtAuthenticationFilter(this.jwtTokenUtils!!), + UsernamePasswordAuthenticationFilter::class.java + ) + } + + fun jwtTokenUtils(jwtTokenUtils: JwtTokenUtils): JwtAuthenticationConfigurer { + this.jwtTokenUtils = jwtTokenUtils + return this + } +} \ No newline at end of file diff --git a/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/filter/JwtAuthenticationFilter.kt b/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/filter/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..11463d4 --- /dev/null +++ b/dongne-service-api/src/main/kotlin/io/beaniejoy/dongnecafe/security/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,56 @@ +package io.beaniejoy.dongnecafe.security.filter + +import io.beaniejoy.dongnecafe.security.JwtTokenUtils +import mu.KLogging +import mu.KotlinLogging +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.filter.GenericFilterBean +import javax.servlet.FilterChain +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse +import javax.servlet.http.HttpServletRequest + +class JwtAuthenticationFilter( + private val jwtTokenUtils: JwtTokenUtils +) : GenericFilterBean() { + private val log = KotlinLogging.logger {} + + companion object { + private const val AUTHORIZATION = "Authorization" + private const val BEARER = "Bearer" + private const val WHITESPACE = " " + } + + /** + * JWT access token 인가 처리 + */ + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + val httpRequest = request as HttpServletRequest + log.info { "[JwtAuthenticationFilter][${request.dispatcherType}][${request.requestURI}]" } + + getAccessToken(httpRequest)?.let { + jwtTokenUtils.getAuthentication(it) + }?.also { + SecurityContextHolder.getContext().authentication = it + log.info { "Valid Access Token [${it.name}]" } + } + + chain.doFilter(request, response) + } + + private fun getAccessToken(request: HttpServletRequest): String? { + val bearer = request.getHeader(AUTHORIZATION) + ?: return null + + val splitBearer = bearer.split(WHITESPACE) + if (splitBearer.first() != BEARER) { + return null + } + + if (splitBearer.size != 2 || splitBearer.last().isBlank()) { + return null + } + + return splitBearer.last() + } +} \ No newline at end of file diff --git a/dongne-service-api/src/main/resources/application-local.yml b/dongne-service-api/src/main/resources/application-local.yml index 58a0493..eafd9db 100644 --- a/dongne-service-api/src/main/resources/application-local.yml +++ b/dongne-service-api/src/main/resources/application-local.yml @@ -5,4 +5,4 @@ spring: password: beaniejoy # TODO 추후 보안에 대해 생각해보기 jwt: secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo= - expiration_time: 86400 \ No newline at end of file + validity_time_in_sec: 86400 \ No newline at end of file diff --git a/dongne-service-api/src/test/resources/application.yml b/dongne-service-api/src/test/resources/application.yml index 71426a6..9619c20 100644 --- a/dongne-service-api/src/test/resources/application.yml +++ b/dongne-service-api/src/test/resources/application.yml @@ -14,4 +14,4 @@ logging: jwt: secret_key: ZG9uZ25lLWNhZmUtcHJvamVjdC1rZXktZm9yLXRlc3QtY29kZQo - expiration_time: 60 \ No newline at end of file + validity_time_in_sec: 60 \ No newline at end of file