文章目录
Gin中使用jwt
JWT介绍
在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT的构成
JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HS256
完整的头部就像下面这样的JSON:
{"alg":"HS256","typ":"JWT"}
然后将头部进行base64编码构成了第一部分.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避时序攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{"foo":"bar","nbf":1444478400}
然后将其进行base64加密,得到JWT的第二部分。
eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9
signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
// u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9.u1riaD1rW97opCoAuRCTy4w58Br-Zk-bh7vLiRIsrpU
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
本质原理
/*
1)jwt分三段式:头.体.签名 (head.payload.sgin)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间
{
"user_id": 1,
...
}
6)签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密
{
"head": "头的加密字符串",
"payload": "体的加密字符串",
"secret_key": "安全码"
}
*/
签发
根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token
/*
1)用基本信息存储json字典,采用base64算法加密得到 头字符串
2)用关键信息存储json字典,采用base64算法加密得到 体字符串
3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串
账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台
*/
校验
根据客户端带token的请求 反解出 user 对象
/*
1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的
3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户
*/
jwt认证开发流程(重点)
/*
1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中
2)校验token的算法应该写在中间件中,所有请求,都会进行认证校验,所以请求带了token,就会反解出用户信息
*/
base64编码解码
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// 1 编码
res:=base64.StdEncoding.EncodeToString([]byte("lxx is nb"))
fmt.Println(res)
//2 解码
res,err:=base64.StdEncoding.DecodeString("eyJmb28iOiJiYXIiLCJuYmYiOjE0NDQ0Nzg0MDB9")
if err != nil {
fmt.Println("解码出错,",err)
return
}
fmt.Println(string(res))
}
Gin中使用jwt
github地址:https://github.com/golang-jwt/jwt
文档地址:https://pkg.go.dev/github.com/golang-jwt/jwt
下载:go get github.com/golang-jwt/jwt
签发token和验证token
package main
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt"
"time"
)
// 第一步:定义结构体
// MyClaims 定义结构体并继承jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们需要额外记录一个username和id字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyCustomClaims struct {
Id int `json:"id"`
Username string `json:"username"`
jwt.StandardClaims
}
// 定义加密秘钥
var mySigningKey = []byte("lxxisnb")
func genToken(claims MyCustomClaims) (string, error) {
// 使用HS256加密方式,注意不要使用ES256,非对称加密会报错
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signToken, err := token.SignedString(mySigningKey)
if err != nil {
return "", err
}
return signToken, nil
}
// 简单解析token
func parserToken(signToken string) (*MyCustomClaims, error) {
var claims MyCustomClaims
token, err := jwt.ParseWithClaims(signToken, &claims, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if token.Valid {
return &claims, nil
} else {
return nil, err
}
}
// 带详细错误的解析
func parserTokenWithError(signToken string) (*MyCustomClaims, error) {
var claims MyCustomClaims
token, err := jwt.ParseWithClaims(signToken, &claims, func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
})
if token.Valid {
return &claims, nil
} else if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, errors.New("不是一个合法的token")
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
return nil, errors.New("token过期了")
} else {
fmt.Println("Couldn't handle this token:", err)
return nil, errors.New("无法处理这个token")
}
} else {
return nil, errors.New("无法处理这个token")
}
}
func main() {
claims := MyCustomClaims{
1,
"lxx",
jwt.StandardClaims{
ExpiresAt: time.Now().Add(7 * time.Hour).Unix(), // 过期时间7小时
Issuer: "lxx", // 签发人
},
}
signToken, _ := genToken(claims)
fmt.Println(signToken)
c, err := parserToken(signToken)
fmt.Println(c, err)
}
Gin框架中集成
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"net/http"
"time"
)
// 基于JWT的认证中间件
func JWTAuthMiddleware(c *gin.Context) {
// 从请求头中取出
signToken := c.Request.Header.Get("Authorization")
if signToken == "" {
c.JSON(http.StatusOK, gin.H{
"code": 1002,
"msg": "token为空",
})
c.Abort()
return
}
// 校验token
myclaims, err := parserToken(signToken)
if err != nil {
fmt.Println(err)
c.JSON(http.StatusOK, gin.H{
"code": 1003,
"msg": "token校验失败",
})
c.Abort()
return
}
// 将用户的id放在到请求的上下文c上
c.Set("userid", myclaims.Id)
c.Next() // 后续的处理函数可以用过c.Get("userid")来获取当前请求的id
}
func main() {
r := gin.Default()
r.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
if username == "lxx" && password == "123" {
token, _ := genToken(MyCustomClaims{1, "lxx", jwt.StandardClaims{
ExpiresAt: time.Now().Add(7 * time.Hour).Unix(), // 过期时间
Issuer: "lxx", // 签发人
}})
c.JSON(200, gin.H{"code": "100", "msg": "登陆成功", "token": token})
} else {
c.JSON(200, gin.H{"code": "101", "msg": "用户名或密码错误"})
}
})
// 该接口登陆后才能访问,加中间件
r.GET("/home", JWTAuthMiddleware, func(c *gin.Context) {
fmt.Println(c.Get("userid"))
c.JSON(200, gin.H{"code": 100, "msg": "home"})
})
r.Run(":8080")
}
补充
双token 认证
token 由于有过期时间,过期时间过长会存在泄漏风险,因此 目前有一种双token认证的方法,能有效预防该风险.理论如下
- 登录成功之后, 服务端向前端返回两个token,一个token(refreshToken)过期时间为7天,一个token(accessToken)过期时间为10分钟
- 以后请求,都采用accessToken发送请求,10分后过期,服务端检测到了过期,就返回一个 token 过期给前端, 前端再发一个请求, 携带 refreshToken过来,服务端再签发一个 accessToken给前端。
- refeshToken 也会跟着 accessToken 的刷新而刷新