diff --git a/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt index fd13a65..75eee51 100644 --- a/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt +++ b/src/main/kotlin/top/fatweb/api/config/RedisConfig.kt @@ -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() redisTemplate.connectionFactory = redisConnectionFactory val stringRedisSerializer = StringRedisSerializer() - val objectMapper = ObjectMapper().registerModules(JavaTimeModule()) - val anyJackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(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 diff --git a/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt b/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt index e868e1f..c2b1738 100644 --- a/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt +++ b/src/main/kotlin/top/fatweb/api/constant/SecurityConstants.kt @@ -18,4 +18,8 @@ object SecurityConstants { var jwtKey = "FatWeb" var jwtIssuer = "FatWeb" + + var redisTtl = 20L + + var redisTtlUnit = TimeUnit.MINUTES } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt index 2d26a12..63f68b4 100644 --- a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt +++ b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt @@ -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)) ) } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt b/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt similarity index 81% rename from src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt rename to src/main/kotlin/top/fatweb/api/converter/UserConverter.kt index 8d24463..71b980a 100644 --- a/src/main/kotlin/top/fatweb/api/entity/converter/UserConverter.kt +++ b/src/main/kotlin/top/fatweb/api/converter/UserConverter.kt @@ -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 { diff --git a/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt index f160c2c..951d1e2 100644 --- a/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt +++ b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt @@ -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 } + @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 } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt index 74f6ec0..048aa39 100644 --- a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt +++ b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt @@ -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(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 diff --git a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt index 412c401..5e742e1 100644 --- a/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt +++ b/src/main/kotlin/top/fatweb/api/handler/ExceptionHandler.kt @@ -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) diff --git a/src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt b/src/main/kotlin/top/fatweb/api/param/LoginParam.kt similarity index 86% rename from src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt rename to src/main/kotlin/top/fatweb/api/param/LoginParam.kt index 4ed8d0c..701ab12 100644 --- a/src/main/kotlin/top/fatweb/api/entity/param/LoginParam.kt +++ b/src/main/kotlin/top/fatweb/api/param/LoginParam.kt @@ -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) diff --git a/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt index b58a4b6..28becfc 100644 --- a/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt +++ b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt @@ -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 + fun login(user: User): LoginVo fun logout(token: String): Boolean - fun renewToken(token: String): HashMap + fun renewToken(token: String): TokenVo } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt b/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt index b742f3d..d08bc10 100644 --- a/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt +++ b/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt @@ -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 { + 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 { - 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) } } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/WebUtil.kt b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt index 892c0c6..da37681 100644 --- a/src/main/kotlin/top/fatweb/api/util/WebUtil.kt +++ b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt @@ -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)) } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/vo/LoginVo.kt b/src/main/kotlin/top/fatweb/api/vo/LoginVo.kt new file mode 100644 index 0000000..8727d54 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/vo/LoginVo.kt @@ -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 +) \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/vo/TokenVo.kt b/src/main/kotlin/top/fatweb/api/vo/TokenVo.kt new file mode 100644 index 0000000..c17ea01 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/vo/TokenVo.kt @@ -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 +) \ No newline at end of file diff --git a/src/main/resources/application-config-template.yml b/src/main/resources/application-config-template.yml index 5280697..487ce6e 100644 --- a/src/main/resources/application-config-template.yml +++ b/src/main/resources/application-config-template.yml @@ -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