这是我参与「第五届⻘训营 」笔记创作活动的第19天。
这篇文章记录了项目中jwt(JSON Web Token)验证的实现。
什么是JWT
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。
传统的用户认证流程,即服务器在session中保存登录用户信息,返回给用户id,用户在每次访问服务器时每次将id放入cookie中发送给服务端,服务端根据id验证用户登录状态。
这种解决方案的一个弊端是,如果系统部署在集群中,用户访问的机器不定,所有的机器都需要保存登录的用户信息,造成了服务端实现上的不便。
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
jwt原理
jwt的原理是,服务器完成验证后,生成一个json对象,返回给用户,之后每次通信时候,都需要发送这个json对象。为防止用户篡改数据,服务器生成这个对象时候会加上签名。
jwt结构
实际的jwt大概是这样
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlhpYW9NaW4iLCJleHAiOjE2NzY0NDM0OTl9.zhsKFmV3gdmcqQ81B6Woq10JmX1l56GQUeyOP44L6lo
分为三个部分,由‘.’隔开,分别为Header、Payload、Signature。
Header
Header部分是一个Json对象,描述JWT元数据,通常是这样
{
"alg": "HS256",
"typ": "JWT"
}
alg表示签名算法,typ属性表示令牌类型,jwt令牌写作JWT。实际的jwt中此部分使用base64URL算法转成字符,此步骤不加密。
Payload
此部分也是一个JSON对象,存放实际需要传递数据。 官方规定字段。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号 除了官方字段自己也可以存放一些私有字段,当然由于jwt不加密,不要放密码之类的敏感信息。 同样此部分也是用base64URL转为字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名后,将三部分使用点拼接成一个字符串返回给用户。
jwt客户端使用方法
客户端收到服务器返回的JWT之后,每次与服务器通信都需要带上JWT,如果放到cookie中不可以跨域,建议放到HTTP信息头的Authorization字段里面。
golang中jwt
使用jwt-go来方便的进行jwt令牌发放和验证。
安装
go get -u github.com/golang-jwt/jwt/v4
导入
import "github.com/golang-jwt/jwt/v4"
生成令牌
mySigningKey := []byte("AllYourBase")
type MyCustomClaims struct {
Foo string `json:"foo"`
jwt.RegisteredClaims
}
// Create the claims
claims := MyCustomClaims{
"bar",
jwt.RegisteredClaims{
// A usual scenario is to set the expiration time relative to the current time
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "test",
Subject: "somebody",
ID: "1",
Audience: []string{"somebody_else"},
},
}
// Create claims while leaving out some of the optional fields
claims = MyCustomClaims{
"bar",
jwt.RegisteredClaims{
// Also fixed dates can be used for the NumericDate
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
Issuer: "test",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)
output:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil>
此处在登录或注册时候将生成的令牌返回给客户端,以便客户端之后的使用。
解析
// Token from another example. This token is expired
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("AllYourBase"), nil
})
if token.Valid {
fmt.Println("You look nice today")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("That's not even a token")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
// Token is either expired or not active yet
fmt.Println("Timing is everything")
} else {
fmt.Println("Couldn't handle this token:", err)
}
output:
Timing is everything
gin验证中间件实现
// JWTAuthMiddleware 基于JWT的认证中间件--验证用户是否登录
func JWTAuthMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
authHeader := c.Request.Header.Get("authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 2003,
"msg": "请求头中auth为空",
})
c.Abort()
return
}
// 按空格分割
parts := strings.Split(authHeader, ".")
if len(parts) != 3 {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 2004,
"msg": "请求头中auth格式有误",
})
c.Abort()
return
}
mc, ok := ParseToken(authHeader, Secret)
if ok == false {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 2005,
"msg": "无效的Token",
})
c.Abort()
return
}
m := mc.(jwt.MapClaims)
// 将当前请求的username信息保存到请求的上下文c上
c.Set("username", m["username"])
c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
}
}