시큐리티 설정
유저 API - 로그인, 회원가입, 계정정보 추가
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package kr.co.vividnext.sodalive.jwt
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException
|
||||
import org.springframework.security.web.access.AccessDeniedHandler
|
||||
import org.springframework.stereotype.Component
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
class JwtAccessDeniedHandler : AccessDeniedHandler {
|
||||
override fun handle(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
accessDeniedException: AccessDeniedException
|
||||
) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN)
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package kr.co.vividnext.sodalive.jwt
|
||||
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.stereotype.Component
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
class JwtAuthenticationEntryPoint : AuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException
|
||||
) {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
||||
}
|
||||
}
|
45
src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt
Normal file
45
src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package kr.co.vividnext.sodalive.jwt
|
||||
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.util.StringUtils
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class JwtFilter(private val tokenProvider: TokenProvider) : OncePerRequestFilter() {
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain
|
||||
) {
|
||||
val jwt = resolveToken(request)
|
||||
val requestURI = request.requestURI
|
||||
|
||||
if (jwt != null && StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
|
||||
val authentication = tokenProvider.getAuthentication(jwt)
|
||||
SecurityContextHolder.getContext().authentication = authentication
|
||||
logger.debug("Security Context에 '${authentication.name}' 인증정보를 저장했습니다, uri: $requestURI")
|
||||
} else {
|
||||
logger.debug("유효한 JWT 토큰이 없습니다., uri: $requestURI")
|
||||
if (response.status != 200) {
|
||||
response.status = 401
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
|
||||
private fun resolveToken(request: HttpServletRequest): String? {
|
||||
val bearerToken = request.getHeader(AUTHORIZATION_HEADER)
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val AUTHORIZATION_HEADER = "Authorization"
|
||||
}
|
||||
}
|
133
src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt
Normal file
133
src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt
Normal file
@@ -0,0 +1,133 @@
|
||||
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<Long, ReentrantReadWriteLock> = 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, listOf())
|
||||
|
||||
val memberTokenSet = memberToken.tokenList.toMutableSet()
|
||||
memberTokenSet.add(token)
|
||||
memberToken.tokenList = memberTokenSet.toList()
|
||||
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("로그인 정보를 확인해주세요.")
|
||||
|
||||
if (!memberToken.tokenList.contains(token)) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
val member = repository.findByIdOrNull(id = claims.subject.toLong())
|
||||
?: throw SodaException("로그인 정보를 확인해주세요.")
|
||||
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"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user