Finish authentication
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
package top.fatweb.api.config
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import org.springframework.context.annotation.Bean
|
||||
@@ -16,8 +19,15 @@ class RedisConfig {
|
||||
val redisTemplate = RedisTemplate<String, Any>()
|
||||
redisTemplate.connectionFactory = redisConnectionFactory
|
||||
val stringRedisSerializer = StringRedisSerializer()
|
||||
val objectMapper = ObjectMapper().registerModules(JavaTimeModule())
|
||||
val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer<Any>(objectMapper, Any::class.java)
|
||||
val objectMapper = ObjectMapper().registerModules(JavaTimeModule()).apply {
|
||||
setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
|
||||
activateDefaultTyping(
|
||||
this.polymorphicTypeValidator,
|
||||
ObjectMapper.DefaultTyping.NON_FINAL,
|
||||
JsonTypeInfo.As.PROPERTY
|
||||
)
|
||||
}
|
||||
val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(objectMapper, Any::class.java)
|
||||
|
||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
||||
redisTemplate.keySerializer = stringRedisSerializer
|
||||
|
||||
@@ -18,4 +18,8 @@ object SecurityConstants {
|
||||
var jwtKey = "FatWeb"
|
||||
|
||||
var jwtIssuer = "FatWeb"
|
||||
|
||||
var redisTtl = 20L
|
||||
|
||||
var redisTtlUnit = TimeUnit.MINUTES
|
||||
}
|
||||
@@ -2,30 +2,46 @@ package top.fatweb.api.controller.permission
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import top.fatweb.api.annotation.ApiVersion
|
||||
import top.fatweb.api.converter.UserConverter
|
||||
import top.fatweb.api.entity.common.ResponseCode
|
||||
import top.fatweb.api.entity.common.ResponseResult
|
||||
import top.fatweb.api.entity.converter.UserConverter
|
||||
import top.fatweb.api.entity.param.LoginParam
|
||||
import top.fatweb.api.param.LoginParam
|
||||
import top.fatweb.api.service.permission.IAuthenticationService
|
||||
import top.fatweb.api.util.WebUtil
|
||||
|
||||
@Tag(name = "身份认证", description = "身份认证相关接口")
|
||||
@Suppress("MVCPathVariableInspection")
|
||||
@RequestMapping("/api/{apiVersion}")
|
||||
@ApiVersion(2)
|
||||
@RestController
|
||||
class AuthenticationController(val loginService: IAuthenticationService, val userConverter: UserConverter) {
|
||||
class AuthenticationController(val authenticationService: IAuthenticationService, val userConverter: UserConverter) {
|
||||
@Operation(summary = "登录")
|
||||
@PostMapping("/login")
|
||||
fun login(@Valid @RequestBody loginParam: LoginParam) =
|
||||
ResponseResult.success(
|
||||
ResponseCode.SYSTEM_LOGIN_SUCCESS,
|
||||
"Login success",
|
||||
loginService.login(userConverter.loginParamToUser(loginParam))
|
||||
authenticationService.login(userConverter.loginParamToUser(loginParam))
|
||||
)
|
||||
|
||||
@Operation(summary = "登出")
|
||||
@PostMapping("/logout")
|
||||
fun logout(request: HttpServletRequest) =
|
||||
when (authenticationService.logout(WebUtil.getToken(request))) {
|
||||
true -> ResponseResult.success(ResponseCode.SYSTEM_LOGOUT_SUCCESS, "Logout success", null)
|
||||
false -> ResponseResult.fail(ResponseCode.SYSTEM_LOGOUT_FAILED, "Logout failed", null)
|
||||
}
|
||||
|
||||
@Operation(summary = "更新 Token")
|
||||
@GetMapping("/token")
|
||||
fun renewToken(request: HttpServletRequest) =
|
||||
ResponseResult.success(
|
||||
ResponseCode.SYSTEM_TOKEN_RENEW_SUCCESS,
|
||||
"Token renew success",
|
||||
authenticationService.renewToken(WebUtil.getToken(request))
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package top.fatweb.api.entity.converter
|
||||
package top.fatweb.api.converter
|
||||
|
||||
import org.mapstruct.Mapper
|
||||
import org.mapstruct.Mapping
|
||||
import org.mapstruct.Mappings
|
||||
import top.fatweb.api.entity.param.LoginParam
|
||||
import top.fatweb.api.entity.permission.User
|
||||
import top.fatweb.api.param.LoginParam
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
interface UserConverter {
|
||||
@@ -1,9 +1,11 @@
|
||||
package top.fatweb.api.entity.permission
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
class LoginUser() : UserDetails {
|
||||
lateinit var user: User
|
||||
|
||||
@@ -22,15 +24,21 @@ class LoginUser() : UserDetails {
|
||||
return authorities as List<GrantedAuthority>
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun getPassword(): String? = user.password
|
||||
|
||||
@JsonIgnore
|
||||
override fun getUsername(): String? = user.username
|
||||
|
||||
@JsonIgnore
|
||||
override fun isAccountNonExpired(): Boolean = true
|
||||
|
||||
@JsonIgnore
|
||||
override fun isAccountNonLocked(): Boolean = true
|
||||
|
||||
@JsonIgnore
|
||||
override fun isCredentialsNonExpired(): Boolean = true
|
||||
|
||||
@JsonIgnore
|
||||
override fun isEnabled(): Boolean = user.enable == 1
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import top.fatweb.api.entity.permission.LoginUser
|
||||
import top.fatweb.api.exception.TokenHasExpiredException
|
||||
import top.fatweb.api.util.JwtUtil
|
||||
import top.fatweb.api.util.RedisUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
import top.fatweb.api.util.WebUtil
|
||||
|
||||
@Component
|
||||
class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() {
|
||||
@@ -29,14 +29,14 @@ class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRe
|
||||
return
|
||||
}
|
||||
|
||||
val token = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix)
|
||||
val token = WebUtil.getToken(tokenWithPrefix)
|
||||
JwtUtil.parseJwt(token)
|
||||
|
||||
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32)
|
||||
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token
|
||||
val loginUser = redisUtil.getObject<LoginUser>(redisKey)
|
||||
loginUser ?: let { throw TokenHasExpiredException() }
|
||||
|
||||
redisUtil.setExpire(redisKey, 20, TimeUnit.MINUTES)
|
||||
redisUtil.setExpire(redisKey, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit)
|
||||
|
||||
val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package top.fatweb.api.handler
|
||||
|
||||
import com.auth0.jwt.exceptions.JWTDecodeException
|
||||
import com.auth0.jwt.exceptions.SignatureVerificationException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||
@@ -11,6 +13,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import top.fatweb.api.entity.common.ResponseCode
|
||||
import top.fatweb.api.entity.common.ResponseResult
|
||||
import top.fatweb.api.exception.TokenHasExpiredException
|
||||
|
||||
@RestControllerAdvice
|
||||
class ExceptionHandler {
|
||||
@@ -45,6 +48,16 @@ class ExceptionHandler {
|
||||
ResponseResult.fail(ResponseCode.SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR, e.localizedMessage, null)
|
||||
}
|
||||
|
||||
is SignatureVerificationException, is JWTDecodeException -> {
|
||||
log.debug(e.localizedMessage, e)
|
||||
ResponseResult.fail(ResponseCode.SYSTEM_TOKEN_ILLEGAL, "Token illegal", null)
|
||||
}
|
||||
|
||||
is TokenHasExpiredException -> {
|
||||
log.debug(e.localizedMessage, e)
|
||||
ResponseResult.fail(ResponseCode.SYSTEM_TOKEN_HAS_EXPIRED, e.localizedMessage, null)
|
||||
}
|
||||
|
||||
else -> {
|
||||
log.error(e.localizedMessage, e)
|
||||
ResponseResult.fail(ResponseCode.SYSTEM_ERROR, data = null)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package top.fatweb.api.entity.param
|
||||
package top.fatweb.api.param
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import java.io.Serializable
|
||||
|
||||
@Schema(description = "登录请求参数")
|
||||
class LoginParam : Serializable {
|
||||
|
||||
@Schema(description = "用户名", example = "test", required = true)
|
||||
@@ -1,11 +1,13 @@
|
||||
package top.fatweb.api.service.permission
|
||||
|
||||
import top.fatweb.api.entity.permission.User
|
||||
import top.fatweb.api.vo.LoginVo
|
||||
import top.fatweb.api.vo.TokenVo
|
||||
|
||||
interface IAuthenticationService {
|
||||
fun login(user: User): HashMap<String, String>
|
||||
fun login(user: User): LoginVo
|
||||
|
||||
fun logout(token: String): Boolean
|
||||
|
||||
fun renewToken(token: String): HashMap<String, String>
|
||||
fun renewToken(token: String): TokenVo
|
||||
}
|
||||
@@ -10,14 +10,15 @@ import top.fatweb.api.service.permission.IAuthenticationService
|
||||
import top.fatweb.api.util.JwtUtil
|
||||
import top.fatweb.api.util.RedisUtil
|
||||
import top.fatweb.api.util.WebUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
import top.fatweb.api.vo.LoginVo
|
||||
import top.fatweb.api.vo.TokenVo
|
||||
|
||||
@Service
|
||||
class AuthenticationServiceImpl(
|
||||
private val authenticationManager: AuthenticationManager,
|
||||
private val redisUtil: RedisUtil
|
||||
) : IAuthenticationService {
|
||||
override fun login(user: User): HashMap<String, String> {
|
||||
override fun login(user: User): LoginVo {
|
||||
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(user.username, user.password)
|
||||
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
|
||||
authentication ?: let {
|
||||
@@ -33,18 +34,17 @@ class AuthenticationServiceImpl(
|
||||
throw RuntimeException("Login failed")
|
||||
}
|
||||
|
||||
val hashMap = hashMapOf("token" to jwt)
|
||||
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32)
|
||||
redisUtil.setObject(redisKey, loginUser, 20, TimeUnit.MINUTES)
|
||||
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt
|
||||
redisUtil.setObject(redisKey, loginUser, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit)
|
||||
|
||||
return hashMap
|
||||
return LoginVo(jwt)
|
||||
}
|
||||
|
||||
override fun logout(token: String): Boolean =
|
||||
redisUtil.delObject("${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32))
|
||||
redisUtil.delObject("${SecurityConstants.jwtIssuer}_login:" + token)
|
||||
|
||||
override fun renewToken(token: String): HashMap<String, String> {
|
||||
val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32)
|
||||
override fun renewToken(token: String): TokenVo {
|
||||
val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token
|
||||
redisUtil.delObject(oldRedisKey)
|
||||
val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString())
|
||||
|
||||
@@ -52,10 +52,14 @@ class AuthenticationServiceImpl(
|
||||
throw RuntimeException("Login failed")
|
||||
}
|
||||
|
||||
val hashMap = hashMapOf("token" to jwt)
|
||||
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32)
|
||||
redisUtil.setObject(redisKey, WebUtil.getLoginUser(), 20, TimeUnit.MINUTES)
|
||||
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt
|
||||
redisUtil.setObject(
|
||||
redisKey,
|
||||
WebUtil.getLoginUser(),
|
||||
SecurityConstants.redisTtl,
|
||||
SecurityConstants.redisTtlUnit
|
||||
)
|
||||
|
||||
return hashMap
|
||||
return TokenVo(jwt)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
package top.fatweb.api.util
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import top.fatweb.api.constant.SecurityConstants
|
||||
import top.fatweb.api.entity.permission.LoginUser
|
||||
|
||||
object WebUtil {
|
||||
fun getLoginUser() = SecurityContextHolder.getContext().authentication.principal as LoginUser
|
||||
|
||||
fun getLoginUserId() = getLoginUser().user.id
|
||||
|
||||
fun getToken(tokenWithPrefix: String) = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix)
|
||||
|
||||
fun getToken(request: HttpServletRequest) = getToken(request.getHeader(SecurityConstants.headerString))
|
||||
}
|
||||
11
src/main/kotlin/top/fatweb/api/vo/LoginVo.kt
Normal file
11
src/main/kotlin/top/fatweb/api/vo/LoginVo.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package top.fatweb.api.vo
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
|
||||
@Schema(description = "登录返回参数")
|
||||
data class LoginVo(
|
||||
@Schema(
|
||||
description = "Token",
|
||||
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTllYjFkYmVmZDQ0OWRkOThlOGNjNzZlNzZkMDgyNSIsInN1YiI6IjE3MDk5ODYwNTg2Nzk5NzU5MzgiLCJpc3MiOiJGYXRXZWIiLCJpYXQiOjE2OTY1MjgxMTcsImV4cCI6MTY5NjUzNTMxN30.U2ZsyrGk7NbsP-DJfdz9xgWSfect5r2iKQnlEsscAA8"
|
||||
) val token: String
|
||||
)
|
||||
11
src/main/kotlin/top/fatweb/api/vo/TokenVo.kt
Normal file
11
src/main/kotlin/top/fatweb/api/vo/TokenVo.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package top.fatweb.api.vo
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
|
||||
@Schema(description = "Token")
|
||||
data class TokenVo(
|
||||
@Schema(
|
||||
description = "Token",
|
||||
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkYTllYjFkYmVmZDQ0OWRkOThlOGNjNzZlNzZkMDgyNSIsInN1YiI6IjE3MDk5ODYwNTg2Nzk5NzU5MzgiLCJpc3MiOiJGYXRXZWIiLCJpYXQiOjE2OTY1MjgxMTcsImV4cCI6MTY5NjUzNTMxN30.U2ZsyrGk7NbsP-DJfdz9xgWSfect5r2iKQnlEsscAA8"
|
||||
) val token: String
|
||||
)
|
||||
@@ -3,9 +3,11 @@ app:
|
||||
# header-string: "Authorization" # The key of head to get token
|
||||
# token-prefix: "Bearer " # Token prefix
|
||||
# jwt-ttl: 2 # The life of token
|
||||
# jwt-ttl-unit: hours # Unit of life of token
|
||||
# jwt-ttl-unit: hours # Unit of life of token [nanoseconds, microseconds, milliseconds, seconds, minutes, hours, days]
|
||||
jwt-key: $uuid$ # Key to generate token (Only numbers and letters allow)
|
||||
# jwt-issuer: FatWeb # Token issuer
|
||||
# redis-ttl: 20 # The life of token in redis
|
||||
# redis-ttl-unit: minutes # Unit of life of token in redis [nanoseconds, microseconds, milliseconds, seconds, minutes, hours, days]
|
||||
|
||||
server:
|
||||
# port: 8080 # Server port
|
||||
|
||||
Reference in New Issue
Block a user