likes
comments
collection
share

在你的Gin项目中添加jwt功能吧

作者站长头像
站长
· 阅读数 17

啥是JWT

JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token实现方式,目前多用于前后端分离项目和OAuth2.0业务场景下。

为什么要用在你的Gin中使用JWT

传统的Cookie-Sesson模式占用服务器内存, 拓展性不好,遇到集群或者跨服务验证的场景的话, 要支持Sesson复制或者sesson持久化

JWT的基本原理

在服务器验证之后, 得到用户信息JSON

{
     "user_id": "xxxxxx",
    "role": "xxxxxx",
    "refresh_token": "xxxxx"
}

JWT TOKEN怎么组成

JWT是一个很长的字符串

eyJhbGciOiJI123afasrwrqqewrcCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

在你的Gin项目中添加jwt功能吧

它由三部分组成, 每部分用点(.)分隔, 三个部分依次如下

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

Header

Header是一个经过BASE64URL算法加密过的JSON对象, 解密后如下

{
  "alg": "HS256",
  "typ": "JWT"
}

其中, alg属性表示签名所用的算法,默认是HS256;

typ则表示当前token的类型, 而JWT的类型则为jwt

Base64URL

与BASE64类似, 由于+/=这几个符号在URL中有特殊含义, 因此BASE64RUL算法, 把这几个符号进行了替换

+ -> -

= -> 被忽略

/ -> _

Payload

Payload部分也是JSON对象经过BASE64URL算法转成字符串的, Payload部分包含7个基本字段

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

也可以往里面塞入自定义的业务字段, 如下

  • user_id
  • role

Signature

Signature 部分是对前两部分的签名,防止数据篡改

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

解密过程

当系统接收到TOKEN时, 拿出Header和Payload的字符串用.拼接在一起之后, 用Header里面指定的哈希方法通过公式

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

进行加密得出密文

然后用刚刚得出的密文与TOKEN传过来的密文对比, 如果相等则表明密文没有更改.

一些特点(优点和缺点)

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

GIN整合JWT

go get -u github.com/dgrijalva/jwt-go
go get github.com/gin-gonic/gin

编写jwtutil

var Secret = []byte("whatasecret")

// jwt过期时间, 按照实际环境设置
const expiration = 2 * time.Minute

type Claims struct {
	// 自定义字段, 可以存在用户名, 用户ID, 用户角色等等
	Username string
	// jwt.StandardClaims包含了官方定义的字段
	jwt.StandardClaims
}

func GenToken(username string) (string, error) {
	// 创建声明
	a := &Claims{
		Username: username,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间
			IssuedAt:  time.Now().Unix(),                 // 签发时间
			Issuer:    "gin-jwt-demo",                    // 签发者
			Id:        "",                                // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效
			NotBefore: 0,                                 // 生效起始时间
			Subject:   "",                                // 主题
		},
	}
	
	// 用指定的哈希方法创建签名对象
	tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
	// 用上面的声明和签名对象签名字符串token
	// 1. 先对Header和PayLoad进行Base64URL转换
	// 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
	// 3. 用secret对拼接在一起之后的字符串进行HASH加密
	// 4. 连在一起返回
	return tt.SignedString(Secret)
}

func ParseToken(tokenStr string) (*Claims, error) {
	// 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return Secret, nil
	})
	if err != nil {
		return nil, err
	}
	// 校验token
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

Secret是秘钥, 用于加密签名

expiration是TOKEN过期时间

Claims是签名声明对象, 包含自定义的字段和JWT规定的字段

type Claims struct {
	// 自定义字段, 可以存在用户名, 用户ID, 用户角色等等
	Username string
	// jwt.StandardClaims包含了官方定义的字段
	jwt.StandardClaims
}

type StandardClaims struct {
	Audience  string `json:"aud,omitempty"`
	ExpiresAt int64  `json:"exp,omitempty"`
	Id        string `json:"jti,omitempty"`
	IssuedAt  int64  `json:"iat,omitempty"`
	Issuer    string `json:"iss,omitempty"`
	NotBefore int64  `json:"nbf,omitempty"`
	Subject   string `json:"sub,omitempty"`
}

GenToken方法

GenToken方法为某个username生成一个token, 每次生成都不一样

jwt.NewWithClaims(jwt.SigningMethodHS256, a)声明了一个签名对象, 并且指定了HS256的哈希算法

token.SignedString(Secret)表明用刚刚的声明对象和SECRET利用指定的哈希算法去加密,包含下面流程

  1. 先对Header和PayLoad进行Base64URL转换
  2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
  3. 用secret对拼接在一起之后的字符串进行HASH加密
  4. BASE64URL(Header).BASE64URL(Payload).signature连在一起的字符串返回
func GenToken(username string) (string, error) {
	// 创建声明
	a := &Claims{
		Username: username,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间
			IssuedAt:  time.Now().Unix(),                 // 签发时间
			Issuer:    "gin-jwt-demo",                    // 签发者
			Id:        "",                                // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效
			NotBefore: 0,                                 // 生效起始时间
			Subject:   "",                                // 主题
		},
	}
	
	// 用指定的哈希方法创建签名对象
	tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
	// 用上面的声明和签名对象签名字符串token
	// 1. 先对Header和PayLoad进行Base64URL转换
	// 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
	// 3. 用secret对拼接在一起之后的字符串进行HASH加密
	// 4. 连在一起返回
	return tt.SignedString(Secret)
}

ParseToken方法

ParseToken方法解析一个Token, 并验证Token是否生效

jwt.ParseWithClaims方法, 用于解析Token, 其第三个参数:

提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如判断kid来选用不同的secret

KID(可选): 代表秘钥序号。开发人员可以用它标识认证token的某一秘钥

func ParseToken(tokenStr string) (*Claims, error) {
	// 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return Secret, nil
	})
	if err != nil {
		return nil, err
	}
	// 校验token
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

编写中间件

从Header中取出Authorization并拿去解析jwt.ParseToken,

验证token是否被串改, 是否过期

从token取出有效信息并设置到上下文

func JWTAuthMiddleware() func(ctx *gin.Context) {
	return func(ctx *gin.Context) {
		// 根据实际情况取TOKEN, 这里从request header取
		tokenStr := ctx.Request.Header.Get("Authorization")
		if tokenStr == "" {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"code": code.ERR_AUTH_NULL,
				"msg":  code.GetMsg(code.ERR_AUTH_NULL),
			})
			return
		}
		
		claims, err := jwt.ParseToken(tokenStr)
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"code": code.ERR_AUTH_INVALID,
				"msg":  code.GetMsg(code.ERR_AUTH_INVALID),
			})
			return
		} else if time.Now().Unix() > claims.ExpiresAt {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"code": code.ERR_AUTH_EXPIRED,
				"msg":  code.GetMsg(code.ERR_AUTH_EXPIRED),
			})
			return
		}
		
		// 此处已经通过了, 可以把Claims中的有效信息拿出来放入上下文使用
		ctx.Set("username", claims.Username)
		ctx.Next()
	}
}

使用中间件

/login不用中间件

中间件指定在authorizedrouter, 因此authorized下的所有路由都会使用此中间件

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(":8082")
}

测试

login请求获取token

POST http://localhost:8082/login

在你的Gin项目中添加jwt功能吧

把token放入getUserInfo请求

GET http://localhost:8082/auth/getUserInfo

在你的Gin项目中添加jwt功能吧

其他

完整的JWT登录还应该包括

  1. 使TOKEN失效(过期或者黑名单等功能)
  2. refresh token

通过上述流程你就能快速的在你的Go Gin项目中应用JWT能力