diff --git a/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt b/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt new file mode 100644 index 0000000..f087e97 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/annotation/ApiVersion.kt @@ -0,0 +1,14 @@ +package top.fatweb.api.annotation + +import org.springframework.core.annotation.AliasFor + + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class ApiVersion( + @get:AliasFor("version") + val value: Int = 1, + + @get:AliasFor("value") + val version: Int = 1 +) diff --git a/src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt b/src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt new file mode 100644 index 0000000..5836f3d --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/SecurityConfig.kt @@ -0,0 +1,92 @@ +package top.fatweb.api.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.* +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import top.fatweb.api.filter.JwtAuthenticationTokenFilter +import top.fatweb.api.handler.JwtAccessDeniedHandler +import top.fatweb.api.handler.JwtAuthenticationEntryPointHandler + +@Configuration +@EnableMethodSecurity +class SecurityConfig( + val jwtAuthenticationTokenFilter: JwtAuthenticationTokenFilter, + val authenticationEntryPointHandler: JwtAuthenticationEntryPointHandler, + val accessDeniedHandler: JwtAccessDeniedHandler +) { + @Bean + fun passwordEncoder() = BCryptPasswordEncoder() + + @Bean + fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager = + authenticationConfiguration.authenticationManager + + @Bean + fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedMethods = listOf("*") + corsConfiguration.allowedHeaders = listOf("*") + corsConfiguration.maxAge = 3600L + corsConfiguration.allowedOrigins = listOf("*") + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", corsConfiguration) + + return source + } + + @Bean + fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = httpSecurity + // Disable CSRF + .csrf { csrfConfigurer: CsrfConfigurer -> csrfConfigurer.disable() } + // Do not get SecurityContent by Session + .sessionManagement { sessionManagementConfigurer: SessionManagementConfigurer -> + sessionManagementConfigurer.sessionCreationPolicy( + SessionCreationPolicy.STATELESS + ) + } + .authorizeHttpRequests { authorizeHttpRequests: AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry -> + authorizeHttpRequests + // Allow anonymous access + .requestMatchers( + "/api/v*/login", + "/error/thrown", + "/doc.html", + "/swagger-ui/**", + "/webjars/**", + "/v3/**", + "/swagger-ui.html", + "/favicon.ico" + ).anonymous() + // Authentication required + .anyRequest().authenticated() + } + + .logout { logoutConfigurer: LogoutConfigurer -> logoutConfigurer.disable() } + + .exceptionHandling { exceptionHandlingConfigurer: ExceptionHandlingConfigurer -> + exceptionHandlingConfigurer.authenticationEntryPoint( + authenticationEntryPointHandler + ) + exceptionHandlingConfigurer.accessDeniedHandler( + accessDeniedHandler + ) + } + + .cors { cors: CorsConfigurer -> + cors.configurationSource( + corsConfigurationSource() + ) + } + + .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java).build() +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/config/WebMvcRegistrationsConfig.kt b/src/main/kotlin/top/fatweb/api/config/WebMvcRegistrationsConfig.kt new file mode 100644 index 0000000..1db6a0f --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/config/WebMvcRegistrationsConfig.kt @@ -0,0 +1,11 @@ +package top.fatweb.api.config + +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping +import top.fatweb.api.util.ApiResponseMappingHandlerMapping + +@Configuration +class WebMvcRegistrationsConfig : WebMvcRegistrations { + override fun getRequestMappingHandlerMapping(): RequestMappingHandlerMapping = ApiResponseMappingHandlerMapping() +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/controller/UserController.kt b/src/main/kotlin/top/fatweb/api/controller/UserController.kt new file mode 100644 index 0000000..188caf5 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/controller/UserController.kt @@ -0,0 +1,17 @@ +package top.fatweb.api.controller + +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + *

+ * 用户 前端控制器 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@RestController +@RequestMapping("/api/user") +class UserController + diff --git a/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt new file mode 100644 index 0000000..c36bdac --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/controller/permission/AuthenticationController.kt @@ -0,0 +1,21 @@ +package top.fatweb.api.controller.permission + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.* +import top.fatweb.api.annotation.ApiVersion +import top.fatweb.api.entity.common.ResponseCode +import top.fatweb.api.entity.common.ResponseResult +import top.fatweb.api.entity.permission.User +import top.fatweb.api.service.permission.IAuthenticationService + +@Tag(name = "身份认证", description = "身份认证相关接口") +@RestController +@RequestMapping("/api/{apiVersion}") +@ApiVersion(2) +class AuthenticationController(val loginService: IAuthenticationService) { + @Operation(summary = "登录") + @PostMapping("/login") + fun login(@PathVariable apiVersion: String, @RequestBody user: User) = + ResponseResult.success(ResponseCode.SYSTEM_LOGIN_SUCCESS, "Login success", loginService.login(user)) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt new file mode 100644 index 0000000..f160c2c --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/permission/LoginUser.kt @@ -0,0 +1,36 @@ +package top.fatweb.api.entity.permission + +import com.fasterxml.jackson.annotation.JsonIgnore +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +class LoginUser() : UserDetails { + lateinit var user: User + + @JsonIgnore + private var authorities: List? = null + + constructor(user: User) : this() { + this.user = user + } + + @JsonIgnore + override fun getAuthorities(): List { + authorities?.let { return it } + authorities = emptyList() + + return authorities as List + } + + override fun getPassword(): String? = user.password + + override fun getUsername(): String? = user.username + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = user.enable == 1 +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/entity/permission/User.kt b/src/main/kotlin/top/fatweb/api/entity/permission/User.kt new file mode 100644 index 0000000..3af3d08 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/entity/permission/User.kt @@ -0,0 +1,56 @@ +package top.fatweb.api.entity.permission + +import com.baomidou.mybatisplus.annotation.* +import java.io.Serializable + +/** + *

+ * 用户 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@TableName("t_user") +class User : Serializable { + + @TableId("id") + var id: Long? = null + + /** + * 用户名 + */ + @TableField("username") + var username: String? = null + + /** + * 密码 + */ + @TableField("password") + var password: String? = null + + /** + * 启用 + */ + @TableField("enable") + var enable: Int? = null + + @TableField("deleted") + @TableLogic + var deleted: Long? = null + + @TableField("version") + @Version + var version: Int? = null + + override fun toString(): String { + return "User{" + + "id=" + id + + ", username=" + username + + ", password=" + password + + ", enable=" + enable + + ", deleted=" + deleted + + ", version=" + version + + "}" + } +} diff --git a/src/main/kotlin/top/fatweb/api/exception/TokenHasExpiredException.kt b/src/main/kotlin/top/fatweb/api/exception/TokenHasExpiredException.kt new file mode 100644 index 0000000..2b0521f --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/exception/TokenHasExpiredException.kt @@ -0,0 +1,3 @@ +package top.fatweb.api.exception + +class TokenHasExpiredException : RuntimeException("Token has expired") \ 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 9319248..74f6ec0 100644 --- a/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt +++ b/src/main/kotlin/top/fatweb/api/filter/JwtAuthenticationTokenFilter.kt @@ -3,24 +3,44 @@ package top.fatweb.api.filter import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component import org.springframework.util.StringUtils import org.springframework.web.filter.OncePerRequestFilter -import top.fatweb.api.constants.SecurityConstants -import top.fatweb.api.utils.RedisUtil +import top.fatweb.api.constant.SecurityConstants +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 +@Component class JwtAuthenticationTokenFilter(private val redisUtil: RedisUtil) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { - val token = request.getHeader(SecurityConstants.headerString) + val tokenWithPrefix = request.getHeader(SecurityConstants.headerString) - if (!StringUtils.hasText(token) || "/error/thrown" == request.servletPath) { + if (!StringUtils.hasText(tokenWithPrefix) || "/error/thrown" == request.servletPath) { filterChain.doFilter(request, response) return } + val token = tokenWithPrefix.removePrefix(SecurityConstants.tokenPrefix) + JwtUtil.parseJwt(token) + val redisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) + val loginUser = redisUtil.getObject(redisKey) + loginUser ?: let { throw TokenHasExpiredException() } + + redisUtil.setExpire(redisKey, 20, TimeUnit.MINUTES) + + val authenticationToken = UsernamePasswordAuthenticationToken(loginUser, null, loginUser.authorities) + SecurityContextHolder.getContext().authentication = authenticationToken + + filterChain.doFilter(request, response) } } \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/handler/JwtAccessDeniedHandler.kt b/src/main/kotlin/top/fatweb/api/handler/JwtAccessDeniedHandler.kt new file mode 100644 index 0000000..22e03c6 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/handler/JwtAccessDeniedHandler.kt @@ -0,0 +1,19 @@ +package top.fatweb.api.handler + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.stereotype.Component + +@Component +class JwtAccessDeniedHandler : AccessDeniedHandler { + override fun handle( + request: HttpServletRequest?, + response: HttpServletResponse?, + accessDeniedException: AccessDeniedException? + ) { + request?.setAttribute("filter.error", accessDeniedException) + request?.getRequestDispatcher("/error/thrown")?.forward(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/handler/JwtAuthenticationEntryPointHandler.kt b/src/main/kotlin/top/fatweb/api/handler/JwtAuthenticationEntryPointHandler.kt new file mode 100644 index 0000000..cf5b808 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/handler/JwtAuthenticationEntryPointHandler.kt @@ -0,0 +1,19 @@ +package top.fatweb.api.handler + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.stereotype.Component + +@Component +class JwtAuthenticationEntryPointHandler : AuthenticationEntryPoint { + override fun commence( + request: HttpServletRequest?, + response: HttpServletResponse?, + authException: AuthenticationException? + ) { + request?.setAttribute("filter.error", authException) + request?.getRequestDispatcher("/error/thrown")?.forward(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt b/src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt new file mode 100644 index 0000000..8b37df3 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/mapper/UserMapper.kt @@ -0,0 +1,16 @@ +package top.fatweb.api.mapper + +import com.baomidou.mybatisplus.core.mapper.BaseMapper +import org.apache.ibatis.annotations.Mapper +import top.fatweb.api.entity.permission.User + +/** + *

+ * 用户 Mapper 接口 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@Mapper +interface UserMapper : BaseMapper diff --git a/src/main/kotlin/top/fatweb/api/service/IUserService.kt b/src/main/kotlin/top/fatweb/api/service/IUserService.kt new file mode 100644 index 0000000..7bc58e9 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/IUserService.kt @@ -0,0 +1,14 @@ +package top.fatweb.api.service + +import com.baomidou.mybatisplus.extension.service.IService +import top.fatweb.api.entity.permission.User + +/** + *

+ * 用户 服务类 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +interface IUserService : IService diff --git a/src/main/kotlin/top/fatweb/api/service/impl/UserServiceImpl.kt b/src/main/kotlin/top/fatweb/api/service/impl/UserServiceImpl.kt new file mode 100644 index 0000000..e522f6b --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/impl/UserServiceImpl.kt @@ -0,0 +1,18 @@ +package top.fatweb.api.service.impl + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl +import org.springframework.stereotype.Service +import top.fatweb.api.entity.permission.User +import top.fatweb.api.mapper.UserMapper +import top.fatweb.api.service.IUserService + +/** + *

+ * 用户 服务实现类 + *

+ * + * @author FatttSnake + * @since 2023-10-04 + */ +@Service +class UserServiceImpl : ServiceImpl(), IUserService diff --git a/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt new file mode 100644 index 0000000..b58a4b6 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/permission/IAuthenticationService.kt @@ -0,0 +1,11 @@ +package top.fatweb.api.service.permission + +import top.fatweb.api.entity.permission.User + +interface IAuthenticationService { + fun login(user: User): HashMap + + fun logout(token: String): Boolean + + fun renewToken(token: String): HashMap +} \ 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 new file mode 100644 index 0000000..b742f3d --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/service/permission/impl/AuthenticationServiceImpl.kt @@ -0,0 +1,61 @@ +package top.fatweb.api.service.permission.impl + +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.stereotype.Service +import top.fatweb.api.constant.SecurityConstants +import top.fatweb.api.entity.permission.LoginUser +import top.fatweb.api.entity.permission.User +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 + +@Service +class AuthenticationServiceImpl( + private val authenticationManager: AuthenticationManager, + private val redisUtil: RedisUtil +) : IAuthenticationService { + override fun login(user: User): HashMap { + val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(user.username, user.password) + val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken) + authentication ?: let { + throw RuntimeException("Login failed") + } + + val loginUser = authentication.principal as LoginUser + loginUser.user.password = "" + val userId = loginUser.user.id.toString() + val jwt = JwtUtil.createJwt(userId) + + jwt ?: let { + 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) + + return hashMap + } + + override fun logout(token: String): Boolean = + redisUtil.delObject("${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32)) + + override fun renewToken(token: String): HashMap { + val oldRedisKey = "${SecurityConstants.jwtIssuer}_login:" + token.substring(0, 32) + redisUtil.delObject(oldRedisKey) + val jwt = JwtUtil.createJwt(WebUtil.getLoginUserId().toString()) + + jwt ?: let { + 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) + + return hashMap + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/ApiResponseMappingHandlerMapping.kt b/src/main/kotlin/top/fatweb/api/util/ApiResponseMappingHandlerMapping.kt new file mode 100644 index 0000000..8dafc0e --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/ApiResponseMappingHandlerMapping.kt @@ -0,0 +1,31 @@ +package top.fatweb.api.util + +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.servlet.mvc.condition.RequestCondition +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping +import top.fatweb.api.annotation.ApiVersion +import java.lang.reflect.Method + +class ApiResponseMappingHandlerMapping : RequestMappingHandlerMapping() { + private val versionFlag = "{apiVersion}" + + private fun createCondition(clazz: Class<*>): RequestCondition? { + val classRequestMapping = clazz.getAnnotation(RequestMapping::class.java) + classRequestMapping ?: let { return null } + val mappingUrlBuilder = StringBuilder() + if (classRequestMapping.value.isNotEmpty()) { + mappingUrlBuilder.append(classRequestMapping.value[0]) + } + val mappingUrl = mappingUrlBuilder.toString() + if (!mappingUrl.contains(versionFlag)) { + return null + } + val apiVersion = clazz.getAnnotation(ApiVersion::class.java) + + return if (apiVersion == null) ApiVersionCondition(1) else ApiVersionCondition(apiVersion.version) + } + + override fun getCustomMethodCondition(method: Method): RequestCondition<*>? = createCondition(method.javaClass) + + override fun getCustomTypeCondition(handlerType: Class<*>): RequestCondition<*>? = createCondition(handlerType) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt b/src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt new file mode 100644 index 0000000..381edaa --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/ApiVersionCondition.kt @@ -0,0 +1,26 @@ +package top.fatweb.api.util + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.web.servlet.mvc.condition.RequestCondition +import java.util.regex.Pattern + +class ApiVersionCondition(private val apiVersion: Int) : RequestCondition { + private val versionPrefixPattern: Pattern = Pattern.compile(".*v(\\d+).*") + + override fun combine(other: ApiVersionCondition): ApiVersionCondition = ApiVersionCondition(other.apiVersion) + + override fun getMatchingCondition(request: HttpServletRequest): ApiVersionCondition? { + val matcher = versionPrefixPattern.matcher(request.requestURI) + if (matcher.find()) { + val version = matcher.group(1).toInt() + if (version >= this.apiVersion) { + return this + } + } + + return null + } + + override fun compareTo(other: ApiVersionCondition, request: HttpServletRequest): Int = + other.apiVersion - this.apiVersion +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/util/JwtUtil.kt b/src/main/kotlin/top/fatweb/api/util/JwtUtil.kt new file mode 100644 index 0000000..ea7c5aa --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/JwtUtil.kt @@ -0,0 +1,67 @@ +package top.fatweb.api.util + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.interfaces.DecodedJWT +import top.fatweb.api.constant.SecurityConstants +import java.util.* +import java.util.concurrent.TimeUnit +import javax.crypto.spec.SecretKeySpec + +object JwtUtil { + private fun getUUID() = UUID.randomUUID().toString().replace("-", "") + + /** + * 生成加密后的秘钥 secretKey + * + * @return 密钥 + */ + private fun generalKey(): SecretKeySpec { + val encodeKey = Base64.getDecoder().decode(SecurityConstants.jwtKey) + return SecretKeySpec(encodeKey, 0, encodeKey.size, "AES") + } + + private fun algorithm(): Algorithm = Algorithm.HMAC256(generalKey().toString()) + + /** + * 创建 token + * + * @param subject token 中存放的数据(json格式) + * @param ttl token 生存时间 + * @param timeUnit ttl 时间单位 + * @param uuid 唯一 ID + * @return jwt 串 + */ + fun createJwt( + subject: String, + ttl: Long = SecurityConstants.jwtTtl, + timeUnit: TimeUnit = SecurityConstants.jwtTtlUnit, + uuid: String = getUUID() + ): String? { + val nowMillis = System.currentTimeMillis() + val nowDate = Date(nowMillis) + val unitTtl = (ttl * when (timeUnit) { + TimeUnit.DAYS -> 24 * 60 * 60 * 1000 + TimeUnit.HOURS -> 60 * 60 * 1000 + TimeUnit.MINUTES -> 60 * 1000 + TimeUnit.SECONDS -> 1000 + TimeUnit.MILLISECONDS -> 1 + TimeUnit.NANOSECONDS -> 1 / 1000 + TimeUnit.MICROSECONDS -> 1 / 1000 / 1000 + }) + val expMillis = nowMillis + unitTtl + val expDate = Date(expMillis) + + return JWT.create().withJWTId(uuid).withSubject(subject).withIssuer(SecurityConstants.jwtIssuer) + .withIssuedAt(nowDate).withExpiresAt(expDate).sign(algorithm()) + } + + /** + * 解析 jwt + * + * @param jwt jwt 串 + * @return 解析内容 + */ + fun parseJwt(jwt: String): DecodedJWT = + JWT.require(algorithm()).build().verify(jwt) +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt b/src/main/kotlin/top/fatweb/api/util/RedisUtil.kt similarity index 97% rename from src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt rename to src/main/kotlin/top/fatweb/api/util/RedisUtil.kt index 426b772..35991a8 100644 --- a/src/main/kotlin/top/fatweb/api/utils/RedisUtil.kt +++ b/src/main/kotlin/top/fatweb/api/util/RedisUtil.kt @@ -1,4 +1,4 @@ -package top.fatweb.api.utils +package top.fatweb.api.util import org.springframework.data.redis.core.BoundSetOperations import org.springframework.data.redis.core.RedisTemplate @@ -141,7 +141,8 @@ class RedisUtil(private val redisTemplate: RedisTemplate) { * @param key 缓存的键 * @return 缓存的键对应的 Map 数据 */ - fun getMap(key: String): Map? = redisTemplate.opsForHash().entries(key) as? Map + fun getMap(key: String): Map? = + redisTemplate.opsForHash().entries(key) as? Map /** * 往 Hash 中存入数据 diff --git a/src/main/kotlin/top/fatweb/api/util/WebUtil.kt b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt new file mode 100644 index 0000000..892c0c6 --- /dev/null +++ b/src/main/kotlin/top/fatweb/api/util/WebUtil.kt @@ -0,0 +1,10 @@ +package top.fatweb.api.util + +import org.springframework.security.core.context.SecurityContextHolder +import top.fatweb.api.entity.permission.LoginUser + +object WebUtil { + fun getLoginUser() = SecurityContextHolder.getContext().authentication.principal as LoginUser + + fun getLoginUserId() = getLoginUser().user.id +} \ No newline at end of file diff --git a/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt b/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt deleted file mode 100644 index 9d18414..0000000 --- a/src/main/kotlin/top/fatweb/api/utils/JwtUtil.kt +++ /dev/null @@ -1,5 +0,0 @@ -package top.fatweb.api.utils - -class JwtUtil { - -} \ No newline at end of file diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..cecc32c --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt index 34fc696..9eb700d 100644 --- a/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt +++ b/src/test/kotlin/top/fatweb/api/FatWebApiApplicationTests.kt @@ -1,17 +1,19 @@ package top.fatweb.api +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest -import top.fatweb.api.constants.SecurityConstants -import java.security.MessageDigest -import java.util.* +import top.fatweb.api.constant.SecurityConstants @SpringBootTest class FatWebApiApplicationTests { @Test fun contextLoads() { - SecurityConstants.jwtKey } + @Test + fun removePrefixTest() { + assertEquals("12312", "Bearer 12312".removePrefix(SecurityConstants.tokenPrefix)) + } }