Skip to content

Commit

Permalink
Merge pull request #26 from PSR-Co/feat/#25-logout-signout
Browse files Browse the repository at this point in the history
[feat] 로그아웃 및 회원탈퇴 API
  • Loading branch information
chaerlo127 authored Aug 1, 2023
2 parents 306f3ab + d931ffa commit 7bede12
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<DefaultSecurityFilterChain, HttpSecurity> () {
class JwtSecurityConfig (private val jwtUtils: JwtUtils, private val redisTemplate: RedisTemplate<String, String>): SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> () {
/**
* jwtFilter를 SecurityFilter 앞에 추가
*/
override fun configure(builder: HttpSecurity?) {
val jwtFilter = JwtFilter(jwtUtils)
val jwtFilter = JwtFilter(jwtUtils, redisTemplate)
builder!!.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +18,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher
@EnableWebSecurity
class WebSecurityConfig(
private val jwtUtils: JwtUtils,
private val redisTemplate: RedisTemplate<String, String>,
private val jwtAuthenticationEntryPoint: JwtAuthenticationEntryPoint,
private val jwtAccessDeniedHandler: JwtAccessDeniedHandler
) {
Expand Down Expand Up @@ -46,7 +48,7 @@ class WebSecurityConfig(
c.requestMatchers("/users/nickname").permitAll()
c.anyRequest().authenticated()
}
.apply(JwtSecurityConfig(jwtUtils))
.apply(JwtSecurityConfig(jwtUtils, redisTemplate))

return http.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "이미 가입되어 있는 휴대폰 번호입니다."),
Expand Down
13 changes: 11 additions & 2 deletions src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,34 @@ 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<String, String>) : OncePerRequestFilter() {

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
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) {
Expand Down
22 changes: 15 additions & 7 deletions src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 // 일주일

Expand Down Expand Up @@ -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())
}



}
26 changes: 26 additions & 0 deletions src/main/kotlin/com/psr/psr/user/controller/UserController.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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
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.*
Expand Down Expand Up @@ -62,4 +65,27 @@ class UserController(
return BaseResponse(BaseResponseCode.SUCCESS)
}

/**
* 로그아웃
*/
@PatchMapping("/logout")
@ResponseBody
fun logout(@AuthenticationPrincipal userAccount: UserAccount, request: HttpServletRequest) : BaseResponse<Any> {
userService.blackListToken(userAccount.getUser(), request, LOGOUT)
return BaseResponse(BaseResponseCode.SUCCESS)
}

/**
* 회원 탈퇴
*/
@DeleteMapping("/signout")
@ResponseBody
fun signOut(@AuthenticationPrincipal userAccount: UserAccount, request: HttpServletRequest) : BaseResponse<Any> {
userService.blackListToken(userAccount.getUser(), request, INACTIVE_STATUS)
userService.signOut(userAccount.getUser())
return BaseResponse(BaseResponseCode.SUCCESS)
}



}
29 changes: 23 additions & 6 deletions src/main/kotlin/com/psr/psr/user/service/UserService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.psr.psr.user.service

import com.psr.psr.global.Constant
import com.psr.psr.global.exception.BaseException
import com.psr.psr.global.exception.BaseResponseCode
import com.psr.psr.global.exception.BaseResponseCode.INVALID_PASSWORD
Expand All @@ -13,6 +14,7 @@ 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
Expand All @@ -21,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional
import java.util.regex.Pattern
import java.util.stream.Collectors


@Service
@Transactional(readOnly = true)
class UserService(
Expand Down Expand Up @@ -71,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)
Expand All @@ -100,5 +97,25 @@ class UserService(
userRepository.save(user)
}

// 토큰 자동 토큰 만료 및 RefreshToken 삭제
fun blackListToken(user: User, request: HttpServletRequest, loginStatus: String) {
val token = getHeaderAuthorization(request)
// 토큰 만료
jwtUtils.expireToken(token, loginStatus);
// refresh token 삭제
jwtUtils.deleteRefreshToken(user.id!!)
}

// 회원 탈퇴
fun signOut(user: User) {
// todo: cascade 적용 후 모두 삭제 되었는지 확인 필요
userRepository.delete(user)
}

/**
* header에서 token 불러오기
*/
private fun getHeaderAuthorization(request: HttpServletRequest): String {
return request.getHeader(Constant.JWT.AUTHORIZATION_HEADER)
}
}

0 comments on commit 7bede12

Please sign in to comment.