Go语言使用JWT
我们知道有 cookie 和 session 来检验用户登录的方法,但是这过于麻烦并且储存数据很多。我们可以使用 token 方法来鉴权。
用户发送数据给后端,后端使用数据经加密签名形成复杂的字符串,我们可以叫做令牌。然后服务器将这个令牌交给客户端,以后客户端每次访问就带上这个令牌,然后服务端检验是否有这个的存在或这个令牌是否过期,检验通过后才会让客户端进行访问。
生成JWT
定义传入的信息
生成 token 需要用户自己特定信息,我们可以自定义结构体,加入自己想要的信息。其中 jwt.StandardClaims
是官方字段。
// MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
UserID uint64 `json:"user_id"`
Username string `json:"username"`
jwt.StandardClaims
}
我们的令牌是有过期时间的,所以我们还需定义 JWT 的过期时间
const TokenExpireDuration = time.Hour * 2
接下来还需要定义Secret:
var MySecret = []byte("syz")
生成JWT
- 自定义结构体
- 传入结构体和签名方法,创建签名对象
- 最后对令牌使用签名,获取完整的签名令牌
// GenToken 生成JWT
func GenToken(userID uint64, username string) (string, error) {
// 创建一个我们自己的声明的数据
c := MyClaims{
userID,
"username", // 自定义字段
jwt.StandardClaims{
ExpiresAt: time.Now().Add( //这里注意INT不能直接与time.hour相乘,所以用time.duration转换一下
time.Duration(viper.GetInt("auth.jet_expire")) * time.Hour).Unix(), // 过期时间
Issuer: "bluebell", // 签发人
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(mySecret)
}
解析JWT
// ParseToken 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
// 解析token
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) {
return MySecret, nil
})
if err != nil {
return nil, err
}
// 令牌有效
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid { // 校验token
return claims, nil
}
return nil, errors.New("invalid token")
}
研究函数
我们可以观察下面代码的细节,使用签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
这是 NewWithClaims
函数,可以看出它需要得到一个加密的方法和包含信息的结构体。然后使用该信息初始化一个 Token ,并返回它的指针。
func NewWithClaims(method SigningMethod, claims Claims) *Token {
return &Token{
Header: map[string]interface{}{
"typ": "JWT",
"alg": method.Alg(),
},
Claims: claims,
Method: method,
}
我们可以观察一下该 Token 所包含的信息。
type Token struct {
Raw string // The raw token. Populated when you Parse a token
Method SigningMethod // The signing method used or to be used
Header map[string]interface{} // The first segment of the token
Claims Claims // The second segment of the token
Signature string // The third segment of the token. Populated when you Parse a token
Valid bool // Is the token valid? Populated when you Parse/Verify a token
}
- Raw: 原始令牌
- Method: 使用或将要使用的签名方法
- Header: 令牌的第一段
- Claims: 令牌的第二段
- Signature: 令牌的第三段(解析令牌时填充)
- Valid: 令牌是否有效(解析令牌时填充)
最后对令牌进行签名,获取完整的签名令牌
func (t *Token) SignedString(key interface{}) (string, error) {
var sig, sstr string
var err error
if sstr, err = t.SigningString(); err != nil {
return "", err
}
if sig, err = t.Method.Sign(sstr, key); err != nil {
return "", err
}
return strings.Join([]string{sstr, sig}, "."), nil
}
在Gin框架中使用JWT
首先我们注册一条路由/auth
,对外提供获取Token的渠道:
r.POST("/auth", authHandler)
我们的authHandler
定义如下:
func authHandler(c *gin.Context) {
// 用户发送用户名和密码过来
var user UserInfo
err := c.ShouldBind(&user)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 2001,
"msg": "无效的参数",
})
return
}
// 校验用户名和密码是否正确(正确则返回token)
if user.Username == "q1mi" && user.Password == "q1mi123" {
// 生成Token
tokenString, _ := GenToken(user.Username)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"token": tokenString},
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 2002,
"msg": "鉴权失败",
})
return
}
用户通过上面的接口获取Token之后,后续就会携带着Token再来请求我们的其他接口,这个时候就需要对这些请求的Token进行校验操作了,很显然我们应该实现一个检验Token的中间件,具体实现如下:
// JWTAuthMiddleware 基于JWT的认证中间件
func JWTAuthMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
// 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
// 这里假设Token放在Header的Authorization中,并使用Bearer开头
// 这里的具体实现方式要依据你的实际业务情况决定
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
c.JSON(http.StatusOK, gin.H{
"code": 2003,
"msg": "请求头中auth为空",
})
c.Abort()
return
}
// 按空格分割
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusOK, gin.H{
"code": 2004,
"msg": "请求头中auth格式有误",
})
c.Abort()
return
}
// parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
mc, err := ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 2005,
"msg": "无效的Token",
})
c.Abort()
return
}
// 将当前请求的username信息保存到请求的上下文c上
c.Set("username", mc.Username)
c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
}
}
注册一个/home
路由,发个请求验证一下吧。
r.GET("/home", JWTAuthMiddleware(), homeHandler)
func homeHandler(c *gin.Context) {
username := c.MustGet("username").(string)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"username": username},
})
}
参考