介绍
JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token 实现方式,目前多用于前后端分离项目和 OAuth2.0 业务场景下。
为什么需要jwt
在之前的一些web项目中,我们通常使用的是Cookie-Session模式实现用户认证。相关流程大致如下:
- 用户在浏览器端填写用户名和密码,并发送给服务端
- 服务端对用户名和密码校验通过后会生成一份保存当前用户相关信息的session数据和一个与之对应的标识(通常称为session_id)
- 服务端返回响应时将上一步的session_id写入用户浏览器的Cookie
- 后续用户来自该浏览器的每次请求都会自动携带包含session_id的Cookie
- 服务端通过请求中的session_id就能找到之前保存的该用户那份session数据,从而获取该用户的相关信息。
这种方案依赖于客户端(浏览器)保存 Cookie,并且需要在服务端存储用户的session数据。
在移动互联网时代,我们的用户可能使用浏览器也可能使用APP来访问我们的服务,我们的web应用可能是前后端分开部署在不同的端口,有时候我们还需要支持第三方登录,这下Cookie-Session的模式就有些力不从心了。
JWT就是一种基于Token的轻量级认证模式,服务端认证通过后,会生成一个JSON对象,经过签名后得到一个Token(令牌)再发回给用户,用户后续请求只需要带上这个Token,服务端解密之后就能获取该用户的相关信息了。
可以参考:阮一峰的JWT入门教程
安装
go get github.com/golang-jwt/jwt/v4
gin使用jwt
第一步:增加jwt配置
signing-key: '123455' //秘钥,切记不可泄露
expires-time: 604800 //token过期时间 单位秒
buffer-time: 86400 //token过期缓冲时间 单位秒
issuer: 'wsn' //秘钥颁发者
use-multipoint:true //是否开启多点登录拦截
第二步:颁发jwt
登录逻辑不再陈述,默认登录通过,开始颁发token
大致逻辑如下图:
func (b *BaseApi) tokenNext(c *gin.Context, user system.SysUser) {
j := utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)}
claims := j.CreateClaims(systemReq.BaseClaims{
UUID: user.UUID,
NickName: user.NickName,
Username: user.Username,
AuthorityId: user.AuthorityId,
})
token, err := j.CreateToken(claims)
if err != nil {
global.GVA_LOG.Error("获取token失败!", zap.Error(err))
response.FailWithMessage("获取token失败", c)
return
}
//如果没有开启多点登录限制,则直接返回token
if !global.GVA_CONFIG.System.UseMultipoint {
response.OkWithDetailed(systemRes.LoginResponse{
User: user,
Token: token,
ExpiresAt: claims.StandardClaims.ExpiresAt * 1000,
}, "登录成功", c)
return
}
if err, jwtStr := jwtService.GetRedisJWT(user.Username); err == redis.Nil {
if err := jwtService.SetRedisJWT(token, user.Username); err != nil {
global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err))
response.FailWithMessage("设置登录状态失败", c)
return
}
response.OkWithDetailed(systemRes.LoginResponse{
User: user,
Token: token,
ExpiresAt: claims.StandardClaims.ExpiresAt * 1000,
}, "登录成功", c)
} else if err != nil {
global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err))
response.FailWithMessage("设置登录状态失败", c)
}else {
//说明获取到了之前的token,则把之前token设置作废
var blackJWT system.JwtBlacklist
blackJWT.Jwt = jwtStr
if err := jwtService.JsonInBlacklist(blackJWT); err != nil {
response.FailWithMessage("jwt作废失败", c)
return
}
if err := jwtService.SetRedisJWT(token, user.Username); err != nil {
response.FailWithMessage("设置登录状态失败", c)
return
}
response.OkWithDetailed(systemRes.LoginResponse{
User: user,
Token: token,
ExpiresAt: claims.StandardClaims.ExpiresAt * 1000,
}, "登录成功", c)
}
}
第三步:编写中间件
大致流程如下
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := c.Request.Header.Get("x-token")
if token == "" {
response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c)
c.Abort()
return
}
if jwtService.IsBlacklist(token) {
response.FailWithDetailed(gin.H{"reload": true}, "您的帐户异地登陆或令牌失效", c)
c.Abort()
return
}
j := utils.NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
if err == utils.TokenExpired {
response.FailWithDetailed(gin.H{"reload": true}, "授权已过期", c)
c.Abort()
return
}
response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
c.Abort()
return
}
// 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开
//if err, _ = userService.FindUserByUuid(claims.UUID.String()); err != nil {
// _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token})
// response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
// c.Abort()
//}
if claims.ExpiresAt-time.Now().Unix() < claims.BufferTime {
claims.ExpiresAt = time.Now().Unix() + global.GVA_CONFIG.JWT.ExpiresTime
newToken, _ := j.CreateTokenByOldToken(token, *claims)
newClaims, _ := j.ParseToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt, 10))
if global.GVA_CONFIG.System.UseMultipoint {
err, RedisJwtToken := jwtService.GetRedisJWT(newClaims.Username)
if err != nil {
global.GVA_LOG.Error("get redis jwt failed", zap.Error(err))
} else { // 当之前的取成功时才进行拉黑操作
_ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: RedisJwtToken})
}
// 无论如何都要记录当前的活跃状态
_ = jwtService.SetRedisJWT(newToken, newClaims.Username)
}
}
c.Set("claims", claims)
c.Next()
}
}