Spring Boot Kotlin Jwt Token
本篇记录了,基于 Spring Boot、Kotlin、Jwt 的 Token 接口认证功能的项目内容
环境如下
Plugin | Version |
---|---|
SpringBoot | 2.0.8.RELEASE |
Kotlin | 1.2.71 |
Java-Jwt | 3.3 |
时序图
项目配置
Maven 依赖如下:
<!-- Spring Boot --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.8.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <java.version>1.8</java.version> <kotlin.version>1.2.71</kotlin.version> </properties> <dependencies> <!-- Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.3.0</version> </dependency> <!-- FastJson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.49</version> </dependency> <!-- Kotlin --> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> </dependency> </dependencies> <build> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <configuration> <args> <arg>-Xjsr305=strict</arg> </args> <compilerPlugins> <plugin>spring</plugin> </compilerPlugins> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>${kotlin.version}</version> </dependency> </dependencies> </plugin> </plugins> </build>
JwtUtil.kt 工具类,实现了签名、解析、校验、获取用户名的相关方法.
package com.raindrop.auth.utils import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.interfaces.Claim import java.util.* /** * Jwt 工具类 * * @author Raindrop */ open class JwtUtil { companion object { /* 秘钥 */ private val secret = "raindrop-666" /* 过期时间 15 分钟 */ private val expireTime = 15 * 60 * 1000 /** * 生成 Token * * @param username 用户名 * @return token 令牌 */ fun sign(username: String): String {var createDate = Date() var expireDate = Date(System.currentTimeMillis() + expireTime) var claims = mapOf("username" to username) return JWT.create() .withHeader(claims) .withSubject(username) .withIssuedAt(createDate) .withExpiresAt(expireDate) .sign(Algorithm.HMAC512(secret)) } /** * 解析 Token * * @param token 令牌 * @return claim */ fun parse(token: String): MutableMap<String, Claim>? {return JWT.decode(token).claims } /** * 检验 Token * * @param token 令牌 * @return 有效 true 无效 false */ fun verifyToken(token: String): Boolean { return try {JWT.require(Algorithm.HMAC512(secret)).build().verify(token) true } catch (e: Exception) {false} } /** * 获取用户名 * * @param token 令牌 * @return 用户名 */ fun getUserName(token: String) = JWT.decode(token).getClaim("username").asString() /** * 刷新 token * * @param token 原 token * @return 新 token */ fun refresh(token: String): String {var createDate = Date() var expireDate = Date(System.currentTimeMillis() + expireTime) var username = parse(token)!!["sub"]!!.asString() var header = mapOf("username" to username) return JWT.create() .withHeader(header) .withSubject(username) .withIssuedAt(createDate) .withExpiresAt(expireDate) .sign(Algorithm.HMAC512(secret)) } } }
AuthInterceptor.kt 拦截器,请求前进行拦截. 如果请求方法为 OPTIONS,则放过不进行拦截,关于 OPTIONS 请求方法,主要是在发送 PUT、DELETE 或 Content-Type application/json 请求前进行 预检 ,浏览器会先询问服务器,当前请求域名是否在服务器允许的范围内,如果允许则浏览器会发出正式的 XMLHttpRequest 请求,否则会报错. 此拦截器对 登录 接口不进行拦截.
package com.raindrop.auth.interceptors import com.alibaba.fastjson.JSON import com.raindrop.auth.model.ResultEntity import com.raindrop.auth.model.buildResultEntity import com.raindrop.auth.utils.JwtUtil import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import org.springframework.web.servlet.HandlerInterceptor import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @Component class AuthInterceptor : HandlerInterceptor {private val logger = LoggerFactory.getLogger(this.javaClass) private val options = "OPTIONS" override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {if (request.method == options) return true var authorization = request.getHeader("Authorization") if (authorization != null) {var token = authorization.substring(7) if (JwtUtil.verifyToken(token)) {logger.info("Auth Success Token Effective...") return true } } logger.info("Auth Fail Token Invalid...") response.writer.write(JSON.toJSONString(buildUnauthorizedResultEntity())) return false } fun buildUnauthorizedResultEntity(): ResultEntity { return buildResultEntity { code = 403 message = "Auth Fail" } } }
AuthConfig.kt 拦截器配置,此处将 登录 接口过滤,不进行拦截,其他全部接口,全部需要携带 token 并认证通过后,方可访问.
package com.raindrop.auth.config import com.raindrop.auth.interceptors.AuthInterceptor import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration class AuthConfig : WebMvcConfigurer { /** * 添加拦截器 */ override fun addInterceptors(registry: InterceptorRegistry) {var excludePath = listOf("/auth/login", "/auth/guest", "/auth/refresh") registry.addInterceptor(AuthInterceptor()).excludePathPatterns(excludePath) } }
ResultEntity.kt 通用返回实体,此处使用了 kotlin 高级语法,从而可以使用 dsl 方式构建通用返回实体.
package com.raindrop.auth.model import java.io.Serializable data class ResultEntity( var code: Int = 200, var message: String = "success", var data: Any = "" ) : Serializable fun buildResultEntity(builder: ResultEntity.() -> Unit) = ResultEntity().apply(builder)
User.kt 用户实体.
package com.raindrop.auth.model class User( var username: String, var password: String, var nickName: String )
IUserService.kt 用户服务接口.
package com.raindrop.auth.service interface IUserService { /** * 登录 * * @param username 用户名 * @param password 用户密码 * @return */ fun login(username: String, password: String): Boolean }
UserServiceImpl.kt 用户服务实现.
package com.raindrop.auth.service.impl import com.raindrop.auth.service.IUserService import org.springframework.stereotype.Service @Service class UserServiceImpl : IUserService { /** * 登录 * * @param username 用户名 * @param password 用户密码 * @return */ override fun login(username: String, password: String): Boolean = true }
AuthController.kt 业务 Controller,/login 接口不进行拦截,账号密码正确后,返回 token. /users 接口需要拦截,需认证通过后携带正确的 token 后,方可访问.
package com.raindrop.auth.web import com.raindrop.auth.model.ResultEntity import com.raindrop.auth.model.User import com.raindrop.auth.model.buildResultEntity import com.raindrop.auth.service.IUserService import com.raindrop.auth.utils.JwtUtil import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/auth") class AuthController {private val logger = LoggerFactory.getLogger(this.javaClass) @Autowired lateinit var userService: IUserService /** * 登录 * * @param username 用户名 * @param password 用户密码 * @return */ @PostMapping("/login") fun login(username: String, password: String): ResultEntity {if (username == null) {logger.warn("Login Fail, UserName Is Null") throw IllegalArgumentException(" 登录失败,用户不存在 ") } if (password == null) {logger.warn("Login Fail, Password Is Null") throw IllegalArgumentException(" 登录失败,账号或密码错误 ") } if (userService.login(username, password)) {var token = JwtUtil.sign(username) return buildResultEntity {data = token} } return buildResultEntity { code = 400 message = " 登录失败 " } } /** * 获取用户列表 * * @return */ @GetMapping("/users") fun getUserList(): ResultEntity { var users = listOf(User("wl", "wl", "wl"), User("lv", "lv", "lv"), User("ee", "ee", "ee") ) return buildResultEntity {data = users} }
@PostMapping("/refresh")
fun refreshToken(token: String): String = if (JwtUtil.verifyToken(token)) JwtUtil.refresh(token) else "Token Invalid"
@GetMapping("/guest")
fun getGuestMessage(): ResultEntity {
return buildResultEntity {data = "Hello Guest, You are handsome!"}
}
}
```
AuthTokenApplication.kt 启动类.
package com.raindrop.auth import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class AuthTokenApplication fun main(args: Array<String>) {runApplication<AuthTokenApplication>(*args) }
Example
无登录时访问 GET http://localhost:8888/auth/guest , 可以访问,因为该接口并未被过滤器拦截.
无登录时访问 GET http://localhost:8888/auth/users , 此时无意外提醒未授权,因为此接口被过滤器拦截.
访问登录接口 POST http://localhost:8888/auth/login , 认证成功,并返回 token.
此时再次访问 GET http://localhost:8888/auth/users , 可以正确获取结果.
token 过期,刷新 token 访问 POST http://localhost:8888/auth/refresh , 认证原 token 成功后,返回新 token.
Source: Link
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!