这是我参与「第五届青训营」笔记创作活动的第3天
大项目基础架构图
今天对api部分每个项目文件做了具体的完善,明确了api部分的技术架构,以下作说明记录:
1. main.go
// Code generated by hertz generator.
package main
import (
"api/biz/mw"
"api/biz/rpc"
"github.com/cloudwego/hertz/pkg/app/server"
)
func Init() {
rpc.Init()
mw.InitJWT()
}
func main() {
Init()
h := server.New(
server.WithHostPorts(":8080"),
server.WithHandleMethodNotAllowed(true), // coordinate with NoMethod
)
register(h)
h.Spin()
}
main.go中,首先对rpc和middleware做了初始化,然后new了一个httpserver,供前端调用。
2. rpc/user.go
package rpc
import (
"api/biz/consts"
"api/biz/errno"
"api/kitex_gen/user"
"api/kitex_gen/user/userservice"
"context"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/rpcinfo"
etcd "github.com/kitex-contrib/registry-etcd"
)
var userClient userservice.Client
func initUser() {
r, err := etcd.NewEtcdResolver([]string{consts.ETCDAddress})
if err != nil {
panic(err)
}
c, err := userservice.NewClient(
consts.UserServiceName,
client.WithResolver(r),
client.WithMuxConnection(1),
// client.WithMiddleware(mw.CommonMiddleware),
// client.WithInstanceMW(mw.ClientMiddleware),
// client.WithSuite(tracing.NewClientSuite()),
client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: consts.ApiServiceName}),
)
if err != nil {
panic(err)
}
userClient = c
}
// CreateUser create user info
func CreateUser(ctx context.Context, req *user.CreateUserRequest) error {
resp, err := userClient.CreateUser(ctx, req)
if err != nil {
return err
}
if resp.BaseResp.StatusCode != 0 {
return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return nil
}
// CheckUser check user info
func CheckUser(ctx context.Context, req *user.CheckUserRequest) (int64, error) {
resp, err := userClient.CheckUser(ctx, req)
if err != nil {
return 0, err
}
if resp.BaseResp.StatusCode != 0 {
return 0, errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return resp.UserId, nil
}
rpc/user.go文件中,声明并初始化了一个rpc客户端userservice.Client。提供了两个调用rpc服务器的函数CreateUser和CheckUser,是对用户注册和登录功能实现的辅助函数。二者都将api部分接收到的req中的用户名和密码进行封装,再传到对应的rpc服务端。
3. handler/api/api_service.go
// Code generated by hertz generator.
package api
import (
"context"
"api/biz/errno"
api "api/biz/model/api"
"api/biz/mw"
"api/biz/rpc"
"api/kitex_gen/user"
"github.com/cloudwego/hertz/pkg/app"
)
// CreateUser .
// @router /douyin/user/register [POST]
func CreateUser(ctx context.Context, c *app.RequestContext) {
var err error
var req api.CreateUserRequest
err = c.BindAndValidate(&req)
if err != nil {
SendResponse(c, errno.ConvertErr(err), nil)
return
}
err = rpc.CreateUser(context.Background(), &user.CreateUserRequest{
Username: req.Username,
Password: req.Password,
})
if err != nil {
SendResponse(c, errno.ConvertErr(err), nil)
return
}
SendResponse(c, errno.Success, nil)
}
// CheckUser .
// @router /douyin/user/login [POST]
func CheckUser(ctx context.Context, c *app.RequestContext) {
mw.JwtMiddleware.LoginHandler(ctx, c)
}
该文件对前端传来的username和password信息做绑定和验证,并调用上文中提到的rpc包中相应的函数,对用户的注册和登录请求做相应的处理。
4. mw/jwt.go
package mw
import (
"api/biz/consts"
"api/biz/errno"
"api/biz/model/api"
"api/biz/rpc"
"api/kitex_gen/user"
"context"
"encoding/json"
"net/http"
"time"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/common/utils"
jwtv4 "github.com/golang-jwt/jwt/v4"
"github.com/hertz-contrib/jwt"
)
var JwtMiddleware *jwt.HertzJWTMiddleware
func InitJWT() {
JwtMiddleware, _ = jwt.New(&jwt.HertzJWTMiddleware{
Key: []byte(consts.SecretKey),
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: consts.IdentityKey,
IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
claims := jwt.ExtractClaims(ctx, c)
userid, _ := claims[consts.IdentityKey].(json.Number).Int64()
return &api.User{
UserID: userid,
}
},
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(int64); ok {
return jwt.MapClaims{
consts.IdentityKey: v,
}
}
return jwt.MapClaims{}
},
Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
var err error
var req api.CheckUserRequest
if err = c.BindAndValidate(&req); err != nil {
return "", jwt.ErrMissingLoginValues
}
if len(req.Username) == 0 || len(req.Password) == 0 {
return "", jwt.ErrMissingLoginValues
}
return rpc.CheckUser(context.Background(), &user.CheckUserRequest{
Username: req.Username,
Password: req.Password,
})
},
LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
c.JSON(http.StatusOK, utils.H{
"code": errno.Success.ErrCode,
"token": token,
"expire": expire.Format(time.RFC3339),
})
},
Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
c.JSON(http.StatusOK, utils.H{
"code": errno.AuthorizationFailedErr.ErrCode,
"message": message,
})
},
HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
switch t := e.(type) {
case errno.ErrNo:
return t.ErrMsg
default:
return t.Error()
}
},
ParseOptions: []jwtv4.ParserOption{jwtv4.WithJSONNumber()},
})
}
该文件声明了一个jwt.HertzJWTMiddleware中间件,并设置了一些响应函数。当前端传入请求时,对用户token进行校验,或为登录用户分发token。这块知识尚不明晰,还需要仔细研读代码。
5. router/api/middleware.go
// Code generated by hertz generator.
package Api
import (
"api/biz/errno"
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
"github.com/cloudwego/hertz/pkg/common/hlog"
"github.com/cloudwego/hertz/pkg/common/utils"
"github.com/cloudwego/hertz/pkg/protocol/consts"
"github.com/hertz-contrib/gzip"
"github.com/hertz-contrib/requestid"
"go.opentelemetry.io/otel/trace"
)
func rootMw() []app.HandlerFunc {
// your code...
return []app.HandlerFunc{
// use recovery mw
recovery.Recovery(recovery.WithRecoveryHandler(
func(ctx context.Context, c *app.RequestContext, err interface{}, stack []byte) {
hlog.SystemLogger().CtxErrorf(ctx, "[Recovery] err=%v\nstack=%s", err, stack)
c.JSON(consts.StatusInternalServerError, utils.H{
"code": errno.ServiceErr.ErrCode,
"message": fmt.Sprintf("[Recovery] err=%v\nstack=%s", err, stack),
})
},
)),
// use requestid mw
requestid.New(
requestid.WithGenerator(func(ctx context.Context, c *app.RequestContext) string {
traceID := trace.SpanFromContext(ctx).SpanContext().TraceID().String()
return traceID
}),
),
// use gzip mw
gzip.Gzip(gzip.DefaultCompression),
}
}
func _douyinMw() []app.HandlerFunc {
// your code...
return nil
}
func _userMw() []app.HandlerFunc {
// your code...
return nil
}
func _checkuserMw() []app.HandlerFunc {
// your code...
return nil
}
func _createuserMw() []app.HandlerFunc {
// your code...
return nil
}
该文件在rootMw函数中添加了3个middleware,分别是recovery, requestid, gzip。recovery用户在etcd注册中心发现需要的rpc服务。requestid提供每次请求的id,方便查看日志文件。gzip是Hertz启用gzip服务的中间件。
小结
通过对api部分的深入剖析,基本明确了每个文件的用途和调用结构。但中间件部分尚不清晰,需要再阅读相关资料学习。