From 6075f966ffe7086964f3d554eace77b074d58b92 Mon Sep 17 00:00:00 2001 From: chaerlo127 Date: Mon, 31 Jul 2023 20:08:58 +0900 Subject: [PATCH 1/3] =?UTF-8?q?#25=20feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B0=8F=20Redis=20Blacklist=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../psr/global/config/JwtSecurityConfig.kt | 5 ++-- .../psr/global/config/WebSecurityConfig.kt | 4 +++- .../psr/global/exception/BaseResponseCode.kt | 1 + .../com/psr/psr/global/jwt/JwtFilter.kt | 13 +++++++++-- .../com/psr/psr/global/jwt/utils/JwtUtils.kt | 22 ++++++++++++------ .../psr/psr/user/controller/UserController.kt | 12 ++++++++++ .../com/psr/psr/user/service/UserService.kt | 23 +++++++++++++++++++ 7 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/psr/psr/global/config/JwtSecurityConfig.kt b/src/main/kotlin/com/psr/psr/global/config/JwtSecurityConfig.kt index c02050d..1c222bb 100644 --- a/src/main/kotlin/com/psr/psr/global/config/JwtSecurityConfig.kt +++ b/src/main/kotlin/com/psr/psr/global/config/JwtSecurityConfig.kt @@ -3,18 +3,19 @@ package com.psr.psr.global.config import com.psr.psr.global.jwt.JwtFilter import com.psr.psr.global.jwt.utils.JwtUtils import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.core.RedisTemplate 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 @Configuration -class JwtSecurityConfig (private val jwtUtils: JwtUtils): SecurityConfigurerAdapter () { +class JwtSecurityConfig (private val jwtUtils: JwtUtils, private val redisTemplate: RedisTemplate): SecurityConfigurerAdapter () { /** * jwtFilter를 SecurityFilter 앞에 추가 */ override fun configure(builder: HttpSecurity?) { - val jwtFilter = JwtFilter(jwtUtils) + val jwtFilter = JwtFilter(jwtUtils, redisTemplate) builder!!.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) } } \ No newline at end of file diff --git a/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt b/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt index f9c3662..28b495e 100644 --- a/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt +++ b/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt @@ -5,6 +5,7 @@ import com.psr.psr.global.jwt.exception.JwtAuthenticationEntryPoint import com.psr.psr.global.jwt.utils.JwtUtils import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.core.RedisTemplate import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy @@ -17,6 +18,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher @EnableWebSecurity class WebSecurityConfig( private val jwtUtils: JwtUtils, + private val redisTemplate: RedisTemplate, private val jwtAuthenticationEntryPoint: JwtAuthenticationEntryPoint, private val jwtAccessDeniedHandler: JwtAccessDeniedHandler ) { @@ -46,7 +48,7 @@ class WebSecurityConfig( c.requestMatchers("/users/nickname").permitAll() c.anyRequest().authenticated() } - .apply(JwtSecurityConfig(jwtUtils)) + .apply(JwtSecurityConfig(jwtUtils, redisTemplate)) return http.build() } diff --git a/src/main/kotlin/com/psr/psr/global/exception/BaseResponseCode.kt b/src/main/kotlin/com/psr/psr/global/exception/BaseResponseCode.kt index acb5c94..a57b438 100644 --- a/src/main/kotlin/com/psr/psr/global/exception/BaseResponseCode.kt +++ b/src/main/kotlin/com/psr/psr/global/exception/BaseResponseCode.kt @@ -11,6 +11,7 @@ enum class BaseResponseCode(status: HttpStatus, message: String) { UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 형식의 토큰 값입니다."), MALFORMED_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 구조의 토큰 값입니다."), EXPIRED_TOKEN(HttpStatus.FORBIDDEN, "만료된 토큰 값입니다."), + BLACKLIST_TOKEN(HttpStatus.FORBIDDEN, "로그아웃 혹은 회원 탈퇴된 유저의 토큰 값입니다."), // user EXISTS_PHONE(HttpStatus.BAD_REQUEST, "이미 가입되어 있는 휴대폰 번호입니다."), diff --git a/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt b/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt index c0507ea..fc97897 100644 --- a/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt +++ b/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt @@ -4,15 +4,18 @@ import com.psr.psr.global.Constant.JWT.JWT.AUTHORIZATION_HEADER import com.psr.psr.global.Constant.JWT.JWT.BEARER_PREFIX import com.psr.psr.global.dto.BaseResponse import com.psr.psr.global.exception.BaseException +import com.psr.psr.global.exception.BaseResponseCode import com.psr.psr.global.jwt.utils.JwtUtils import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import org.springframework.data.redis.core.RedisTemplate import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.util.ObjectUtils import org.springframework.util.StringUtils import org.springframework.web.filter.OncePerRequestFilter -class JwtFilter(private val jwtUtils: JwtUtils) : OncePerRequestFilter() { +class JwtFilter(private val jwtUtils: JwtUtils, private val redisTemplate: RedisTemplate) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, @@ -20,9 +23,15 @@ class JwtFilter(private val jwtUtils: JwtUtils) : OncePerRequestFilter() { filterChain: FilterChain ) { try{ + // header 에서 token 꺼내기 val token = resolveToken(request) + // 유효한 token 인지 확인 if(StringUtils.hasText(token) && jwtUtils.validateToken(token)){ - val authentication = jwtUtils.getAuthentication(token!!) + // 이미 blacklist 가 된 토큰인지 아닌지 확인 + if(!ObjectUtils.isEmpty(redisTemplate.opsForValue().get(token!!))){ + throw BaseException(BaseResponseCode.BLACKLIST_TOKEN) + } + val authentication = jwtUtils.getAuthentication(token) SecurityContextHolder.getContext().authentication = authentication } } catch (e: BaseException) { diff --git a/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt b/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt index 5b12f50..fb89e60 100644 --- a/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt +++ b/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt @@ -34,7 +34,6 @@ class JwtUtils( ) { val logger = KotlinLogging.logger {} - // todo: 암호화시켜 yaml 파일에 넣어두기 private final val ACCESS_TOKEN_EXPIRE_TIME: Long = 1000L * 60 * 30 // 30 분 private final val REFRESH_TOKEN_EXPIRE_TIME: Long = 1000L * 60 * 60 * 24 * 7 // 일주일 @@ -111,20 +110,29 @@ class JwtUtils( /** * 토큰 만료 */ - private fun expireToken(token: String, status: String){ - val refreshToken = token.replace(BEARER_PREFIX, "") - val expiration = getExpiration(refreshToken) - redisTemplate.opsForValue().set(refreshToken, status, expiration, TimeUnit.MILLISECONDS) + fun expireToken(token: String, status: String){ + val accessToken = token.replace(BEARER_PREFIX, "") + val expiration = getExpiration(accessToken) + redisTemplate.opsForValue().set(accessToken, status, expiration, TimeUnit.MILLISECONDS) } /** * Token 남은 시간 return */ - fun getExpiration(token: String): Long { + private fun getExpiration(token: String): Long { val expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).body.expiration val now = Date().time - return expiration.time - now + return (expiration.time - now) } + /** + * refresh token 삭제 + */ + fun deleteRefreshToken(userId: Long) { + if(redisTemplate.opsForValue().get(userId.toString()) != null) redisTemplate.delete(userId.toString()) + } + + + } \ No newline at end of file diff --git a/src/main/kotlin/com/psr/psr/user/controller/UserController.kt b/src/main/kotlin/com/psr/psr/user/controller/UserController.kt index 734c70d..5e8d90f 100644 --- a/src/main/kotlin/com/psr/psr/user/controller/UserController.kt +++ b/src/main/kotlin/com/psr/psr/user/controller/UserController.kt @@ -6,9 +6,11 @@ import com.psr.psr.global.jwt.UserAccount import com.psr.psr.global.jwt.dto.TokenRes import com.psr.psr.user.dto.* import com.psr.psr.user.service.UserService +import jakarta.servlet.http.HttpServletRequest import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* +import java.net.http.HttpRequest @RestController @RequestMapping("/users") @@ -62,4 +64,14 @@ class UserController( return BaseResponse(BaseResponseCode.SUCCESS) } + /** + * 로그아웃 + */ + @PatchMapping("/logout") + @ResponseBody + fun logout(@AuthenticationPrincipal userAccount: UserAccount, request: HttpServletRequest) : BaseResponse { + userService.logout(userAccount.getUser(), request) + return BaseResponse(BaseResponseCode.SUCCESS) + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/psr/psr/user/service/UserService.kt b/src/main/kotlin/com/psr/psr/user/service/UserService.kt index efafd2f..02c5644 100644 --- a/src/main/kotlin/com/psr/psr/user/service/UserService.kt +++ b/src/main/kotlin/com/psr/psr/user/service/UserService.kt @@ -1,5 +1,7 @@ package com.psr.psr.user.service +import com.psr.psr.global.Constant +import com.psr.psr.global.Constant.USER_STATUS.USER_STATUS.LOGOUT import com.psr.psr.global.exception.BaseException import com.psr.psr.global.exception.BaseResponseCode import com.psr.psr.global.exception.BaseResponseCode.INVALID_PASSWORD @@ -13,14 +15,17 @@ import com.psr.psr.user.dto.SignUpReq import com.psr.psr.user.entity.User import com.psr.psr.user.repository.UserInterestRepository import com.psr.psr.user.repository.UserRepository +import jakarta.servlet.http.HttpServletRequest import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.net.http.HttpRequest import java.util.regex.Pattern import java.util.stream.Collectors + @Service @Transactional(readOnly = true) class UserService( @@ -100,5 +105,23 @@ class UserService( userRepository.save(user) } + // 로그아웃 + fun logout(user: User, request: HttpServletRequest) { + val token = getHeaderAuthorization(request) + // 토큰 만료 + jwtUtils.expireToken(token, LOGOUT); + // refresh token 삭제 + jwtUtils.deleteRefreshToken(user.id!!) + } + + /** + * header에서 token 불러오기 + */ + fun getHeaderAuthorization(request: HttpServletRequest): String { + return request.getHeader(Constant.JWT.AUTHORIZATION_HEADER) + } + + + } \ No newline at end of file From f61eb61391aa874ee1dfa0d869ceb38e50221eb7 Mon Sep 17 00:00:00 2001 From: chaerlo127 Date: Mon, 31 Jul 2023 20:16:23 +0900 Subject: [PATCH 2/3] =?UTF-8?q?#25=20feat:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../psr/psr/user/controller/UserController.kt | 18 +++++++++++++++-- .../com/psr/psr/user/service/UserService.kt | 20 +++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/psr/psr/user/controller/UserController.kt b/src/main/kotlin/com/psr/psr/user/controller/UserController.kt index 5e8d90f..aaf4d30 100644 --- a/src/main/kotlin/com/psr/psr/user/controller/UserController.kt +++ b/src/main/kotlin/com/psr/psr/user/controller/UserController.kt @@ -1,5 +1,7 @@ package com.psr.psr.user.controller +import com.psr.psr.global.Constant.USER_STATUS.USER_STATUS.INACTIVE_STATUS +import com.psr.psr.global.Constant.USER_STATUS.USER_STATUS.LOGOUT import com.psr.psr.global.dto.BaseResponse import com.psr.psr.global.exception.BaseResponseCode import com.psr.psr.global.jwt.UserAccount @@ -10,7 +12,6 @@ import jakarta.servlet.http.HttpServletRequest import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* -import java.net.http.HttpRequest @RestController @RequestMapping("/users") @@ -70,8 +71,21 @@ class UserController( @PatchMapping("/logout") @ResponseBody fun logout(@AuthenticationPrincipal userAccount: UserAccount, request: HttpServletRequest) : BaseResponse { - userService.logout(userAccount.getUser(), request) + userService.blackListToken(userAccount.getUser(), request, LOGOUT) return BaseResponse(BaseResponseCode.SUCCESS) } + /** + * 회원 탈퇴 + */ + @DeleteMapping("/signout") + @ResponseBody + fun signOut(@AuthenticationPrincipal userAccount: UserAccount, request: HttpServletRequest) : BaseResponse { + userService.blackListToken(userAccount.getUser(), request, INACTIVE_STATUS) + userService.signOut(userAccount.getUser()) + return BaseResponse(BaseResponseCode.SUCCESS) + } + + + } \ No newline at end of file diff --git a/src/main/kotlin/com/psr/psr/user/service/UserService.kt b/src/main/kotlin/com/psr/psr/user/service/UserService.kt index 02c5644..2a143d2 100644 --- a/src/main/kotlin/com/psr/psr/user/service/UserService.kt +++ b/src/main/kotlin/com/psr/psr/user/service/UserService.kt @@ -1,7 +1,6 @@ package com.psr.psr.user.service import com.psr.psr.global.Constant -import com.psr.psr.global.Constant.USER_STATUS.USER_STATUS.LOGOUT import com.psr.psr.global.exception.BaseException import com.psr.psr.global.exception.BaseResponseCode import com.psr.psr.global.exception.BaseResponseCode.INVALID_PASSWORD @@ -21,7 +20,6 @@ import org.springframework.security.config.annotation.authentication.builders.Au import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.net.http.HttpRequest import java.util.regex.Pattern import java.util.stream.Collectors @@ -105,23 +103,25 @@ class UserService( userRepository.save(user) } - // 로그아웃 - fun logout(user: User, request: HttpServletRequest) { + // 토큰 자동 토큰 만료 및 RefreshToken 삭제 + fun blackListToken(user: User, request: HttpServletRequest, loginStatus: String) { val token = getHeaderAuthorization(request) // 토큰 만료 - jwtUtils.expireToken(token, LOGOUT); + jwtUtils.expireToken(token, loginStatus); // refresh token 삭제 jwtUtils.deleteRefreshToken(user.id!!) } + // 회원 탈퇴 + fun signOut(user: User) { + // todo: cascade 적용 후 모두 삭제 되었는지 확인 필요 + userRepository.delete(user) + } + /** * header에서 token 불러오기 */ - fun getHeaderAuthorization(request: HttpServletRequest): String { + private fun getHeaderAuthorization(request: HttpServletRequest): String { return request.getHeader(Constant.JWT.AUTHORIZATION_HEADER) } - - - - } \ No newline at end of file From d931ffaa947e53e40ee0b64d16513d3127a5d51e Mon Sep 17 00:00:00 2001 From: chaerlo127 Date: Mon, 31 Jul 2023 20:16:40 +0900 Subject: [PATCH 3/3] =?UTF-8?q?#25=20chore:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20method=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/psr/psr/user/service/UserService.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/kotlin/com/psr/psr/user/service/UserService.kt b/src/main/kotlin/com/psr/psr/user/service/UserService.kt index 2a143d2..3e74f90 100644 --- a/src/main/kotlin/com/psr/psr/user/service/UserService.kt +++ b/src/main/kotlin/com/psr/psr/user/service/UserService.kt @@ -74,12 +74,6 @@ class UserService( return userRepository.existsByNickname(nickname) } - // 정규 표현식 확인 extract method - private fun isValidRegularExpression(word: String, validation: String) : Boolean{ - val pattern = Pattern.compile(validation) - return pattern.matcher(word).matches() - } - // token 생성 extract method private fun createToken(user: User, password: String): TokenRes { val authenticationToken = UsernamePasswordAuthenticationToken(user.id.toString(), password)