diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt new file mode 100644 index 0000000..a8ab7ee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.common + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.web.filter.OncePerRequestFilter +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class ExceptionHandlerFilter(private val objectMapper: ObjectMapper) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + filterChain.doFilter(request, response) + } catch (e: Exception) { + response.status = 401 + response.contentType = "application/json" + response.characterEncoding = "UTF-8" + + val json = objectMapper.writeValueAsString(ApiResponse.error("로그인 정보를 확인해주세요.")) + response.writer.write(json) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt index aae4472..172a24a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -1,5 +1,7 @@ package kr.co.vividnext.sodalive.configs +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.common.ExceptionHandlerFilter import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler import kr.co.vividnext.sodalive.jwt.JwtAuthenticationEntryPoint import kr.co.vividnext.sodalive.jwt.JwtFilter @@ -21,6 +23,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) class SecurityConfig( + private val objectMapper: ObjectMapper, private val tokenProvider: TokenProvider, private val accessDeniedHandler: JwtAccessDeniedHandler, private val authenticationEntryPoint: JwtAuthenticationEntryPoint @@ -69,6 +72,7 @@ class SecurityConfig( .anyRequest().authenticated() .and() .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) + .addFilterBefore(ExceptionHandlerFilter(objectMapper), JwtFilter::class.java) .build() } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 5979817..6d12515 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestPart @@ -29,6 +30,16 @@ class MemberController(private val service: MemberService) { @PostMapping("/login") fun login(@RequestBody loginRequest: LoginRequest) = service.login(loginRequest) + @PostMapping("/logout") + fun logout( + @RequestHeader("Authorization") token: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.logout(token.removePrefix("Bearer "), member.id!!)) + } + @GetMapping("/info") fun getMemberInfo( @RequestParam container: String?, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index b7feb09..1b59996 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository import kr.co.vividnext.sodalive.member.stipulation.StipulationIds import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository +import kr.co.vividnext.sodalive.member.token.MemberTokenRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull @@ -38,11 +39,14 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.write @Service @Transactional(readOnly = true) class MemberService( private val repository: MemberRepository, + private val tokenRepository: MemberTokenRepository, private val stipulationRepository: StipulationRepository, private val stipulationAgreeRepository: StipulationAgreeRepository, private val creatorFollowingRepository: CreatorFollowingRepository, @@ -64,6 +68,9 @@ class MemberService( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) : UserDetailsService { + + private val tokenLocks: MutableMap = mutableMapOf() + @Transactional fun signUp( profileImage: MultipartFile?, @@ -349,4 +356,27 @@ class MemberService( } .toList() } + + @Transactional + fun logout(token: String, memberId: Long) { + val member = repository.findByIdOrNull(memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + member.pushToken = null + + val lock = getOrCreateLock(memberId = memberId) + lock.write { + val memberToken = tokenRepository.findByIdOrNull(memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + val memberTokenSet = memberToken.tokenList.toMutableSet() + memberTokenSet.remove(token) + memberToken.tokenList = memberTokenSet.toList() + tokenRepository.save(memberToken) + } + } + + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { + return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } + } }