시큐리티 설정

유저 API - 로그인, 회원가입, 계정정보 추가
This commit is contained in:
2023-07-23 03:26:17 +09:00
parent 23506e79f1
commit f81f07bd05
36 changed files with 1247 additions and 0 deletions

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View 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"
}
}

View 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"
}
}