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, } }
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 NickName string AuthorityId uint jwt.RegisteredClaims }
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") } 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) if err != nil { c.JSON(http.StatusOK, err.Error()) return } if u.Name == "mitaka" && u.Password == "123" { 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 } c.Set("id", verify.ID) } }
|
微服务之间验证
- 服务A将认证信息通过公钥加密,发送给统一认证服务,验明服务身份,附带服务信息,统一认证服务验证
JWT
之后,完成该服务认证,将该服务能够访问的信息通过私钥加密,返回另外一个JWT
给到对应服务,该服务通过此JWT
在服务之间验证