JWT教程

Json Web Token

加密

对称加密:指对数据加密和解密使用同一个密钥,例如JWT常用的HS256算法,客户端用这套密钥加密,服务端用同一套密钥解密,所以需要确保密钥不被泄露。

非对称加密:指对数据加密和解密使用一对密钥,分为公钥和私钥,例如JWT常用的RS256算法。服务端将公钥发给客户端,客户端使用公钥加密,将加密后的数据返回给服务端,服务端使用私钥解密即可获取数据。这种方式更加可靠、

组成

对称加密(HS256算法)

Encode:其中包含三个部分

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

第一个部分 HEADER

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

decode结果:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

包含加密算法和token类型

第二个部分:PAYLOAD

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

decode结果:

1
2
3
4
5
6
7
8
9
10
{
"sub": "1234567890", // 一般放UserID
"name": "John Doe", // 用户名称
"iat": 1516239022, // 颁发时间
"exp": 1516246222, // 过期时间
"iss": "jwtlearn", // 颁发者
"aud": "jwtlearn/jwtapplication", // 颁发应用
"nbf": "1516239122", // 颁发之后多久才能使用
"something_else": ""
}

包含的结果是数据,数据内容可自定义,内置参数JWT PAYLOAD内置参数

第三个部分:VERIFY SIGNATURE

1
Mn9QmUhVGJnI90ed2gjvAj5C0d4nRb4f4AjNg3615YU

decode结果:

1
2
3
4
5
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
abc // 密码
)

通过密码签名,相同的Encode,通过这个签名可以解析Payload

这种方式,客户端获取到JWT之后,需要去认证服务请求密码和加密算法,验证JWT的签名是否可以通过验证。密码需要所有的服务都知道,在微服务场景下不方便,而且密码泄露之后安全性无法保证。

非对称加密(RS256算法)

更多的使用在签名的场景,也就是确认信息的发送者。通过私钥加密,发送到对端,对端通过公钥解密。

JWT的格式与上面的对称加密一样,区别是第三部分内容所有不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RSASHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),

-----BEGIN PUBLIC KEY-----
... // 此处是一个RSA密钥对的公钥
-----END PUBLIC KEY-----
,

-----BEGIN PRIVATE KEY-----
... // 此处是一个RSA密钥对的私钥
-----END PRIVATE KEY-----

)

认证服务放私钥进行签名,其他服务校验的时候,通过公钥验签。

代码实现

token生成端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"crypto/rsa"
"time"

"github.com/golang-jwt/jwt/v4"
)

type TokenGenerator interface {
GeneraToken(accountID string, expire time.Duration) (string, error) // 从外部传进来
}

type JWTTokenGen struct { // 内部自定义
Privatekey *rsa.PrivateKey // 私钥文件
Issure string
nowFunc func() time.Time // 通过函数定义,可用于测试环境下自定义时间
}

func NewJWTTokenGen(issure string, privatekey *rsa.PrivateKey) *JWTTokenGen {
return &JWTTokenGen{
Privatekey: privatekey,
Issure: issure,
nowFunc: time.Now,
}
}

// GeneraToken 过期时间可以自定义
func (t *JWTTokenGen) GeneraToken(userID uint, authorityId uint, expire time.Duration) (string, error) {
nowSec := t.nowFunc().Add(expire)
jtk := jwt.NewWithClaims(jwt.SigningMethodRS512,
CustomClaims{
ID: userID,
AuthorityId: authorityId,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: t.Issure,
ExpiresAt: jwt.NewNumericDate(nowSec),
},
},
)

return jtk.SignedString(t.Privatekey)
}

需要注意的是,jwt官网加密payload的时候顺序与代码不一致,jwt官网按照填入内容排序,代码按照字母顺序排序,所以官网payload内容如下

1
2
3
4
5
6
7
{
"exp": 1516239142,
"iat": 1516239022,
"something_else": "this is a test",
"sub": "account_xu",
"uss": "authserver/jwt"
}

