上一节知道了gin-vue-admin是如何进行login登录并生成token给到前端的。本节就来看看后端是如何验证token是否合规的。对于验证token,就是在后端调用处理api方法之前,统一会进行验证的地方。在gin里是通过中间件形式进行的。可以打开middleware/jwt.go文件,即为jwt中间件。
1.代码示例
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := c.Request.Header.Get("x-token")
if token == "" {
response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c)
c.Abort()
return
}
if jwtService.IsBlacklist(token) {
response.FailWithDetailed(gin.H{"reload": true}, "您的帐户异地登陆或令牌失效", c)
c.Abort()
return
}
j := utils.NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
if err == utils.TokenExpired {
response.FailWithDetailed(gin.H{"reload": true}, "授权已过期", c)
c.Abort()
return
}
response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
c.Abort()
return
}
if claims.ExpiresAt-time.Now().Unix() < claims.BufferTime {
claims.ExpiresAt = time.Now().Unix() + global.GVA_CONFIG.JWT.ExpiresTime
newToken, _ := j.CreateTokenByOldToken(token, *claims)
newClaims, _ := j.ParseToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt, 10))
if global.GVA_CONFIG.System.UseMultipoint {
RedisJwtToken, err := jwtService.GetRedisJWT(newClaims.Username)
if err != nil {
global.GVA_LOG.Error("get redis jwt failed", zap.Error(err))
} else { // 当之前的取成功时才进行拉黑操作
_ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: RedisJwtToken})
}
// 无论如何都要记录当前的活跃状态
_ = jwtService.SetRedisJWT(newToken, newClaims.Username)
}
}
c.Set("claims", claims)
c.Next()
}
}
可以看出,编写中间件,只要就是编写一个gin.HandlerFunc
类型的方法,在方法中会通过获取请求头中的x-token
即为本次请求所发送的token。若token为空或者再黑名单中,就返回失败。然后就是对token进行解码,验证token是否符合规定。若符合规定,然后判断token有效时间是否小于缓冲时间,若小于缓冲时间,则重新生成token并返回给前端。最后,将用户的信息通过key=claims
来放到context中。
可以通过打断点的方式来查看claims里边具体是哪些信息:
后边的api逻辑处理部分就可以通过c.Get(“claims”)来获取到对应的用户信息了。
2.执行原理
我们可以继续在这个中间件上打断点,重新启动项目,然后通过前端请求来让后端执行逻辑走到断点处,此时我们看执行堆栈:
查看堆栈命名可以看出,handleHTTPRequest
方法是开始处理请求的。 因此可以先跳到这里查看代码。
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
}
我删掉了多余处理其他逻辑的代码,只保留了主逻辑部分。方法内的前两行主要是获取当前请求的方法和路由。然后for循环内只要是找到对应的方法树。对于方法树,具体可以查看engine.trees
变量:
一共有五棵树,相当于本项目只用到了Http中的5种方法。然后对于当前请求是哪种方法,就去找出对应的树。再去该树中找到对应的节点后,将handles和fullPath赋值给context对象。只有context对象执行了Next方法,我们可以点到下一层堆栈来看下具体。
// go/pkg/mod/github.com/gin-gonic/gin@v1.7.0/context.go
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
虽然这里的代码很短,但是涵盖了整个中间件执行的过程。首先对于c.handlers
正是在上一个堆栈中,通过方法树中的对应路由的节点所配置的handles复制给c.handlers.我们可以查看当前c.handlers具体是哪些内容:
可以看出,一共5个handler,只有最后一个是实际的程序逻辑。在每个handler处理完成或者显示调用c.Next()后,就会继续触发下一个handler。
我们接下来可以看下对于这5个handler都做了哪些工作
首先第一个是gin.LoggerWithConfig.func1
,具体代码如下:
func(c *Context) {
// Start timer
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// Process request
c.Next()
// Log only when path is not being skipped
if _, ok := skip[path]; !ok {
param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
fmt.Fprint(out, formatter(param))
}
由之前阅读的代码可知,c.Next()
可以触发执行下一个handler,因此可以以此为分界点。c.Next() 的上下部分是对执行c.Next()进行耗时统计。所以整体上就是对当前请求接口处理耗时进行统计。
第二个是CustomRecoveryWithWriter.func1
,具体代码如下:
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
if logger != nil {
stack := stack(3)
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe {
logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), headersToStr, err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}
if brokenPipe {
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
} else {
handle(c, err)
}
}
}()
c.Next()
}
可以看到第二行写了个defer
代表是异常后释放资源的逻辑。所以这个中间件整体上就是对请求出异常情况下,执行的资源释放等操作。通过具体阅读代码可知,里边主要是对出异常后,收集请求等信息,并打印日志。
第三个是JWT了,之前有详细讲述,这里就不在赘述了
第四个是CasbinHandler
这个里边调用了Casbin
库,一个用于权限管理的库,为了判别当前用户是否有权限执行此api
第五个就是我们具体业务处理的handler了。
在gin
框架中,对于第一和第二个是默认有的,后边的jwt和权限管理,则是项目中自己添加的中间件.
PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())