Finish authentication

This commit is contained in:
2023-10-06 14:52:03 +08:00
parent 79e65f0785
commit 03534e4fa9
14 changed files with 121 additions and 33 deletions

View File

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

View File

@@ -18,4 +18,8 @@ object SecurityConstants {
var jwtKey = "FatWeb"
var jwtIssuer = "FatWeb"
var redisTtl = 20L
var redisTtlUnit = TimeUnit.MINUTES
}

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
)

View 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
)