实际使用的 claims如下

1
2
3
4
5
6
7
8
type CustomClaims struct {
ID uint // 用户id,业务层面的id
NickName string // 用户昵称
AuthorityId uint // 认证id,一般包含该用户的权限
jwt.RegisteredClaims // JWT 默认claims,也可以直接使用MapClaims
}

type MapClaims map[string]interface{}

验证端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"crypto/rsa"
"fmt"

"github.com/golang-jwt/jwt/v4"
)

type JWTTokenVerifier struct {
PublicKey *rsa.PublicKey
}

func NewJWTTokenVerifier(publicKey *rsa.PublicKey) *JWTTokenVerifier {
return &JWTTokenVerifier{
PublicKey: publicKey,
}
}

func (v *JWTTokenVerifier) Verify(token string) (CustomClaims, error) {
var claims CustomClaims
t, err := jwt.ParseWithClaims(token, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return v.PublicKey, nil
})
if err != nil {
return claims, fmt.Errorf("cannot parse token: %v", err)
}

if !t.Valid {
return claims, fmt.Errorf("token not valid")
}

// 断言
clm, ok := t.Claims.(*CustomClaims)
if !ok {
return claims, fmt.Errorf("claims not a mapclaims: %t", t.Claims)
}

if err = clm.Valid(); err != nil {
return claims, fmt.Errorf("claims not valid")
}
// 返回claims
return *clm, nil
}

使用方案:

登录验证

  • login接口登录成功之后,认证服务将user信息放到JWT中,可以将用户的权限、角色也一并放入,其他应用在验证的时候,通过JWT信息验证用户和权限即可。
    • 缺点:修改密码,修改用户权限都需要重新登录,更新JWT

使用midware验证jwt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"log"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
)

type User struct {
Name string `json:"name" binding:"required"`
Password string `json:"password" binding:"required"`
}

var tokenGen *JWTTokenGen
var tokenVerifier *JWTTokenVerifier

func main() {
pem, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey))
if err != nil {
log.Fatalf("cannot parse private key: %v", err)
}

tokenGen = NewJWTTokenGen("test", pem)

publicPem, err := jwt.ParseRSAPublicKeyFromPEM([]byte(publicKey))
if err != nil {
log.Fatalf("cannot parse public key: %v", err)
}
tokenVerifier = NewJWTTokenVerifier(publicPem)

r := gin.Default()
r.POST("/login", Login)

r.Use(JWTAuth()) // 在所有的请求接口中都会用到这个中间件
r.POST("/list", List)
r.Run(":8080")
}

func Login(c *gin.Context) {
var u User
err := c.ShouldBind(&u) // 当密码为空,返回200,返回值为空
if err != nil {
c.JSON(http.StatusOK, err.Error())
return
}
if u.Name == "mitaka" && u.Password == "123" {
// 传入用户id,用户权限,过期时间,生成token
token, err := tokenGen.GeneraToken(1, 10, 2*time.Hour)
if err != nil {
c.JSON(http.StatusInternalServerError, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
})
}
return
}

func List(c *gin.Context) {
id, _ := c.Get("id")
c.JSON(http.StatusOK, gin.H{
"userID": id,
})
}

midware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
)

type CustomClaims struct {
ID uint
NickName string
AuthorityId uint
jwt.RegisteredClaims
}

func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Request.Header.Get("x-token")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"msg": "请登录",
})
c.Abort()
return
}

verify, err := tokenVerifier.Verify(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"msg": "认证失败",
})
c.Abort()
return
}
// 将token中的id注入的gin.Context中
c.Set("id", verify.ID)
}
}

微服务之间验证

  • 服务A将认证信息通过公钥加密,发送给统一认证服务,验明服务身份,附带服务信息,统一认证服务验证JWT之后,完成该服务认证,将该服务能够访问的信息通过私钥加密,返回另外一个JWT给到对应服务,该服务通过此JWT在服务之间验证
    • 使用场景:外部服务注册进入系统,对接第三方