这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
1 JWT介绍
JSON Web令牌(JWT)是一个开放标准(RFC7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON对象安全地传输信息。 由于此信息是经过数字签名的,因此可以被验证和信任。 可以使用使用RSA或ECDSA的公用/专用密钥对对JWT进行签名,其格式如下:
JSON Web令牌(JWT)由三部分组成,每部分用“.”相隔,三个部分分别表示:
- Header:头部,包含令牌类型和签名算法名称(HMAC SHA256、RSA 等)
- Payload:有效载荷,用于存储信息,应包含claims
- Signature:签名,通过Header中申请使用的签名算法,加密Header和Payload数据
我们定义好的header、payload字段需经过base64UrlEncode算法的转换,才能成为token的一部分。
Base64UrlEncode是Base64算法的变种,为什么要变呢,原因是在业界中我们经常可以看到JWT令牌会被放入Header或Query Param中(也就是URL)。
而在URL中,一些个别字符是有特殊意义的,例如:“+”、“/”、“=” 等等,因此在Base64UrlEncode算法中,会对其进行替换,例如:“+” 替换为 “-”、“/” 替换成 “_”、“=” 会被进行忽略处理,以此来保证JWT令牌的在URL中的可用性和准确性。
2 准备
添加JWT依赖,在终端执行代码:
go get github.com/dgrijalva/jwt-go
3 编写功能代码
首先,定义加密密钥和claims。
加密密钥应只有服务提供方自己知道(一般写在配置文件中,此处为方便定义成了变量)!
// jwtSecret is the secret used to encrypt header and payload
var jwtSecret = []byte("this_is_a_secret")
Claims中包含token的配置信息,jwt-go库中预定义的 jwt.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"`
}
其中包含 ExpiresAt 过期时间、Issuer 发布者等信息。
我们可以直接使用 jwt.StandardClaims,也可以结合应用场景,设计自己的鉴权场景。例如在服务中会频繁应用到当前用户ID,那么我们可以将用户ID加入到claims中:
// Claims of the token
type Claims struct {
UserId uint
jwt.StandardClaims
}
注意:敏感信息不要放在claims中!
其次,定义token发放函数和解析函数。
发放函数
// ReleaseToken release a token for specified user
func ReleaseToken(userId uint) (string, error) {
// token的有效期为7天
expirationTime := time.Now().Add(7 * 24 * time.Hour)
claims := &Claims{
// 自定义字段
UserId: userId,
// 标准字段
StandardClaims: jwt.StandardClaims{
// 过期时间
ExpiresAt: expirationTime.Unix(),
// 发放的时间
IssuedAt: time.Now().Unix(),
// 发放者
Issuer: "Comintern",
// 主题
Subject: "user token",
},
}
// 指定加密算法,利用密钥加密header和payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return "", err
}
// 返回token
return tokenString, nil
}
解析函数
// ParseToken parses a token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
return jwtSecret, nil
})
return token, claims, err
}
Claims中解析出的信息方便我们后续使用。
完整脚本
import (
"github.com/dgrijalva/jwt-go"
"time"
)
// jwtSecret is the secret used to encrypt header and payload
var jwtSecret = []byte("this_is_a_secret")
// Claims of the token
type Claims struct {
UserId uint
jwt.StandardClaims
}
// ReleaseToken release a token for specified user
func ReleaseToken(userId uint) (string, error) {
// token的有效期为7天
expirationTime := time.Now().Add(7 * 24 * time.Hour)
claims := &Claims{
// 自定义字段
UserId: userId,
// 标准字段
StandardClaims: jwt.StandardClaims{
// 过期时间
ExpiresAt: expirationTime.Unix(),
// 发放的时间
IssuedAt: time.Now().Unix(),
// 发放者
Issuer: "Comintern",
// 主题
Subject: "user token",
},
}
// 指定加密算法,利用密钥加密header和payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return "", err
}
// 返回token
return tokenString, nil
}
// ParseToken parses a token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
return jwtSecret, nil
})
return token, claims, err
}
func main() {
token, err := ReleaseToken(23)
if err != nil {
log.Fatal(err)
}
fmt.Println(token)
_, claims, err := ParseToken(token)
fmt.Println(claims.UserId)
}
运行结果
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIzLCJleHAiOjE2NTU0NTg1MDgsIml
hdCI6MTY1NDg1MzcwOCwiaXNzIjoiQ29taW50ZXJuIiwic3ViIjoidXNlciB0b2tlbiJ9.y84lEZEs64
v8S-qCg_DK8xsYncrkN3MTgWha9u3scY4
23
为方便使用,我们可将token验证过程写成中间件。