Jwt简介
Jwt就是Json Web Token,它是RFC 7519定义的一种令牌结构,一般用于服务端在客户端认证成功后返回给客户端保存,假如客户端后续在http请求头带上这个令牌,那么就可以证明它的身份,实现免登录。
一个例子:
Jwt本质上是一个由三段数据用.
拼接而成的字符串,其中前二段是json字符串用base64编码后得到的字符串,而第三段则是前两段base64编码并拼接后用第一段声明的加密方法(如hs256),用密钥加密后的签名。接下来讲一下前两段的具体内容:
第一段Header主要用来声明token是jwt以及加密的算法:
1
2
3
4
|
{
"alg": "HS256",
"typ": "JWT"
}
|
第二段Payload则是具体传递的令牌内容,如用户id,到期时间等:
1
2
3
4
|
{
"loggedInAs": "admin",
"iat": 1422779638
}
|
在客户端登录的时候,客户端校验用户名和密码,然后生成对应的jwt返回给客户端,客户端存起来,在后续的请求中都在请求头带上这个token来证明自己的身份。
而服务端在收到jwt后,用自己的密钥重新计算jwt前两段,看得出的是否与第三段相等,假如是,则说明对方的jwt确实是自己签发的,于是信任请求的来源,这便是jwt的具体验证过程。
接下来我们来看看如何在gin框架中实现jwt用户登录。
创建并加载gin-jwt中间件
在开发基于gin框架的web app时,我们可以使用gin-jwt来实现登录认证,下面记录一下如何使用这个中间件。
在gin框架中加载jwt中间件之前,我们需要创建一个GinJWTMiddleware
的结构体,并定义好jwt认证相关选项,这是结构体中比较重要的字段:
1
2
3
4
5
6
7
8
9
10
11
12
|
type GinJWTMiddleware struct {
Realm string // jwt标识
Key []byte // 加密密钥 []byte("my secret key")
Timeout time.Duration // 有效时间
MaxRefresh time.Duration // 可以刷新token的最大时间区间
IdentityKey string // payload中的用户标识
Authenticator func(c *gin.Context) (interface{}, error)
Authorizator func(data interface{}, c *gin.Context) bool
PayloadFunc func(data interface{}) MapClaims
IdentityHandler func(c *gin.Context) interface{}
...
}
|
剩余未注释的字段我们结合下面流程部分讲,先看看这个结构体怎么和gin一起使用。实际上很简单,先用New函数创建结构体,传入也是这个结构体,不过New函数会帮忙进行校验,我们自己包装一个自己的New函数,使用闭包来把用户数据层动态传入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import jwt "github.com/appleboy/gin-jwt/v2"
...
func New(userRepo repo.UserRepo) *jwt.GinJWTMiddleware{
var authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
Realm: "my project",
Key: []byte("my secret"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: IdentityKey,
Authenticator: userAuth(us),
IdentityHandler: identityHandler,
PayloadFunc: payloadFunc,
})
if err != nil {
return authMiddleware
}
}
|
然后在gin加载路由的地方进行初始化并在需要用户先登录的接口启用即可:
1
2
3
4
5
6
7
8
9
10
|
func installController(g *gin.Engine) {
...
authMiddleware := New(userRepo)
g.Post("/login", authMiddleware.LoginHandler) //启用结构体自带的login handler
bookRoutes := g.Group("/books")
{
bookRoutes.Use(authMiddleware.MiddlewareFunc()) //调用MiddlewareFunc来生成gin中间件
// ...后续其他接口
}
}
|
登录流程
结构体中有两个字段在登录时会被调用:GinJWTMiddleware.Authenticator
和GinJWTMiddleware.PayloadFunc
。
登录的请求首先会进入Authenticator
函数,这个函数负责从gin.Context
中提取登录信息,并进行校验,最后返回用map或者struct封装的用户信息,这个数据会自动被作为参数传入PayloadFunc
。
这是一个Authenticator
的例子,同样使用闭包来传入用户服务层,我们在创建结构体时调用userAuth(us)
来生成具体的Authenticator
函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
type UserInfo struct {
Username string
Userid int
Role string
}
func userAuth(us service.UserService) func(*gin.Context) (interface{}, error) {
return func(c *gin.Context) (interface{}, error) {
var l Login
if err := c.ShouldBindJSON(&l); err != nil {
return nil, jwt.ErrMissingLoginValues
}
u, err := us.Login(c, l.Username, l.Password)
if err != nil {
return nil, jwt.ErrFailedAuthentication
}
return UserInfo{
u.Username,
int(u.ID),
u.Role
}, nil
}
}
|
用户信息登录成功后,在默认情况下,gin-jwt不会往jwt的payload中添加额外的信息,如userid等,而只会有token签发时间和过期时间:
如想要在payload中加入用户的唯一信息,让后续的请求可以携带,则需要定义PayloadFunc
函数,假如结构体中有这个函数,那么gin-jwt会在调用完Authenticator
之后,把它的输出作为参数传入此函数,而此函数则负责创建想要附带进payload所需要的对象mapClaims
,它是一个map[string]interface{}
,在生成token时会被自动填入payload中。
这是一个简单的PayloadFunc
的例子:
1
2
3
4
5
6
7
8
|
func payloadFunc(data interface{}) jwt.MapClaims {
if v, ok := data.(UserInfo); ok {
return jwt.MapClaims{
IdentityKey: v.Userid,
}
}
return jwt.MapClaims{}
}
|
值得注意的是,mapClaims
中应有一个与结构体的IdentityKey
值一样的字段,里面存放用户的标识,这在后续的token验证中的默认情况下,也就是没有声明IdentityHandler
,会被gin-jwt提取并存放到gin.Context中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// gin-jwt/auth_jwt.go
func (mw *GinJWTMiddleware) MiddlewareInit() error {
...
if mw.IdentityHandler == nil {
mw.IdentityHandler = func(c *gin.Context) interface{} {
claims := ExtractClaims(c)
return claims[mw.IdentityKey]
}
}
...
}
func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) {
...
c.Set("JWT_PAYLOAD", claims)
identity := mw.IdentityHandler(c)
if identity != nil {
c.Set(mw.IdentityKey, identity)
}
...
}
|
通过实现这两个函数,我们便可以验证用户的登录信息,并把对应的用户id嵌入返回的jwt中。接下来便是如何验证token,并提取用户id的部分。
token验证流程
在登录后的后续请求中,客户端应当在请求头附上jwt:
1
|
Authorization: Bearer ${jwt}
|
在gin-jwt中间件处理带有jwt的请求时,gin-jwt便会把jwt的信息写入gin.Context
中,此时我们可以自定义IdentityHandler
,从context中抽取之前写入jwt的完整信息,从上面的源码可知,默认情况下gin-jwt只会抽取IdentityKey
字段进行返回。
在此函数返回后,gin-jwt会把返回值,也就是用户信息,作为参数传入Authorizor
函数进行用户鉴权。在这个函数中,我们同样应使用闭包来动态传入权限数据层的对象并进行调用,道理同上,然后假如鉴权通过,则gin-jwt的中间件处理完成,请求发给下一个中间件或者接口函数接着处理。
由于Authorizor
是gin-jwt的最后一个函数,我们在这里最好把整个用户信息写入gin.Context
,让后续需要用到用户信息的模块或函数不依赖gin-jwt,同时暴露一个get函数来获取这些信息:
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
|
func userAuthz(authRepo interface{}) func(interface{}, *gin.Context) bool {
return func(data interface{}, ctx *gin.Context) bool {
// do authorization here
if !authRepo.Authz(data) {
return false
}
// extract user info into context
if v, ok := data.(map[string]interface{}); ok {
ctx.Set(IdentityKey, v)
return true
}
return false
}
}
func GetUserInfo(ctx *gin.Context) *UserInfo {
v, ok := ctx.Get(IdentityKey)
if ok {
return &UserInfo{
Username: v["Username"],
Userid: v["Userid"],
Role: v["Role"],
}
}
return &UserInfo{}
}
|
总结
认证用户登录并签发jwt时:
Authenticator
从ctx中获取账号密码认证,然后把用户信息传给PayloadFunc
PayloadFunc
处理用户信息,生成mapClaims
以嵌入签发的jwt
校验jwt时:
- gin-jwt负责主要的校验过程
- 可以自定义
IdentityHandler
来获取jwt中嵌入的用户信息,gin-jwt会把返回值传给Authorizor
Authorizor
中进行用户的鉴权,在此函数中还可以进行对gin-jwt的解耦,把用户信息写入gin.Context
中