一、JWT简介
背景
在Web开发中,由于HTTP协议的无状态性,每次请求都是独立的,这意味着服务器无法识别连续的请求是否来自同一个用户。为了解决这个限制,出现了cookie+session的传统认证模式。但是这种模式的缺点随着互联网发展用户增多逐渐显现出来,由于每一个访问请求都会创建一个session对象,当同时访问的用户数量过多时服务器存放过多的Session,会导致服务器开销增大。在这种模式下,用户两次访问的请求必须发生在同一服务器中,才能拿到授权的资源,解决办法有但都有隐患。为了更好的解决这种问题,就出现了JWT令牌。
二、简介
JWT是Json Web Token的缩写,它是一种开放标准方法,使用JSON对象在各方之间安全的传输信息。同时JWT可以使用密钥等方式对信息进行数字签名,让这个信息是可以验证和信任的。它是无状态的因此可以在多个服务器之间共享,而且它是保存在客户端的,下次访问服务器只需要验证token是否合法以此来辨别客服端身份,减小了服务器的负担。它包含了三部分:头部,荷载,签名,两两之间使用 ‘·’ 进行分隔。
token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
1.头部
头部由两部分组成:令牌类型以及签名算法,用JSON描述
//例如
{
"alg":"HS256",//如果是未加密这个属性值需要设置为none
"typ":"JWT"
}
之后这部分会被编码为Base64格式。
2.荷载
荷载包含了声明等一些token的内容。可以按照需求添加信息
{
iss:"发行人"
exp:"到期时间"
sub:"主题"
aud:"用户"
nbf:"在某个时间前不可用"
iat:"发布时间"
jti:"JWT_ID用来标识该JWT"
}
3.签名
签名部分是对上面两部分的数据签名,是通过指定的算法(由JWT头部指定)生成哈希来确保数据不会被篡改。
三、gin框架下token令牌验证实例
用户登录成功后,后续操作若需要用户的账号之类的信息一直让前端传递数据这种操作是不安全的,这个时候只需要用户登陆成功之后后端返回一串加密的字符串(token),由前端配置在Header中,这样又安全还能完美的解决问题。下面是生成token,验证token的步骤。
"github.com/dgrijalva/jwt-go" 使用的库
1.定义一个Token结构体
作者的项目token里包含了用户的账号及角色,大家按需来定义(不建议储存敏感信息)
// Claims Token结构体
type Claims struct {
Username string `json:"username"`//账号
Role string `json:"role"`//角色
jwt.StandardClaims //内置结构体,包含token令牌本身的信息
}
2.定义密钥+生成密钥
我定义的密钥长度为七
// 定义密钥
var JwtKey = []byte("xlszxjm") //首字母大写,另一个文件中验证方法可以调用
// ReleaseToken 生成密钥
func ReleaseToken(username, password, role string) (string, error) {
expirationTime := time.Now().Add(7 * 24 * time.Hour) //token的有效期是七天
claims := &Claims{
Username: username,
Password: password,
Role: role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(), //token的有效期
IssuedAt: time.Now().Unix(), //token发放的时间
Issuer: "xxx", //作者(可修改)
Subject: "user token", //主题
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(JwtKey) //根据前面自定义的Jwt秘钥生成token
tokenString = "Bearer " + tokenString
if err != nil {
//返回生成的错误
return "", err
}
//返回成功生成的字符换
return tokenString, nil
}
3.解析token
// ParseToken 解析从前端获取到的token值
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return JwtKey, nil
})
return token, claims, err
}
4.验证token是否合法的中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取前端传过来的信息
tokenString := c.GetHeader("token")
//验证前端传过来的token格式,不为空,开头为Bearer
if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer ") {
response.ResponseError(c, 400)
c.Abort()
return
}
//验证通过,提取有效部分(除去Bearer)
tokenString = tokenString[7:] //截取字符
//解析token
token, _, err := ParseToken(tokenString)//使用解析token的方法
//解析失败||解析后的token无效
if err != nil || !token.Valid {
response.ResponseError(c, 400)
c.Abort()
return
}
c.Next()
}
}
5.通过token获取用户信息
// GetUsername 通过token获取username
func GetUsername(tokenString string) (string, error) {
tokenString = tokenString[7:]
token, _, err := ParseToken(tokenString)
if err != nil {
fmt.Println("GetUsername ParseToken() err:", err.Error())
return "", err
}
if claims, ok := token.Claims.(*Claims); ok {
return claims.Username, nil
}
return "", nil
}
// GetRole 通过token获取role
func GetRole(tokenString string) (string, error) {
tokenString = tokenString[7:]
token, _, err := ParseToken(tokenString)
if err != nil {
fmt.Println("GetRole ParseToken() err:", err)
return "", err
}
if claims, ok := token.Claims.(*Claims); ok {
return claims.Role, nil
}
return "", err
}
最后,只需要在用户登录的接口调用生成token的方法,将生成的token返回给前端并配置在Header中,把中间件添加在需要使用到c.GetHeader("token")方法的接口前以防止前端因配置失败出现错误。
结尾
JWT并不是毫无缺点的,
首先服务器不会保存会话状态确实减小了服务器的负担,缺少状态意味着如果后续处理需要前面的信息,则客户端必须重传,这样可能导致每次连接每次连接传送的数据量增大。
在通信过程中无法进行取消令牌,更改令牌的操作,而且JWT的荷载部分可能被解码因此不能用于储存敏感信息。