JWT学习

基本概念

jwt: JSON Web Token, 原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,
比如生成这样的结构:

{
    "userName": "test",
    "role": "Admin",
    "expire": 1584271948
}

之后,当用户与服务器通信时,客户在每次请求时都要带上这个JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名(下面会说)。

这样的话, 服务器就不用保存会话数据了,即服务器变为无状态,使其更容易扩展。

当然为了保密, 不会明文传输这个JSON, 而是将其打包到token中, 用户其实每次还给服务端的是这个token.
token是什么样子呢?
三部分: X.Y.Z (其中X, Y, Z分别代表三个字符串), 比如我临时生成的一个:
其中:
X: JWT头(Base64URL编码)
Y: 实际数据(Base64URL编码)
Z: 签名

比如对于下面的token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJtYWlsLnRlc3QuY29tIiwiZXhwIjoxNTg0MjcxODEwLCJpYXQiOjE1ODQyNzE3NTAsImlzcyI6InBzc3BvcnQudGVzdC5jb20iLCJuYmYiOjE1ODQyNzE3NTAsInN1YiI6IjEyMzQ1QHRlc3QuY29tIn0.8XbiPJ212JWUwUBmhtrBWbmVdB-jQdayyO29cAUd5qk

JWT头

$ echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d | jq
{
  "alg": "HS256",
  "typ": "JWT"
}

解析头部, 可以得到上述结果. alg表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ表示token的类型,JWT令牌统一写为JWT。

数据部分

$ echo 'eyJhdWQiOiJtYWlsLnRlc3QuY29tIiwiZXhwIjoxNTg0MjcxODEwLCJpYXQiOjE1ODQyNzE3NTAsImlzcyI6InBzc3BvcnQudGVzdC5jb20iLCJuYmYiOjE1ODQyNzE3NTAsInN1YiI6IjEyMzQ1QHRlc3QuY29tIn0' | base64 -d | jq
base64: invalid input
{
  "aud": "mail.test.com",
  "exp": 1584271810,
  "iat": 1584271750,
  "iss": "pssport.test.com",
  "nbf": 1584271750,
  "sub": "12345@test.com"
}

数据部分可以任意存放自己需要的值, 可以看到也是base64编码的, 所以像一些敏感信息就不能放在这里了, 上述只是举例.
注: 上面的"base64: invalid input"是因为最后少一个=,
用linux的base64命令解析会有警告, 不影响.

签名

签名部分就是个普通的字符串, 不是base64编码的.
计算方法(以HMACSHA256为例子):
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),
secret)

JWT头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见Base64算法类似,稍有差别。

作为令牌的JWT可以放在URL中(例如test.com/?token=xxx)。 Base64中用的三个字符是+/=,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:=去掉,+-替换,/_替换.

我们可以来验证一下, 上面的token用的密钥是"s3cret".
在这里插入图片描述
https://1024tools.com/hmac这个页面中, 输入图中参数,
其中消息是"X.Y",
看最后一行的结果"结果B:(HMAC计算返回原始二进制数据后进行Base64编码)":

8XbiPJ212JWUwUBmhtrBWbmVdB+jQdayyO29cAUd5qk=

按Base64URL的规则, 将计算结果的+改为-, =去掉, 就和上述的Z部分一样了.

实践

package main

import (
    "fmt"
    "time"

    "github.com/dgrijalva/jwt-go"
)

var (
    secret = []byte("s3cret")
)

func main() {
    token, err := CreateToken(secret)
    if err != nil {
        panic(err)
    }
    fmt.Println(token)

    payload, err := ParseToken(token)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", payload)
}

// Ref: https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac
func CreateToken(secret []byte) (token string, err error) {
    // 创建token对象, 指定了签名算法和请求参数
    jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
        Audience:  "mail.test.com",        // 接收方
        ExpiresAt: time.Now().Unix() + 60, // 过期时间
        IssuedAt:  time.Now().Unix(),      // 签发时间
        Issuer:    "pssport.test.com",     // 签发者
        NotBefore: time.Now().Unix(),      // 在此时间之前不可用
        Subject:   "12345@test.com",       // 面向的用户
    })

    // 签名 并 得到 使用密钥签名后的token
    if token, err = jwtToken.SignedString(secret); err != nil {
        return
    }
    return
}

// Ref: https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac
func ParseToken(tokenString string) (payload map[string]interface{}, err error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Don't forget to validate the alg is what you expect:
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return secret, nil
    })

    if err != nil {
        return
    }

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        payload = claims
    } else {
        err = fmt.Errorf("unexpected claims")
    }
    return
}

上面的例子演示了如何创建token, 及验证token, 解析出实际数据.

JWT的一般用法

客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header字段中。

参考

发布了239 篇原创文章 · 获赞 81 · 访问量 55万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览