现在前后端分离的趋势越来越明显了,后端正在逐渐开始做纯RESTful
性质的服务,关于接口鉴权这个问题就是老生常谈了,目前常用的Spring Security + Jwt
, Shiro + Jwt
等极其繁琐的配置,仅仅为了实现一个小小的登陆功能,显得过于笨重繁杂了,本文来探讨一下如何自己实现一个简单优雅的JWT鉴权方案
总体方案
从一切从简的角度考虑,要对接口实现鉴权,最行之有效的可能就是使用HandlerInterceptor
拦截器了。
- 定义注解
@RequiredAuthorize
/@SkipAuthorize
标记类或者函数需要或者跳过鉴权。
- 提供接口,使用Jwt签发
AccessToken
,提供给客户端,作为鉴权的令牌。
- 实现
HandlerInterceptor
,针对注解标记的接口实施鉴权验证。
- 实现全局异常拦截器,处理鉴权失败异常,最终以JSON格式返回失败消息给客户端。
关于Jwt缺点及解决方案
jwt的相关说明不再赘述,在网上能找到大量资料,详见官网https://jwt.io
这里说说jwt的主要缺点,后文将逐一解决之.
- 无法作废已颁布的令牌。
所有的认证信息都在JWT中,由于在服务端没有状态,即使你知道了某个JWT被盗取了,你也没有办法将其作废。在JWT过期之前(你绝对应该设置过期时间),你无能为力。
- 不易应对数据过期。
与上一条类似,JWT有点类似缓存,由于无法作废已颁布的令牌,在其过期前,你只能忍受“过期”的数据。
这两个确实是非常致命的,一旦token签发,则不可收回,是一个比较大的安全隐患,那么有哪些解决方案呢?
- 尽可能缩短token的有效期,让前端定期刷新token,尽可能的降低风险。( 不考虑 )
- 将签发的token在服务端存储一份,校验token时首先检查服务端的token状态。(违背Jwt的设计初衷, 不考虑 )
- 后端只存储已签发token的
BlackList
,且设有过期时间(比token过期时间略长),校验token时,先校验token本身,再检查是否在BlackList
中,后端需要对某个token实施作废,只需要加入BlackList
中即可。(妥协的方案,目前看来可行,解决了无法作废已颁布的令牌的问题)
开始实现
按照预定的方案,咱们开始逐一实现。

