整合JWT

What is JWT

  • JWT 是 JSON Web Token 的缩写,是为了在⽹络应⽤环境间传递声明⽽执⾏的⼀种基于JSON的开放标准((RFC 7519)。JWT 本身没有定义任何技术实现,它只是定义了⼀种基于 Token 的会话管理的规则,涵盖 Token 需要包含的标准内容和 Token 的⽣成过程,特别适⽤于分布式站点的单点登录(SSO)场景
  • 一段JWT Token 由”.”切割成头部、负载、签名三部分

JWT优缺点

  • JWT 拥有基于 Token 的会话管理⽅式所拥有的⼀切优势,不依赖 Cookie,使得其可以防⽌ CSRF 攻击,也能在禁⽤ Cookie 的浏览器环境中正常运⾏。
  • ⽽ JWT 的最⼤优势是服务端不再需要存储 Session,使得服务端认证鉴权业务可以⽅便扩展,避免存储Session 所需要引⼊的 Redis 等组件,降低了系统架构复杂度。但这也是 JWT 最⼤的劣势,由于有效期存储在 Token 中,JWT Token ⼀旦签发,就会在有效期内⼀直可⽤,⽆法在服务端废⽌,当⽤户进⾏登出操作,只能依赖客户端删除掉本地存储的 JWT Token,如果需要禁⽤⽤户,单纯使⽤ JWT 就⽆法做到了

Get JWT-Go

  • go get -u github.com/dgrijalva/jwt-go@v3.2.0

Where JWT-Token

  • 客户端携带Token可以有三个位置:1.请求头 2.请求体 3.放在URI
  • Token放在Headher的Authorization中,并且使用Bearer开头

Prepare Middleware-JWT

Prepare jwt auth(Middleware)

package jwt

import (
"errors"
"github.com/dgrijalva/jwt-go"
"time"
)

const TokenExpireTime = time.Hour * 2

var mySecret = []byte("Hello,Piwriw")

type MyClaims struct {
UserID int64 `json:"user_id"`
UserName string `json:"username"`
jwt.StandardClaims
}

// GenToken 生成JWT
func GenToken(userID int64, username string) (string, error) {
// 声明一个JWT
c := MyClaims{
userID,
"username", // 自定义字段
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireTime).Unix(), // 过期时间
Issuer: "my-project", // 签发人
},
}
// 使用指定的签名方法创建签发对象
token := jwt.NewWithClaims(jwt.SigningMethodES256, c)
// 使用指定secret签名获得完整编码之后的字符串token
return token.SignedString(mySecret)

}

// ParseToken : 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
// 解析token
// 如果是自定义Claim结构体则需要使用 ParseWithClaims 方法
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) {
// 直接使用标准的Claim则可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return mySecret, nil
})
if err != nil {
return nil, err
}
// 对token对象中的Claim进行类型断言
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid { // 校验token
return claims, nil
}
return nil, errors.New("invalid token")
}

Prepare jwt request

package controller

import (
"errors"
"github.com/gin-gonic/gin"
)
const CtxUserIDKey="userID"
var ErrorNotLogin=errors.New("用户未登录")

// getCurrentUser : 获取当前登录用户的ID
func getCurrentUser(c *gin.Context)(userID int64,err error) {
uid,ok:=c.Get(CtxUserIDKey)
if !ok {
err=ErrorNotLogin
return
}
userID,ok=uid.(int64)
if!ok{
err=ErrorNotLogin
return
}
return
}

How to use JWT

Gin 中注册中间件模式

  • v1.Use(middlewares.JWTAuthMiddleware()) // 应用JWT认证中间件

Example - 在Login中添加Code

// Login : 登录
func Login(p *models.ParamLogin)(token string,err error) {
user := &models.User{
UserName: p.Username,
Password: p.Password,
}
if err:=mysql.Login(user);err!=nil{
return "",err
}
// 生成JWT
return jwt.GenToken(user.UserID,user.UserName)
}

