1 本文简化了整体环节过程,只对重难点问题进行详细讲解,建议结合本文与官方文档
。
2 在使用时发现,goctl.exe v1.4.3
生成的xxxhandler.go
:
if err != nil {httpx.ErrorCtx(r.Context(), w, err)} else {httpx.OkJsonCtx(r.Context(), w, resp)}
而 goctl.exe v1.4.2
生成的xxxhandler.go
:
if err != nil {httpx.Error(w, err)} else {httpx.OkJson(w, resp)}
这个会对返回的信息格式产生影响【即4.8和4.9】,所以暂不推荐将goctl.exe
升级到 v1.4.3
。
※参考1:go-zero超时时间
代码:https://gitee.com/XiMuQi/go-zero-micro/tree/v1.0.1
go-zero微服务项目的超时时间有三处配置,具体的配置看代码。
关于2、3的区别:
已经有了在api
中注册发现 rpc
中的timeout
,这里会不会显得多余?作者感觉开发者这样的设计更加双向灵活。从rpc
角度我可以设置为0(即:永不过期),但是我api请求必须保证有一个超时时间节点。这样,我们也可以统一开发规范,将rpc
统一设置为0,api
层面在注册发现时则根据实际要求更改。
参考1:进阶指南
目录拆分是根据业务横向拆分,将一个系统拆分成多个子系统,每个子系统应拥有独立的持久化存储,缓存系统。 如一个商城系统需要有用户系统(user),商品管理系统(product),订单系统(order),购物车系统(cart),结算中心系统(pay),售后系统(afterSale)等组成。
每个系统在对外(api
)提供服务的同时,也会提供数据给其他子系统进行数据访问的接口(rpc
),因此每个子系统可以拆分成两个服务:api
和rpc
。除此之外,一个服务下还可能有其他更多服务类型,如rmq
(消息处理系统),cron
(定时任务系统),script
(脚本)等。
可以将每个服务的公共部分抽出来放在一起,比如错误的封装,sql的model等。
完整工程目录结构示例
book // 工程名称
├── common // 通用库
│ ├── randx
│ └── stringx
├── go.mod
├── go.sum
└── service // 服务存放目录├── afterSale│ ├── api│ └── model│ └── rpc├── cart│ ├── api│ └── model│ └── rpc├── order│ ├── api│ └── model│ └── rpc├── pay│ ├── api│ └── model│ └── rpc├── product│ ├── api│ └── model│ └── rpc└── user├── api├── cronjob├── model├── rmq├── rpc└── script
参考1:model生成
参考1:api文件编写
参考1:业务编码
参考1:jwt鉴权
jwt:全称 json web token。
1 签发时需要配置鉴权的密钥,过期时间,以及实现签发鉴权的逻辑即可。
2 客户端在发送请求时,如果在header
中加入了jwt token
,go-zero
从jwt token
解析后会将生成token
时传入的key-value
原封不动的放在http.Request
的Context
中,因此我们可以通过Context
拿到jwt token
中传递的值。
3 鉴权的签发一般是在用户登录成功
后,每个后续请求将包括JWT
,从而允许用户访问该令牌允许的路由,服务和资源。
jwt 配置流程
Auth:AccessSecret: $AccessSecretAccessExpire: $AccessExpire
$AccessSecret
:生成jwt token的密钥,最简单的方式可以使用一个uuid值。
$AccessExpire
:jwt token有效期,单位:秒
package configimport ("github.com/zeromicro/go-zero/core/stores/cache""github.com/zeromicro/go-zero/rest"
)type Config struct {rest.RestConfMysql struct {DataSource string}CacheRedis cache.CacheConfAuth struct {AccessSecret stringAccessExpire int64}
}
jwt
中还可以额外放入别的信息,比如userId
、userName
等等,可根据情况添加。package logicimport ("book/common/errorx""book/service/user/sql/model""context""github.com/golang-jwt/jwt/v4""strings""time""book/service/user/api/internal/svc""book/service/user/api/internal/types""github.com/zeromicro/go-zero/core/logx"
)type LoginLogic struct {logx.Loggerctx context.ContextsvcCtx *svc.ServiceContext
}func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {return &LoginLogic{Logger: logx.WithContext(ctx),ctx: ctx,svcCtx: svcCtx,}
}func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginReply, error) {if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {return nil, errorx.NewDefaultError(errorx.AccountErrorCode)}userInfo, err := l.svcCtx.UserModel.FindOneByNumber(l.ctx,req.Username)switch err {case nil:case model.ErrNotFound:return nil, errorx.NewDefaultError(errorx.UserIdErrorCode)default:return nil, err}if userInfo.Password != req.Password {return nil, errorx.NewDefaultError(errorx.PasswordErrorCode)}now := time.Now().Unix()accessExpire := l.svcCtx.Config.Auth.AccessExpirejwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)if err != nil {return nil, err}return &types.LoginReply{Id: userInfo.Id,Name: userInfo.Name,Gender: userInfo.Gender,AccessToken: jwtToken,AccessExpire: now + accessExpire,RefreshAfter: now + accessExpire/2,}, nil
}func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {claims := make(jwt.MapClaims)claims["exp"] = iat + secondsclaims["iat"] = iatclaims["userId"] = userIdtoken := jwt.New(jwt.SigningMethodHS256)token.Claims = claimsreturn token.SignedString([]byte(secretKey))
}
参考1:search api使用jwt token鉴权
type (SearchReq {// 图书名称Name string `form:"name"`}SearchReply {Name string `json:"name"`Count int `json:"count"`}
)@server(jwt: Auth
)
service search-api {@handler searchget /search/do (SearchReq) returns (SearchReply)
}//不需要jwt鉴权的路由
service search-api {@handler pingget /search/ping
}
注意:不需要jwt
鉴权的路由可以应用在浏览/下载多媒体文件的请求中。
jwt: Auth:开启jwt鉴权
如果路由需要jwt鉴权,则需要在service上方声明此语法标志,如上文中的 /search/do
不需要jwt鉴权的路由就无需声明,如上文中/search/ping
客户端在登陆成功后,服务端生成并返回了jwt
给客户端,客户端在后续请求时需要在header
中加入jwt token
,go-zero
从jwt token
解析后会将生成token
时传入的key-value
原封不动的放在http.Request
的Context
中,因此我们可以通过Context
拿到jwt token
中传递的值。
前提是要在xxx.api
中加入jwt
的拦截配置
syntax = "v1"info(title: "type title here"desc: "type desc here"author: "type author here"email: "type email here"version: "type version here"
)type (SearchReq {// 图书名称Name string `form:"name"`}SearchReply {Name string `json:"name"`Count int `json:"count"`}
)@server(jwt: Auth
)
service search-api {@handler searchget /search/do (SearchReq) returns (SearchReply)
}service search-api {@handler pingget /search/ping
}
示例:
在xxxlogic.go
中添加一个log
来输出从jwt
解析出来的userId
。
func (l *SearchLogic) Search(req *types.SearchReq) (*types.SearchReply, error) {logx.Infof("userId: %v",l.ctx.Value("userId"))// 这里的key和生成jwt token时传入的key一致return &types.SearchReply{}, nil
}
参考1:中间件使用
中间件分类
在go-zero
中,中间件可以分为路由【局部】中间件
和全局中间件
,路由【局部】中间件
是指某一些特定路由需要实现中间件逻辑,其和jwt
类似,没有放在jwt:xxx
下的路由不会使用中间件功能, 而全局中间件的服务范围则是整个服务。
局部中间件
和全局中间件
的讲解看参考1即可。
在中间件里调用其它服务
以调用Redis服务为例:
package middlewareimport ("dsms-admin/api/internal/common/errorx""encoding/json""fmt""github.com/zeromicro/go-zero/core/logx""github.com/zeromicro/go-zero/core/stores/redis""github.com/zeromicro/go-zero/rest/httpx""net/http""strings"
)type CheckUrlMiddleware struct {Redis *redis.Redis
}func NewCheckUrlMiddleware(Redis *redis.Redis) *CheckUrlMiddleware {return &CheckUrlMiddleware{Redis: Redis}
}func (m *CheckUrlMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {//判断请求header中是否携带了x-user-iduserId := r.Context().Value("userId").(json.Number).String()if userId == "" {logx.Errorf("缺少必要参数x-user-id")httpx.Error(w, errorx.NewDefaultError("缺少必要参数x-user-id"))return}if r.RequestURI == "/api/sys/user/currentUser" || r.RequestURI == "/api/sys/user/selectAllData" || r.RequestURI == "/api/sys/role/queryMenuByRoleId" {logx.Infof("用户userId: %s,访问: %s路径", userId, r.RequestURI)next(w, r)} else {//获取用户能访问的urlurls, err := m.Redis.Get(userId)if err != nil {logx.Errorf("用户:%s,获取redis连接异常", userId)httpx.Error(w, errorx.NewDefaultError(fmt.Sprintf("用户:%s,获取redis连接异常", userId)))return}if len(strings.TrimSpace(urls)) == 0 {logx.Errorf("用户userId: %s,还没有登录", userId)httpx.Error(w, errorx.NewDefaultError(fmt.Sprintf("用户userId: %s,还没有登录,请先登录", userId)))return}backUrls := strings.Split(urls, ",")b := falsefor _, url := range backUrls {if url == r.RequestURI {b = truebreak}}if true || b { //todo deletelogx.Infof("用户userId: %s,访问: %s路径", userId, r.RequestURI)next(w, r)} else {logx.Errorf("用户userId: %s,没有访问: %s路径的权限", userId, r.RequestURI)httpx.Error(w, errorx.NewDefaultError(fmt.Sprintf("用户userId: %s,没有访问: %s,路径的的权限,请联系管理员", userId, r.RequestURI)))return}}}
}
参考1:rpc编写与调用
参考1:错误处理
错误处理是统一封装全局的错误返回信息。
自定义错误返回信息
common
中添加一个baseerror.go
文件,并填入代码package errorxconst defaultCode = 1001type CodeError struct {Code int `json:"code"`Msg string `json:"msg"`
}type CodeErrorResponse struct {Code int `json:"code"`Msg string `json:"msg"`
}func NewCodeError(code int, msg string) error {return &CodeError{Code: code, Msg: msg}
}//func NewDefaultError(msg string) error {
// return NewCodeError(defaultCode, msg)
//}func NewDefaultError(code int) error {return NewCodeError(code, MapErrMsg(code))
}func (e *CodeError) Error() string {return e.Msg
}func (e *CodeError) Data() *CodeErrorResponse {return &CodeErrorResponse{Code: e.Code,Msg: e.Msg,}
}
xxxlogic.go
文件中逻辑错误用CodeError
自定义错误替换if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {return nil, errorx.NewDefaultError("参数错误")}userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username)switch err {case nil:case model.ErrNotFound:return nil, errorx.NewDefaultError("用户名不存在")default:return nil, err}if userInfo.Password != req.Password {return nil, errorx.NewDefaultError("用户密码不正确")}now := time.Now().Unix()accessExpire := l.svcCtx.Config.Auth.AccessExpirejwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)if err != nil {return nil, err}return &types.LoginReply{Id: userInfo.Id,Name: userInfo.Name,Gender: userInfo.Gender,AccessToken: jwtToken,AccessExpire: now + accessExpire,RefreshAfter: now + accessExpire/2,}, nil
func main() {flag.Parse()var c config.Configconf.MustLoad(*configFile, &c)ctx := svc.NewServiceContext(c)server := rest.MustNewServer(c.RestConf)defer server.Stop()handler.RegisterHandlers(server, ctx)// 自定义错误httpx.SetErrorHandler(func(err error) (int, interface{}) {switch e := err.(type) {case *errorx.CodeError:return http.StatusOK, e.Data()default:return http.StatusInternalServerError, nil}})fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)server.Start()
}
参考1:模板修改
参考2:template 指令
模板修改:就是统一返回的格式,不论是正常还是异常的。在4.8错误处理
的基础上进行。这里的改造分两种方式,两种方式主要体现在对err不为nil
的处理上:推荐方式2
。
因为是在上一小节已经加入了错误处理,这里在响应时也要准确的返回错误的Code
信息,而官方文档:修改handler模板 却将返回的Code替换为了 -1
,这显然是不对的。方式1和方式2均是改进后的。
package responseimport ("github.com/zeromicro/go-zero/rest/httpx""net/http"
)type Body struct {Code int `json:"code"`Msg string `json:"msg"`Data interface{} `json:"data,omitempty"`
}func Response(w http.ResponseWriter, resp interface{}, err error) {var body Bodyif err != nil {body.Code = 0body.Msg = err.Error()} else {body.Code = 200body.Msg = "success"body.Data = resp}httpx.OkJson(w, body)
}
xxxhandler.go
文件package handlerimport ("go-zero-micro/common/response""net/http""github.com/zeromicro/go-zero/rest/httpx""go-zero-micro/api/order/internal/logic""go-zero-micro/api/order/internal/svc""go-zero-micro/api/order/internal/types"
)func getOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {var req types.OrderReqif err := httpx.Parse(r, &req); err != nil {httpx.Error(w, err)return}l := logic.NewGetOrderLogic(r.Context(), svcCtx)resp, err := l.GetOrder(&req)if err != nil {httpx.Error(w, err)} else {//httpx.OkJson(w, resp)response.Response(w, resp, err)}}
}
package responseimport ("github.com/zeromicro/go-zero/rest/httpx""net/http"
)type Body struct {Code int `json:"code"`Msg string `json:"msg"`Data interface{} `json:"data,omitempty"`
}func Response(w http.ResponseWriter, resp interface{}, err error) {//var body Body//if err != nil {// body.Code = 0// body.Msg = err.Error()//} else {// body.Code = 200// body.Msg = "success"// body.Data = resp//}//httpx.OkJson(w, body)if err != nil {httpx.Error(w, err)} else {var body Bodybody.Code = 200body.Msg = "success"body.Data = resphttpx.OkJson(w, body)}
}
xxxhandler.go
文件package handlerimport ("go-zero-micro/common/response""net/http""github.com/zeromicro/go-zero/rest/httpx""go-zero-micro/api/order/internal/logic""go-zero-micro/api/order/internal/svc""go-zero-micro/api/order/internal/types"
)func getOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {var req types.OrderReqif err := httpx.Parse(r, &req); err != nil {httpx.Error(w, err)return}l := logic.NewGetOrderLogic(r.Context(), svcCtx)resp, err := l.GetOrder(&req)//if err != nil {// httpx.Error(w, err)//} else {// //httpx.OkJson(w, resp)// response.Response(w, resp, err)//}response.Response(w, resp, err)}
}
参考:Nacos安装使用【Docker】
略,配置有点复杂,非必须。了解配置会修改即可。
上一篇:【node写接口】 通过node 快速搭建一个服务器 小白入门
下一篇:Kylin查询性能优化之使用rowkeys排序列快速读取parquet文件、使用shardby列来裁剪parquet文件