引入依赖
1
2
|
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("com.auth0:java-jwt:3.10.3")
|
引入Redis,是为了实现BlackList,其具备过期时间以及高性能等特性很是适合。
定义配置文件
1
2
3
4
5
6
7
|
@Configuration
@ConfigurationProperties(prefix = "auth.jwt")
class JwtProperties(
var issuer: String = "DEFAULT_ISSUER",
var secret: String = "DEFAULT_JWT_SECRET#!@#$%^&*()==",
var expireAt: Int = 60 * 60 // 1 Hour (单位: 秒)
)
|
对应配置文件:
1
2
3
4
5
6
|
auth:
enabled: true
jwt:
issuer: WebApp
secret: "WebApp#!@#$%^&*()=="
expire-at: 3600
|
Jwt基础封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
class JwtPayload {
companion object {
fun toPayload(map: Map<String, Any?>): JwtPayload {
return BeanUtil.mapToBean(map, JwtPayload::class.java, false)
}
}
var uid: Int = -1
var roles: Array<String> = arrayOf()
var permissions: Array<String> = arrayOf()
constructor()
constructor(uid: Int) {
this.uid = uid
}
constructor(uid: Int, roles: Array<String>) {
this.uid = uid
this.roles = roles
}
constructor(uid: Int, roles: Array<String>, permissions: Array<String>) {
this.uid = uid
this.roles = roles
this.permissions = permissions
}
fun toMap(): Map<String, Any?> {
return BeanUtil.beanToMap(this)
}
}
class JwtToken(var accessToken: String, var expireAt: Date)
class JwtValidException(override val message: String?) : RuntimeException()
@Configuration
class JwtConfiguration(private val jwtProperties: JwtProperties) {
@Bean
fun defaultJwtAlgorithm(): Algorithm {
return Algorithm.HMAC256(jwtProperties.secret)
}
}
@Component
class JwtHelper(
private val algorithm: Algorithm,
private val jwtProperties: JwtProperties) {
companion object {
private const val PAYLOAD_KEY = "payload"
}
fun createToken(payload: JwtPayload): JwtToken {
val expireAt = DateUtil.date().offset(DateField.SECOND, jwtProperties.expireAt)
val accessToken = JWT.create().withIssuer(jwtProperties.issuer)
.withClaim(PAYLOAD_KEY, payload.toMap())
.withExpiresAt(expireAt)
.sign(algorithm)
return JwtToken(accessToken, expireAt)
}
fun verify(token: String): Boolean {
return try {
JWT.require(algorithm).build().verify(token)
true
} catch (e: Throwable) {
false
}
}
fun parse(token: String): JwtPayload {
val decodedJWT = JWT.decode(token)
return JwtPayload.toPayload(decodedJWT.getClaim(PAYLOAD_KEY).asMap())
}
}
|
定义JwtRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
interface JwtRepository {
fun create(payload: JwtPayload): JwtToken
fun delete(token: String)
fun parseAndVerify(token: String): JwtPayload
fun refresh(token: String): JwtToken
}
@Repository
class JwtRedisRepository(
private val jwtHelper: JwtHelper,
private val redisHelper: RedisHelper,
private val jwtProperties: JwtProperties) : JwtRepository {
companion object {
// 将Token存储到Redis黑名单(list),视为无效Token,过期时间为Token的两倍
private fun blackListTokenKey(token: String): String {
return "BLACK_LIST_ACCESS_TOKEN:${token.md5()}"
}
}
override fun create(payload: JwtPayload): JwtToken {
return jwtHelper.createToken(payload)
}
override fun delete(token: String) {
val key = blackListTokenKey(token)
if (!jwtHelper.verify(token)) {
redisHelper.del(key) // 如果token本身已经过期,则尝试从黑名单移除
} else {
// 否则将未过期的token,存入黑名单列表
redisHelper.set(key, token, (jwtProperties.expireAt * 2).toLong())
}
}
override fun parseAndVerify(token: String): JwtPayload {
val key = blackListTokenKey(token)
// 首先验证签名
if (!jwtHelper.verify(token)) {
redisHelper.del(key) // 如果token本身已经过期,则尝试从黑名单移除
throw JwtValidException("令牌无效或已过期")
}
// 再验证是否在黑名单中
if (token == redisHelper.get(key).toString()) {
throw JwtValidException("令牌已被移除")
}
// 开始解析
return jwtHelper.parse(token)
}
override fun refresh(token: String): JwtToken {
// 解析Token(要求Token为有效的), 得到payload,用于生成新的Token
val payload = this.parseAndVerify(token)
// 删除旧的Token
this.delete(token)
// 生成新的Token
return this.create(payload)
}
}
|
定义注解
1
2
3
4
5
6
7
|
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class RequiredAuthorize(
val hasRole: Array<String> = [],
val hasPermission: Array<String> = []
)
|
实现HandlerInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
@Configuration
@ConditionalOnProperty(prefix = "auth", name = ["enabled"], havingValue = "true", matchIfMissing = false)
class WebAuthInterceptorConfiguration(private val jwtRepository: JwtRepository) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(AuthorizeHandlerInterceptor(jwtRepository))
.order(1).addPathPatterns("/**")
}
class AuthorizeHandlerInterceptor(
var jwtRepository: JwtRepository) : HandlerInterceptor {
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
if (handler is HandlerMethod) {
val annotation = handler.getMethodAnnotation(RequiredAuthorize::class.java)
?: return super.preHandle(request, response, handler)
val accessToken = WebContext.getAccessToken()
if (accessToken.isNullOrBlank()) {
return this.writeUnauthorizedMessage(response, "需要传入令牌")
}
try {
// 解析并验证Token
val payload = jwtRepository.parseAndVerify(accessToken)
// 需要验证角色
if (!permissionMatch(payload.roles, annotation.hasRole)) {
return this.writeUnauthorizedMessage(response, "角色不匹配")
}
// 需要验证权限
if (!permissionMatch(payload.permissions, annotation.hasPermission)) {
return this.writeUnauthorizedMessage(response, "没有所需的权限")
}
} catch (e: Throwable) {
return this.writeUnauthorizedMessage(response, e.message ?: "")
}
}
return super.preHandle(request, response, handler)
}
private fun permissionMatch(self: Array<String>?, need: Array<String>): Boolean {
if (need.isNullOrEmpty()) {
return true // 不需要匹配权限
}
if (self.isNullOrEmpty()) {
return false // 匹配失败,Token中不含角色权限信息
}
var count = 0;
for (item in need) {
if (ArrayUtil.contains(self, item)) {
count++
}
}
return count == need.size
}
private fun writeUnauthorizedMessage(response: HttpServletResponse, vararg message: String): Boolean {
var unauthorized: RestError = Errors.UNAUTHORIZED
if (message.isNotEmpty() && message[0].isNotBlank()) {
unauthorized = unauthorized.build(message[0], true)
}
ServletUtil.write(response, RestMessage.error(unauthorized).toJSONString(), ContentType.JSON.toString())
return false
}
}
}
|
定义AuthContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@Component
class AuthContext(
private val jwtRepository: JwtRepository) {
fun getPayload(vararg canBeNull: Boolean): JwtPayload? {
// canBeNull = null Or canBeNull = false,尝试抛出异常
val tryThrow = canBeNull.isEmpty() || (canBeNull.isNotEmpty() && !canBeNull[0])
val accessToken = WebContext.getAccessToken()
if (accessToken.isNullOrBlank()) {
if (tryThrow) {
throw JwtValidException("需要传入令牌")
}
return null
}
return try {
jwtRepository.parseAndVerify(accessToken)
} catch (e: Throwable) {
if (tryThrow) {
throw e
}
null // 忽略验证异常,返回null
}
}
fun getUserId(vararg canBeNull: Boolean): Int? {
return getPayload(*canBeNull)?.uid
}
fun getRoles(): Array<String>? {
return getPayload()?.roles
}
fun getPermissions(): Array<String>? {
return getPayload()?.permissions
}
}
|
使用
提供登录/刷新令牌/注销接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@RestController
@Api(tags = ["用户鉴权"])
@RequestMapping("/auth")
class AuthController(
private val userService: UserService,
private val jwtRepository: JwtRepository) {
@ApiOperation("登录")
@PostMapping("/login")
fun login(): RestMessage<JwtToken> {
// TODO:实现验证用户名密码的具体逻辑
val user = userService.findByUsername();
// 生成用户Token
val payload = JwtPayload(uid = userId, roles=user.roles, permissions=user.permissions)
return RestMessage.result(jwtRepository.create(payload))
}
@RequiredAuthorize
@ApiOperation("刷新令牌(需登录)")
@PostMapping("/refresh_token")
fun refreshToken(): RestMessage<JwtToken> {
val accessToken = WebContext.getAccessToken()
return RestMessage.result(jwtRepository.refresh(accessToken!!))
}
@RequiredAuthorize
@ApiOperation("注销令牌(需登录)")
@PostMapping("/logout")
fun logout(): RestMessage<*> {
val accessToken = WebContext.getAccessToken()
return RestMessage.result(jwtRepository.delete(accessToken!!))
}
}
|
需要鉴权的接口示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 表示需要登录后才能操作的接口
@RequiredAuthorize
@GetMapping("/testRequiredAuthorize")
fun testRequiredAuthorize(): RestMessage<String> {
return RestMessage.result("Ok, 你已经进来了, userId=${authContext.getUserId()}")
}
// 表示需要登录后并且用户身份为“Admin”才能操作的接口
@RequiredAuthorize(hasRole = ["Admin"])
@GetMapping("/testRequiredAuthorizeHasRole")
fun testRequiredAuthorizeHasRole(): RestMessage<String> {
return RestMessage.result("Ok, 你已经进来了, payload=${authContext.getPayload()?.toJSONString()}")
}
// 表示需要登录后,并且用户拥有“CreateUser”权限才能操作的接口
@RequiredAuthorize(hasPermission = ["CreateUser"])
@GetMapping("/testRequiredAuthorizeHasPermission")
fun testRequiredAuthorizeHasPermission(): RestMessage<String> {
return RestMessage.result("Ok, 你已经进来了, payload=${authContext.getPayload()?.toJSONString()}")
}
|
测试
调用登录接口拿到token

使用token调用对应的接口
-
不传入token的情况:

-
传入正确的token:

-
传入的token用户角色不符:

注销登录

使用注销前的token尝试使用

评论