JWT - Refresh token

前⾯讲的 Token,都是 Access Token,也就是访问资源接⼝时所需要的 Token,还有另外⼀种Token,Refresh Token,通常情况下,Refresh Token 的有效期会⽐较⻓,⽽ Access Token 的有效期
⽐较短,当 Access Token 由于过期⽽失效时,使⽤ Refresh Token 就可以获取到新的 Access Token,如果 Refresh Token 也失效了,⽤户就只能重新登录了。

  • 在 JWT 的实践中,引⼊ Refresh Token,将会话管理流程改进如下。
    客户端使⽤⽤户名密码进⾏认证
  • 服务端⽣成有效时间较短的 Access Token(例如 10 分钟),和有效时间较⻓的 RefreshToken(例如 7 天)
  • 客户端访问需要认证的接⼝时,携带 Access Token
  • 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据
    如果携带 Access Token 访问需要认证的接⼝时鉴权失败(例如返回 401 错误),则客户端使⽤Refresh Token 向刷新接⼝申请新的 Access Token
  • 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token
  • 客户端使⽤新的 Access Token 访问需要认证的接⼝
    20230109200025

GenTokenWithRefreshToken 生成RefreshToken


// GenTokenWithRefreshToken : 生成Access token 和 RefreshToken
func GenTokenWithRefreshToken(userID int64) (acToken, reToken string, err error) {
c := MyClaims{
userID,
"username",
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireTime).Unix(), //签发日期
Issuer: "Piwirw", //签发人
},
}
// 加密并获得编码Token
acToken, err = jwt.NewWithClaims(jwt.SigningMethodES256, c).SignedString(mySecret)

// refresh token 不需要存储子自定义数据
reToken, err = jwt.NewWithClaims(jwt.SigningMethodES256, jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Second * 30).Unix(), // 过期时间
Issuer: "Piwriw", //签发人
}).SignedString(mySecret)
// 使用自定secret签名并获得完整后的Token
return
}

// ParseToken2 : 解析Access Token
func ParseToken2(tokenString string)(claims *MyClaims,err error) {
// 解析tokenn
var token *jwt.Token
claims=new(MyClaims)
token,err=jwt.ParseWithClaims(tokenString,claims, func(token *jwt.Token) (i interface{}, err error) {
// 直接使用标准的Claim则可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return mySecret, nil
})
if err != nil {
return
}
if !token.Valid {
err=errors.New("Invalid token")
}
return
}

RefreshToken 刷新token


// RefreshToken : 刷新RefreshToken
func RefreshToken(acToken, reToken string) (newToken, newReToken string, err error) {
// refresh token is invalid returnn
_,err=jwt.Parse(reToken,func(token *jwt.Token) (i interface{}, err error) {
// 直接使用标准的Claim则可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return mySecret, nil
})
if err!=nil{
return
}
// 从 older access token 解析出claims
var claims MyClaims
_,err=jwt.ParseWithClaims(acToken,&claims,func(token *jwt.Token) (i interface{}, err error) {
// 直接使用标准的Claim则可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return mySecret, nil
})
v,_:=err.(*jwt.ValidationError)
// 当access token is 过期错误并且 refresh token 没有过期 ->>>create a new access token
if v.Errors==jwt.ValidationErrorExpired{
return GenTokenWithRefreshToken(claims.UserID)
}
return
}

ParseToken 解析Access Token


// ParseToken2 : 解析Access Token
func ParseToken2(tokenString string)(claims *MyClaims,err error) {
// 解析tokenn
var token *jwt.Token
claims=new(MyClaims)
token,err=jwt.ParseWithClaims(tokenString,claims, func(token *jwt.Token) (i interface{}, err error) {
// 直接使用标准的Claim则可以直接使用Parse方法
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, err error) {
return mySecret, nil
})
if err != nil {
return
}
if !token.Valid {
err=errors.New("Invalid token")
}
return
}