用户信息是一个敏感的接口,它必须要保证用户登录之后才能访问。同样的,用户编辑单词库相关的接口也需要权限认证。我们不可能在每个需要认证的接口前都编写同样的代码用以权限认证。所以要编写一个中间件/拦截器来统一验证Token
是否有效。
中间件/拦截器是处理
HTTP
请求和响应的函数或组件。它们通常用于在请求到达最终处理器之前或响应发送给客户端之前执行一些操作。
Auth 中间件
GoFrame
提供了优雅的中间件请求控制方式, 该方式也是主流的 WebServer
提供的请求流程控制方式, 基于中间件设计可以为 WebServer
提供更灵活强大的插件机制。
internal/logic/middleware/auth.go
package middleware
import (
"net/http"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/golang-jwt/jwt/v5"
"star/utility"
)
func Auth(r *ghttp.Request) {
var (
jwtKey = utility.JwtKey
tokenString = r.Header.Get("Authorization")
)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil || !token.Valid {
r.Response.WriteStatus(http.StatusForbidden)
r.Exit()
}
r.Middleware.Next()
}
r.Middleware.Next()
这段代码是中间件的控制核心,放在函数的最后面表示它是一个前置中间件,代表请求前调用。放在最前面则是后置中间件,在请求后生效。
r.Header.Get("Authorization")
从HTTP Header
中获取Authorization
字段,即获取前端传过来的Token
。jwt.Parse
解析Token
后再通过token.Valid
验证是否有效,如果失效则返回 HTTP StatusForbidden 403
状态码,即权限不足,反之调用r.Middleware.Next()
进入下一步。
编写好的中间件留用,等接口开发完成后统一注册到cmd
中。接下来又是愉快的三板斧法则。
添加Api
api/account/v1/account.go
package v1
import (
"github.com/gogf/gf/v2/frame/g"
)
type InfoReq struct {
g.Meta `path:"account/info" method:"get" sm:"获取信息" tags:"用户"`
}
type InfoRes struct {
Username string `json:"username" dc:"用户名"`
Email string `json:"email" dc:"邮箱"`
CreatedAt *gtime.Time `json:"created" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"update" dc:"更新时间"`
}
InfoRes
结构体定义了四个响应数据,其中*gtime.Time
数据类型是gtime
组件提供的框架时间类型。
编写Logic
internal/logic/users/account.go
package users
import (
"context"
"errors"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/golang-jwt/jwt/v5"
"star/internal/dao"
"star/internal/model/entity"
"star/utility"
)
...
func Info(ctx context.Context) (user *entity.Users, err error) {
user = new(entity.Users)
tokenString := g.RequestFromCtx(ctx).Request.Header.Get("Authorization")
tokenClaims, _ := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return utility.JwtKey, nil
})
if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid {
err = dao.Users.Ctx(ctx).Where("id", claims.Id).Scan(&user)
}
return
}
在Logic
中不能直接获取HTTP
对象,需要使用g.RequestFromCtx(ctx).Request
从上下文中获取。获取Token
后解析出用户ID
,调用Scan
方法将查询结果赋值给entity.Users
结构体。
Scan
方法是一个非常强大的方法,它会根据给定的参数类型自动识别并转换,是数据查询操作中的常客。
Controller调用Logic
internal/controller/account/account_v1_info.go
package account
import (
"context"
"star/api/account/v1"
"star/internal/logic/users"
)
func (c *ControllerV1) Info(ctx context.Context, req *v1.InfoReq) (res *v1.InfoRes, err error) {
user, err := users.Info(ctx)
if err != nil {
return nil, err
}
return &v1.InfoRes{
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}, nil
return
}
注册新的控制器
使用group.Group
新增一个路由分组,并使用group.Middleware
注册Auth
中间件,凡是在本组下的控制器在访问前都需要经过认证。
internal/cmd/cmd.go
package cmd
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
"star/internal/controller/account"
"star/internal/controller/users"
"star/internal/logic/middleware"
)
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Group("/v1", func(group *ghttp.RouterGroup) {
group.Bind(
users.NewV1(),
)
group.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Auth)
group.Bind(
account.NewV1(),
)
})
})
})
s.Run()
return nil
},
}
)
接口测试
注意把Authorization
换成自己的:
$ curl -H "Authorization: eyJhbGci...W6Ed_d3P77Mc" http://127.0.0.1:8000/v1/account/info
{
"code":0,
"message":"",
"data":{
"username":"oldme",
"email":"tyyn1022@gmail.com",
"created":"2024-11-08 17:02:16",
"update":"2024-11-08 17:02:16"
}
}