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 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.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
@@ -16,8 +19,15 @@ class RedisConfig {
val redisTemplate = RedisTemplate<String, Any>() val redisTemplate = RedisTemplate<String, Any>()
redisTemplate.connectionFactory = redisConnectionFactory redisTemplate.connectionFactory = redisConnectionFactory
val stringRedisSerializer = StringRedisSerializer() val stringRedisSerializer = StringRedisSerializer()
val objectMapper = ObjectMapper().registerModules(JavaTimeModule()) val objectMapper = ObjectMapper().registerModules(JavaTimeModule()).apply {
val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer<Any>(objectMapper, Any::class.java) 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值 // 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.keySerializer = stringRedisSerializer redisTemplate.keySerializer = stringRedisSerializer

View File

@@ -18,4 +18,8 @@ object SecurityConstants {
var jwtKey = "FatWeb" var jwtKey = "FatWeb"
var jwtIssuer = "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.Operation
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import top.fatweb.api.annotation.ApiVersion 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.ResponseCode
import top.fatweb.api.entity.common.ResponseResult import top.fatweb.api.entity.common.ResponseResult
import top.fatweb.api.entity.converter.UserConverter import top.fatweb.api.param.LoginParam
import top.fatweb.api.entity.param.LoginParam
import top.fatweb.api.service.permission.IAuthenticationService import top.fatweb.api.service.permission.IAuthenticationService
import top.fatweb.api.util.WebUtil
@Tag(name = "身份认证", description = "身份认证相关接口") @Tag(name = "身份认证", description = "身份认证相关接口")
@Suppress("MVCPathVariableInspection") @Suppress("MVCPathVariableInspection")
@RequestMapping("/api/{apiVersion}") @RequestMapping("/api/{apiVersion}")
@ApiVersion(2) @ApiVersion(2)
@RestController @RestController
class AuthenticationController(val loginService: IAuthenticationService, val userConverter: UserConverter) { class AuthenticationController(val authenticationService: IAuthenticationService, val userConverter: UserConverter) {
@Operation(summary = "登录") @Operation(summary = "登录")
@PostMapping("/login") @PostMapping("/login")
fun login(@Valid @RequestBody loginParam: LoginParam) = fun login(@Valid @RequestBody loginParam: LoginParam) =
ResponseResult.success( ResponseResult.success(
ResponseCode.SYSTEM_LOGIN_SUCCESS, ResponseCode.SYSTEM_LOGIN_SUCCESS,
"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.Mapper
import org.mapstruct.Mapping import org.mapstruct.Mapping
import org.mapstruct.Mappings import org.mapstruct.Mappings
import top.fatweb.api.entity.param.LoginParam
import top.fatweb.api.entity.permission.User import top.fatweb.api.entity.permission.User
import top.fatweb.api.param.LoginParam
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
interface UserConverter { interface UserConverter {

View File

@@ -1,9 +1,11 @@
package top.fatweb.api.entity.permission package top.fatweb.api.entity.permission
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonTypeInfo
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
class LoginUser() : UserDetails { class LoginUser() : UserDetails {
lateinit var user: User lateinit var user: User
@@ -22,15 +24,21 @@ class LoginUser() : UserDetails {
return authorities as List<GrantedAuthority> return authorities as List<GrantedAuthority>
} }
@JsonIgnore
override fun getPassword(): String? = user.password override fun getPassword(): String? = user.password
@JsonIgnore
override fun getUsername(): String? = user.username override fun getUsername(): String? = user.username
@JsonIgnore
override fun isAccountNonExpired(): Boolean = true override fun isAccountNonExpired(): Boolean = true
@JsonIgnore
override fun isAccountNonLocked(): Boolean = true override fun isAccountNonLocked(): Boolean = true
@JsonIgnore
override fun isCredentialsNonExpired(): Boolean = true override fun isCredentialsNonExpired(): Boolean = true
@JsonIgnore
override fun isEnabled(): Boolean = user.enable == 1 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.exception.TokenHasExpiredException
import top.fatweb.api.util.JwtUtil import top.fatweb.api.util.JwtUtil
import top.fatweb.api.util.RedisUtil import top.fatweb.api.util.RedisUtil
import java.util.concurrent.TimeUnit import top.fatweb.api.util.WebUtil
@Component @Component
class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() { class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() {
@@ -29,14 +29,14 @@ class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRe
return return
} }
val token = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix) val token = WebUtil.getToken(tokenWithPrefix)
JwtUtil.parseJwt(token) JwtUtil.parseJwt(token)
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token
val loginUser = redisUtil.getObject<LoginUser>(redisKey) val loginUser = redisUtil.getObject<LoginUser>(redisKey)
loginUser ?: let { throw TokenHasExpiredException() } loginUser ?: let { throw TokenHasExpiredException() }
redisUtil.setExpire(redisKey, 20, TimeUnit.MINUTES) redisUtil.setExpire(redisKey, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit)
val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities) val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities)
SecurityContextHolder.getContext().authentication = authenticationToken SecurityContextHolder.getContext().authentication = authenticationToken

View File

@@ -1,5 +1,7 @@
package top.fatweb.api.handler package top.fatweb.api.handler
import com.auth0.jwt.exceptions.JWTDecodeException
import com.auth0.jwt.exceptions.SignatureVerificationException
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.converter.HttpMessageNotReadableException 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 org.springframework.web.bind.annotation.RestControllerAdvice
import top.fatweb.api.entity.common.ResponseCode import top.fatweb.api.entity.common.ResponseCode
import top.fatweb.api.entity.common.ResponseResult import top.fatweb.api.entity.common.ResponseResult
import top.fatweb.api.exception.TokenHasExpiredException
@RestControllerAdvice @RestControllerAdvice
class ExceptionHandler { class ExceptionHandler {
@@ -45,6 +48,16 @@ class ExceptionHandler {
ResponseResult.fail(ResponseCode.SYSTEM_LOGIN_USERNAME_PASSWORD_ERROR, e.localizedMessage, null) 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 -> { else -> {
log.error(e.localizedMessage, e) log.error(e.localizedMessage, e)
ResponseResult.fail(ResponseCode.SYSTEM_ERROR, data = null) 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 io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
import java.io.Serializable import java.io.Serializable
@Schema(description = "登录请求参数")
class LoginParam : Serializable { class LoginParam : Serializable {
@Schema(description = "用户名", example = "test", required = true) @Schema(description = "用户名", example = "test", required = true)

View File

@@ -1,11 +1,13 @@
package top.fatweb.api.service.permission package top.fatweb.api.service.permission
import top.fatweb.api.entity.permission.User import top.fatweb.api.entity.permission.User
import top.fatweb.api.vo.LoginVo
import top.fatweb.api.vo.TokenVo
interface IAuthenticationService { interface IAuthenticationService {
fun login(user: User): HashMap<String, String> fun login(user: User): LoginVo
fun logout(token: String): Boolean 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.JwtUtil
import top.fatweb.api.util.RedisUtil import top.fatweb.api.util.RedisUtil
import top.fatweb.api.util.WebUtil import top.fatweb.api.util.WebUtil
import java.util.concurrent.TimeUnit import top.fatweb.api.vo.LoginVo
import top.fatweb.api.vo.TokenVo
@Service @Service
class AuthenticationServiceImpl( class AuthenticationServiceImpl(
private val authenticationManager: AuthenticationManager, private val authenticationManager: AuthenticationManager,
private val redisUtil: RedisUtil private val redisUtil: RedisUtil
) : IAuthenticationService { ) : IAuthenticationService {
override fun login(user: User): HashMap<String, String> { override fun login(user: User): LoginVo {
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(user.username, user.password) val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(user.username, user.password)
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken) val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
authentication ?: let { authentication ?: let {
@@ -33,18 +34,17 @@ class AuthenticationServiceImpl(
throw RuntimeException("Login failed") throw RuntimeException("Login failed")
} }
val hashMap = hashMapOf("token" to jwt) val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32) redisUtil.setObject(redisKey, loginUser, SecurityConstants.redisTtl, SecurityConstants.redisTtlUnit)
redisUtil.setObject(redisKey, loginUser, 20, TimeUnit.MINUTES)
return hashMap return LoginVo(jwt)
} }
override fun logout(token: String): Boolean = 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> { override fun renewToken(token: String): TokenVo {
val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token
redisUtil.delObject(oldRedisKey) redisUtil.delObject(oldRedisKey)
val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString()) val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString())
@@ -52,10 +52,14 @@ class AuthenticationServiceImpl(
throw RuntimeException("Login failed") throw RuntimeException("Login failed")
} }
val hashMap = hashMapOf("token" to jwt) val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt
val redisKey = "${SecurityConstants.jwtIssuer}_login:" + jwt.substring(0, 32) redisUtil.setObject(
redisUtil.setObject(redisKey, WebUtil.getLoginUser(), 20, TimeUnit.MINUTES) redisKey,
WebUtil.getLoginUser(),
SecurityConstants.redisTtl,
SecurityConstants.redisTtlUnit
)
return hashMap return TokenVo(jwt)
} }
} }

View File

@@ -1,10 +1,16 @@
package top.fatweb.api.util package top.fatweb.api.util
import jakarta.servlet.http.HttpServletRequest
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import top.fatweb.api.constant.SecurityConstants
import top.fatweb.api.entity.permission.LoginUser import top.fatweb.api.entity.permission.LoginUser
object WebUtil { object WebUtil {
fun getLoginUser() = SecurityContextHolder.getContext().authentication.principal as LoginUser fun getLoginUser() = SecurityContextHolder.getContext().authentication.principal as LoginUser
fun getLoginUserId() = getLoginUser().user.id 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
)

View File

@@ -3,9 +3,11 @@ app:
# header-string: "Authorization" # The key of head to get token # header-string: "Authorization" # The key of head to get token
# token-prefix: "Bearer " # Token prefix # token-prefix: "Bearer " # Token prefix
# jwt-ttl: 2 # The life of token # 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-key: $uuid$ # Key to generate token (Only numbers and letters allow)
# jwt-issuer: FatWeb # Token issuer # 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: server:
# port: 8080 # Server port # port: 8080 # Server port