这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记。
1 JWT
1.1 JWT 简介
HTTP/HTTPS 是无状态的,所以无法完成认证服务。
常见的认证方法有 cookie 和 session,因为 cookie 的保密性问题,所以登录的认证鉴权一般不会使用 cookie。使用 session 的缺点在于,服务器要保存 session 数据,单机情况下是可以的,如果服务器是集群的,或者是跨域的服务导向架构,就要求 session 数据共享,这就会带来一系列的问题。
解决方法是服务器中不保存 session 的数据,所有数据都保存在客户端,每次请求都带着认证相关的数据。JWT就使用了这种方案。
JWT 是 JSON Web Token的缩写。好处是服务端就是无状态的了,这样服务端更容易进行扩展。
1.2 JWT 原理
JWT 是一个字符串,中间用 . 分成三个部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTUwMjMyOTQsImlkIjoxLCJvcmlnX2lhdCI6MTY1NTAxOTY5NH0.pBpZDyrwE2vw8h7bEV7FICobJmgbX9aVtMZFPoLVoNg
这三个部分是:
- Header(头部)
- Payload(负载)
- Signature(签名)
所以 JWT 的结构就是 Header.Payload.Signature
前两个部分是 JSON 使用 Base64URL 算法转码后的结果, Signature是对Header.Payload的签名,为了防止数据的篡改。
注下面提到的 Header 和 Payload 都是指使用 Base64URL 编码后的字符串
1.2.1 JWT 结构
Header
Header 解码出来的 JSON 对象通常是下面的样子
{
"alg": "HS256",
"typ": "JWT"
}
alg 属性表示签名的算法,typ 表示这个令牌(token)的类型,JWT令牌的值是JWT
Payload
Payload 解码出来的 JSON 对象用来存放实际需要传递的数据,JWT 规定了7个官方字段,供选用
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了官方字段之外,我们还可以使用自定义的字段,比如存放用户的 id, name等:
{
"id": 123456,
"name": root
}
Payload 字段一般存放什么(一般会存放用户id),用来认证用户
在实际项目中,我们一般会在Payload中存放后端需要的认证信息。
Signature
如果没有 Signature,JWT 就会变得和 cookie 一样不安全
Signature 是对前两部分的签名,防止数据篡改
Signature 是通过使用 Header 里面指定的签名算法(默认是 HMAC SHA256)和指定的密钥对Header.Payload 进行加密后的结果,密钥只有服务端知道。
服务端在收到 JWT 之后使用 Header 里指定的签名算法和指定的密钥对Header.Payload进行加密,如果和 Signature 一致,就说明 JWT 没有被篡改
使用方式
客户端每次与服务端进行通信都要携带 JWT,可以:将 JWT 放在 Cookie 里面自动发送,但是不能跨域。
下面的方式能解决跨域问题,但是每次请求的时候都要手动带上 JWT:
- 将 JWT 放在 HTTP 请求头的
Authorization字段里面 - 作为 HTTP 的请求参数,比如
http://xxx.com/request?token=<token> - 如果是 POST 请求的话,还可以放到请求体中
之后我们会看到 gin-jwt 是怎么实现这几种方式的
如何将 token 携带在请求中发送给服务器
注意:
JWT 不是加密的,Signature的作用就是保证JWT没有被篡改,而Header和Payload也很容易解码出来其中的JSON对象,所以
base64是用来转码的,不是用来加密的,也就是说通过base64转码后的字符串,可以轻易获取原数据
2 JWT 的一种实现 gin-jwt
gin-jwt 是 JWT 的一种实现,能够很好的支持 gin 框架的认证鉴权。
这里只介绍 gin-jwt 的基本使用,部分没有介绍的可以查看官方文档:github.com/appleboy/gi…
authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: identityKey,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*User); ok {
return jwt.MapClaims{
identityKey: v.UserName,
}
}
return jwt.MapClaims{}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var loginVals login
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
return &User{
UserName: userID,
LastName: "Bo-Yi",
FirstName: "Wu",
}, nil
}
return nil, jwt.ErrFailedAuthentication
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
// TokenLookup is a string in the form of "<source>:<name>" that is used
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
})
Key: 有服务器保管的密钥,用来生成签名Authenticator: 用来做用户的认证服务,比如通过用户名和密码来认证用户,认证后会返回用户的相关信息,这些信息会被传递给PayloadFuncPayloadFunc: 用来设置 JWT 的 Payload 中的数据,会返回一个Map结构的数据TokenLookup: 表明传递的 token 所在的位置,前面提到可以通过cookie,请求参数,Header等方式传递 token(注意,gin-jwt 没有实现从消息体中获取token),默认值是"header:Authorization",表示从 Header 里的Authorization字段中寻找 tokenTokenHeadName: 当通过HTTP的Header传递 token 时的名称,默认值是 Bearer,比如在 Header 中传递 token:Authorization: Bearer <token>TimeFunc: 提供当前时间,当token的时区和服务器的时区不一致时可以自定义TimeFunc
接下来我们看一个认证的例子:用户登录
首先前端会传入用户名和密码,后端在判断密码正确后要生成JWT返回给前端,我们可以看看gin-jwt是如何做这件事情的。
gin-jwt 提供了一个 LoginHandler 可以使我们很方便地完成这件事情,LoginHandler 的大概流程(不是完全的)是:
Authenticator 和 PayloadFunc 上面解释过。
注意其中,unautorized 函数是不能够自定义的,其他的都可以自定义,但是 unauthorized 函数会调用 Unauthorized 函数,而 Unauthoried 函数是可以自定义的,通过修改 Unauthorized 函数,我么可以指定认证失败的返回值。
可以通过自定义 LoginResponse 函数来修改认证成功的返回值,默认的返回是:
{
"code": http.StatusOK,
"token": token,
"expire": expire.Format(time.RFC3339)
}
也就是说,在登录成功后,我们就可以获得 token 了,之后再发送请求的时候就可以携带 token 来认证了。
那服务器是怎样处理 token 呢,gin-jwt 提供了一个中间件函数 MiddlewareFunc() 来处理认证,它会提取 token 中的数据,检验 token 是否合法等,一般在需要认证的接口中使用这个中间件,下面是 gin 中的一个例子
user := r.Group("/user")
user.User(authMiddleware.MiddlewareFunc())
{
api.GET("/info", controller.GetUserInfo)
api.POST("/logout", controller.Logout)
}
MiddlewareFunc() 的具体流程是:
- 寻找 token: 是根据前面提到的 TokenLookup 参数指定的位置进行寻找
- 解析 token: 通过Header部分指定的签名算法和密钥 Key,判断token是否有效,有效则解析 token 获取 token 中的 claims 部分(对应前面的 Payload),返回的 claims 是一个 map 结构
- 然后从 claims 中获取 exp 字段来判断 token 是否过期
- 如果没过期,就将 claims 写入 gin.Context 中的 "JWT_PAYLOAD":
c.Set("JWT_PAYLOAD", claims),之后的 handler 就可以通过 gin-jwt 提供的 ExtractClaims 函数从 gin.Context 中获取 claims - 最后,调用下一个 HandlerFunc:
c.Next()