这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
一、基本概念
JWT的概念
JSON Web Token(JWT)是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。 一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
JWT在web开发中,是一种基于令牌的轻量级认证模式。
当您提交用户凭证(用户名和密码)后,认证服务器通过认证后,为该用户生成一个经过签名的token令牌,然后返回给用户;接下来,用户只需要拿着这个令牌请求并带着请求参数请求资源服务器的资源即可;在向资源服务器请求资源时,会经过认证服务器解析token令牌是否正确(比如:token是否失效、token是否合法等),有效时,解密后就可以从资源服务器获取到该用户的相关业务资源。
为什么要使用JWT
传统的Cookie-Sesson模式占用服务器内存, 拓展性不好,遇到集群或者跨服务验证的场景的话, 要支持Sesson复制或者sesson持久化。
这种方案依赖于客户端(浏览器)保存Cookie,并且需要在服务端存储用户的session数据。
在移动互联网时代,我们的用户可能使用浏览器也可能使用APP来访问我们的服务,我们的web应用可能是前后端分开部署在不同的端口,有时候我们还需要支持第三方登录,这下Cookie-Session的模式就有些力不从心了。
JWT就是一种基于Token的轻量级认证模式,服务端认证通过后,会生成一个JSON对象,经过签名后得到一个Token(令牌)再发回给用户,用户后续请求只需要带上这个Token,服务端解密之后就能获取该用户的相关信息了。
JWT的基本原理
颁发TOKEN时,把自定义信息嵌入TOKEN。使用TOKEN登录服务器验证之后, 得到原始用户自定义信息JSON。
{
"user_id": "xxxxxx",
"username": "xxxxxx",
"role": "xxxxxx",
"refresh_token": "xxxxx"
}
二、jwt-go框架的使用
引入jwt-go包,并import
go get -u github.com/dgrijalva/jwt-go
并 import github.com/dgrijalva/jwt-go
定义参数结构体
我们需要定制自己的需求来决定JWT中保存哪些数据,比如我们规定在JWT中要存储username信息,那么我们就定义一个MyClaims结构体如下:
// MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一个username和userid字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
Username string `json:"username"`
Userid string `json:"userid"`
jwt.StandardClaims // jwt.StandardClaims包含了官方定义的字段
}
然后我们定义JWT的过期时间,这里以2小时为例:
const TokenExpireDuration = time.Hour * 2
接下来还需要定义加密密钥:
var MySecret = []byte("bytedance")
定义GenToken方法-登陆后生成token
func GenToken(username string, id int64) (string, error) {
c := Myclaims{
Username: username, //携带的自定义字段
ID: id,//携带的自定义字段
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), //过期时间
IssuedAt: time.Now().Unix(),//签发时间
Issuer: "bytedance",//签发人
},
}
// 用指定的哈希方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
return token.SignedString(Secret)
}
ParseToken方法- 解析token--登陆后携带,每次访问都解析token
func ParseToken(tokenString string) (*Myclaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Myclaims{}, func(token *jwt.Token) (interface{}, error) {
return Secret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Myclaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
RefreshToken方法--更新token
func RefreshToken(tokenString string) (string, error) {
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}
token, err := jwt.ParseWithClaims(tokenString, &Myclaims{}, func(token *jwt.Token) (interface{}, error) {
return Secret, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*Myclaims); ok && token.Valid { //*Myclaims带指针
jwt.TimeFunc = time.Now
claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix() //设定增加的时间
return GenToken(claims.Username, claims.ID)
}
return "", errors.New("Couldn't handle this token")
}
JWTAuthMiddleware方法-拦截的认证中间件,每次访问都经过它,并检查携带的token
func JWTAuthMiddleware() func(ctx *gin.Context) {
return func(ctx *gin.Context) {
// 根据实际情况取TOKEN,可以从header\query\form等。
// 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI 。具体实现方式要依据你的实际业务情况决定
//tokenStr := ctx.Request.Header.Get("Authorization")//这里假设Token放在Header的Authorization中
tokenStr := ctx.Query("token")//这里假设从query取
if tokenStr == "" {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": -1,
"msg": "Have not token",
})
return
}
claims, err := ParseToken(tokenStr)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": -1,
"msg": "ERR_AUTH_INVALID",//token无效
})
return
} else if time.Now().Unix() > claims.ExpiresAt {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": -1,
"msg": "ERR_AUTH_EXPIRED",//token超市
})
return
}
// 此处已经通过了, 可以把Claims中的有效信息拿出来放入上下文使用
ctx.Set("username", claims.Username)
ctx.Set("id", claims.ID)
ctx.Next()
}
}
指定任意路由使用中间件
func main() {
r := gin.Default()
r.POST("/login", router.Login) //
authorized := r.Group("/auth") //设定组路由
authorized.Use(jwt.JWTAuthMiddleware())//部分路由使用中间件
{
authorized.GET("/getUserInfo", router.GetUserInfo)
}
r.Run(":8080")
}
然后在login方法中,检查用户名和密码正确后,使用gentoken生成token,token会返回设置到请求头中,如果没有,就要自行用ctx.set或http.set方法把token放入请求头。
JWT-GO最简单,但是功能比较少,后面再写一篇文章介绍功能更加强大但使用比较复杂的gin-jwt和hertz-jwt。