package kr.co.vividnext.sodalive.jwt import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.Jwts import io.jsonwebtoken.MalformedJwtException import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.UnsupportedJwtException import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.SignatureException import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.MemberAdapter import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.token.MemberToken import kr.co.vividnext.sodalive.member.token.MemberTokenRepository import org.slf4j.LoggerFactory import org.springframework.beans.factory.InitializingBean import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.stereotype.Component import java.security.Key import java.util.Date import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.write // 토큰의 생성, 유효성 검증 담당 클래스 @Component class TokenProvider( @Value("\${jwt.secret}") private val secret: String, @Value("\${jwt.token-validity-in-seconds}") private val tokenValidityInSeconds: Long, private val repository: MemberRepository, private val tokenRepository: MemberTokenRepository ) : InitializingBean { private val logger = LoggerFactory.getLogger(TokenProvider::class.java) private val tokenValidityInMilliseconds: Long = tokenValidityInSeconds * 1000 private val tokenLocks: MutableMap = mutableMapOf() private lateinit var key: Key override fun afterPropertiesSet() { val keyBytes = Decoders.BASE64.decode(secret) this.key = Keys.hmacShaKeyFor(keyBytes) } fun createToken(authentication: Authentication, memberId: Long): String { val authorities = authentication.authorities .joinToString(separator = ",", transform = GrantedAuthority::getAuthority) val now = Date().time val validity = Date(now + tokenValidityInMilliseconds) val token = Jwts.builder() .setSubject(memberId.toString()) .claim(AUTHORITIES_KEY, authorities) .signWith(key, SignatureAlgorithm.HS512) .setExpiration(validity) .compact() val lock = getOrCreateLock(memberId = memberId) lock.write { val memberToken = tokenRepository.findByIdOrNull(memberId) ?: MemberToken(id = memberId) memberToken.tokenSet.add(token) tokenRepository.save(memberToken) } return token } fun getAuthentication(token: String): Authentication { val claims = Jwts .parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .body val authorities = claims[AUTHORITIES_KEY].toString().split(",").map { SimpleGrantedAuthority(it) } val memberToken = tokenRepository.findByIdOrNull(id = claims.subject.toLong()) ?: throw SodaException(messageKey = "common.error.bad_credentials") if (!memberToken.tokenSet.contains(token)) { throw SodaException(messageKey = "common.error.bad_credentials") } val member = repository.findByIdOrNull(id = claims.subject.toLong()) ?: throw SodaException(messageKey = "common.error.bad_credentials") val principal = MemberAdapter(member) return UsernamePasswordAuthenticationToken(principal, token, authorities) } fun validateToken(token: String): Boolean { try { Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) return true } catch (e: SecurityException) { logger.info("잘못된 JWT 서명입니다.") } catch (e: MalformedJwtException) { logger.info("잘못된 JWT 서명입니다.") } catch (e: ExpiredJwtException) { logger.info("만료된 JWT 서명입니다.") } catch (e: UnsupportedJwtException) { logger.info("지원되지 않는 JWT 서명입니다.") } catch (e: IllegalArgumentException) { logger.info("JWT 토큰이 잘못되었습니다.") } catch (e: SignatureException) { logger.info("잘못된 JWT 서명입니다.") } return false } private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } } companion object { private const val AUTHORITIES_KEY = "auth" } }