JWT验证 | 青训营笔记

153 阅读3分钟

JWT验证 | 青训营笔记

这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天

JWT介绍

相较于传统的session方法,jwt验证无需在服务端储存用户的登录记录,大概流程是这样的:

  • 客户端使用用户名跟密码请求登录
  • 服务端收到请求,去验证用户名与密码
  • 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  • 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
  • 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  • 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

JWT分为Header,Claims,Signature这三部分。我们只需要管Claims就行,可以自定义我们需要的字段。

官方文档

JWT验证流程

  • 在头部信息中声明加密算法和常量,然后把 header 使用 json 转化为字符串
  • 在载荷中声明用户信息,同时还有一些其他的内容,再次使用 json 把在和部分进行转化,转化为字符串
  • 使用在 header 中声明的加密算法来进行加密,把第一部分字符串和第二部分的字符串结合和每个项目随机生成的 secret 字符串进行加密,生成新的字符串,此字符串是独一无二的
  • 解密的时候,只要客户端带着 jwt 来发起请求,服务端就直接使用 secret 进行解密,解签证解出第一部分和第二部分,然后比对第二部分的信息和客户端穿过来的信息是否一致。如果一致验证成功,否则验证失败。

创建JWT

通常使用 NewWithClaims,因为我们可以通过匿名结构体来实现 Claims 接口,从而携带自己的参数。

源码中的 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"`
 }

关注这些参数:NoteBefore 生效时间、ExpiresAt 过期时间、Issuer 签发者。

Claim可以用结构体写,也可以用map写,这里演示用结构体的写法:

 type MyClaim struct {
     Username string `json:"username"`
     Password string `json:"password"`
     jwt.StandardClaims
 }
 ​
 func main() {
     //创建一个JWT
     mySigningKey := []byte("askldfjaqwiopeklasdjqwerfasdfawerfsldfjkalsdfj") //密钥,不要告诉他人
     myclaim := MyClaim{
         Username: "金晖", //自定义
         Password: "怎么可能告诉你sb", //自定义
         StandardClaims: jwt.StandardClaims{
             NotBefore: time.Now().Unix() - 60, //生效时间
             ExpiresAt: time.Now().Unix() + 2*60*60, //失效时间
             Issuer: "金晖", //签发者
         },
     }
     jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, myclaim) //创建token(未加密),第一个参数是加密方法,第二个参数是自己定义的结构体
     fmt.Println(jwtToken)
     s, err := jwtToken.SignedString(mySigningKey) //s就是已签发的token(已加密)
     if err != nil {
         fmt.Println(err)
     }
     fmt.Println(s)
 ​
     
 }
 ​
 /*
 &{ 0xc000008138 map[alg:HS256 typ:JWT] {金晖 怎么可能告诉你sb { 1674986896  0 金晖 1674979636 }}  false}
 ​
 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IumHkeaZliIsInBhc3N3b3JkIjoi5oCO5LmI5Y-v6IO95ZGK6K-J5L2gc2IiLCJleHAiOjE2NzQ5OD
 Y4OTYsImlzcyI6IumHkeaZliIsIm5iZiI6MTY3NDk3OTYzNn0.e2SrjjvFSFIgLqmG1VCf3E1FO4Nwd1vjydw-NCDDgRI
 */

解析一个Token

主要通过以下方法实现:

func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error)

其中 keyFunc 是个特殊的回调函数,固定接受 *Token 类型指针,返回一个 i 和 err,i 就是我们先前约定的密钥

 //假设s是前端返回来的一个用户token,把他解析
 token, err := jwt.ParseWithClaims(s, &MyClaim{}, func(token *jwt.Token) (interface{}, error) {
     return mySigningKey, nil
 })
 if err != nil {
     fmt.Println(err)
 }

获取的 token 是一个 jwtToken 类型的数据,我们需要其中的 Claims。需要对 Claims 进行断言,然后取用即可。

 //对Claims进行断言并使用
 fmt.Println(token.Claims.(*MyClaim))
 fmt.Println(token.Claims.(*MyClaim).Username) //自定义的字段
 fmt.Println(token.Claims.(*MyClaim).Password) //自定义的字段