go-zero进行RPC调用、错误处理、JWT鉴权| 青训营

前言

上一篇文章介绍了如何配置一个go-zero + gorm的开发框架,并演示创建和查询用户。本文介绍如何创建go-zero API项目,调用编写的RPC服务提供注册和登录功能,并且实现JWT鉴权

定义数据结构

.api文件是go-zero自创的文件格式,和protobuf的语法有很大不同,但好在不难理解。具体语法介绍可看官网文档:go-zero.dev/docs/tasks/...

这里我打算先编写接口定义,然后直接一次性使用goctl生成代码。api文件支持将一个数据结构嵌入另一个结构中,便于编写统一的响应格式。

首先定义空请求结构和基础的响应结构:

go 复制代码
syntax = "v1"  
  
type Empty {  
}  
  
type BasicResponse {  
    StatusCode int32 `json:"status_code"`  
    StatusMsg string `json:"status_msg"`  
}

type关键字也支持代码块,只需要用type()包裹,就可以省去关键词。下面定义了注册和登录的请求和响应结构。

go 复制代码
type (  
    RegisterRequest struct {  
        Username string `form:"username"`  
        Password string `form:"password"`  
    }  

    RegisterResponse struct {  
        BasicResponse  
        UserId int64 `json:"user_id"`  
        Token string `json:"token"`  
    }  
)  
  
type (  
    LoginRequest struct {  
        Username string `form:"username"`  
        Password string `form:"password"`  
    }  

    LoginResponse struct {  
        BasicResponse  
        UserId int64 `json:"user_id"`  
        Token string `json:"token"`  
    }  
)

最后还要定义一个获取用户信息的接口。因为本文不涉及创作、社交方面的业务逻辑实现,所以User结构其实没什么可以返回的信息。之后将其嵌入响应类型的user字段中:

go 复制代码
type (  
    User {  
        Id int64 `json:"id"`  
        Name string `json:"name"`  
    }  
    
    GetUserInfoRequest struct {  
        UserId int64 `form:"user_id"`  
        Token string `form:"token"`  
    }  

    GetUserInfoResponse struct {  
        BasicResponse  
        User User `json:"user"`  
    }  
)

定义接口路由

先不考虑鉴权问题,把所有api路由配置好,这里的请求路径参考了青训营给出的要求:

less 复制代码
@server(  
    group: app  
)  
service app {  
    @handler Ping  
    get /ping (Empty) returns (BasicResponse)  
}  
  
@server(  
    group: user  
    prefix: /douyin/user  
)  
service app {  
    @handler Register  
    post /register (RegisterRequest) returns (RegisterResponse)  
  
    @handler Login  
    post /login (LoginRequest) returns (LoginResponse)  
  
    @handler GetUserInfo  
    get / (GetUserInfoRequest) returns (GetUserInfoResponse)  
}

这段配置有以下几个要点:

  1. 如果service代码块有多个,要求后面的名称(即"app")相同
  2. 形如装饰器的@server代码块可以做代码分组、路由前缀、鉴权、中间件等多种配置。
    • 这里把路由分成app和user两组,之后生成时会分组放在文件夹里
    • 给user组设置了/douyin/user的路由器前缀,就不用重复写了
    • 鉴权和中间件配置下文会提到
  3. 每个接口需要一个handler,其实就是给函数起名
  4. 接口定义的格式是 <HTTP方法> <子路径> (<请求数据结构>) returns (<响应数据结构>)

创建项目模板

这次不是新建示例项目,而是根据现有的api声明创建,所以要运行:

shell 复制代码
goctl api go --api app.api --dir=. --style=goZero 

这会在当前目录生成项目文件。之后进入目录安装依赖:

shell 复制代码
go mod tidy

测试Ping接口

Ping接口的逻辑在internal/logic/app/pingLogic.go,打开文件并编辑handler函数:

go 复制代码
func (l *PingLogic) Ping(req *types.Empty) (resp *types.BasicResponse, err error) {  
    return &types.BasicResponse{
        StatusCode: 0,
        StatusMsg:  "pong",
    }, nil
}

这里的写法和rpc微服务有点区别。注意到传入的resp是一个指针,所以需要手动创建一个响应结构体并设置键值。

这样就算完成Ping接口了,可以运行服务,在浏览器里访问这个地址来测试:

配置RPC客户端连接

扩展config数据结构

internal/config/config.go增加一行UserRPC配置项,这里要引用zrpc.RpcClientConf这个数据类型

go 复制代码
package config  
  
import (  
    "github.com/zeromicro/go-zero/rest"  
    "github.com/zeromicro/go-zero/zrpc"  
)  
  
type Config struct {  
    rest.RestConf  
    UserRpc zrpc.RpcClientConf  
}

添加节点配置

etc/app.yaml增加UserRpc的地址配置:

yaml 复制代码
Name: app  
Host: 0.0.0.0  
Port: 8888  
  
UserRpc:  
    Endpoints:  
        - 127.0.0.1:8080  
    NonBlock: true

虽然现在只有本地测试的一个节点,但是配置的时候我使用的是多节点直连模式,便于以后扩展节点数量。相关文档在此:go-zero.dev/docs/tutori...

这里的NonBlock配置,官方文档解释不清楚,我从一份学习笔记中找到了解释:

rpc的client的NonBlock选项默认为false,必须等依赖的rpc启动后自己才能启动,设置为true后可以不等依赖启动

oublie 《golang-go-zero-个人笔记》 oublie6.github.io/posts/golan...

修改go.mod

在使用代码生成编写RPC服务时,会一并生成调用结构对应的RpcClient,为了使用它,需要在本项目里调用RPC项目的userClient包。

目前,我的两个项目放在同一个文件夹里,需要在go.mod中增加以下两条配置,首先引用包,然后声明引用的相对路径

java 复制代码
module app  
  
go 1.20  
  
// ...
require user v0.0.0  
replace user => ../user
//...

修改ServiceContext

修改ServiceContext的定义,添加UserRpc一项。因为刚刚引用了本地包,所以这里需要import "user/userClient"

在初始化函数中创建客户端连接并返回得到的客户端,全部代码如下:

go 复制代码
package svc  
  
import (  
    "app/internal/config"  
    "user/userClient" 
    "github.com/zeromicro/go-zero/zrpc"  
)  
  
type ServiceContext struct {  
    Config config.Config  
    UserRpc userClient.User  
}  
  
func NewServiceContext(c config.Config) *ServiceContext {  
    return &ServiceContext{  
        Config: c,  
        UserRpc: userClient.NewUser(zrpc.MustNewClient(c.UserRpc)),  
    }  
}

这样就完成了RPC客户端的配置和连接。

配置请求错误处理

定义公共错误类型

服务过程中可能遇到错误的用户请求,或者发生未知的内部错误,因此定义ApiErrorServerError两个错误类型。这部分内容是公用的,所以创建common/common.go来存放。

go 复制代码
package common  
  
import (  
    "app/internal/types"  
    "fmt"  
    "regexp"  
)

type ApiError struct {  
    StatusCode int  
    Code int32  
    Message string  
}  
  
type ServerError struct {  
    ApiError  
    Detail error  
}  
  
func (e ApiError) Error() string {  
    return fmt.Sprintf("(%d) %s", e.Code, e.Message)  
}  
  
func (e ApiError) Response() *types.BasicResponse {  
    return &types.BasicResponse{  
        StatusCode: e.Code,  
        StatusMsg: e.Message,  
    }  
}

Go规定所有的的Error类型都要实现Error()接口,输出一段错误文本。

但是API在报错的时候应该向用户端返回结构化的错误信息,因此增加一个Response方法,将错误信息填入BasicResponse中返回

ServerError会额外携带原本的error结构体,用于跟踪错误

自定义错误处理函数

打开app.go,在handler.RegisterHandlers(server, ctx)后新增一行httpx.SetErrorHandler(errHandler),之后编写错误处理函数:

go 复制代码
func errHandler(err error) (int, interface{}) {  
    switch e := err.(type) {  
    case common.ApiError:  
        return e.StatusCode, e.Response()  
    case common.ServerError:  
        fmt.Printf("%s: %s\n", e, e.Detail)  
        return e.StatusCode, e.Response()  
    default:  
        fmt.Printf("Internal Server Error: %s\n", e)  
        return http.StatusInternalServerError, &types.BasicResponse{  
            StatusCode: 50000,  
            StatusMsg: "Internal Server Error",  
        }  
    }  
}

这里用到了Go的类型断言功能,根据进入的case分支不同,变量e会被断言到对应的数据结构上。

  • 如果判断是ApiError,则返回自定义的状态码和响应即可
  • 如果是ServerError,则额外打印一下错误信息后返回
  • 如果是未知错误,则打印错误后返回标准的http.StatusInternalServerError和默认信息

完善RPC错误处理

暴露公共错误

这里需要修改上文编写的RPC组件,将一些标准化的错误暴露成公共变量。回到RPC微服务的user文件夹 ,创建common/errors.go,将之前Logic文件中用status.Error创建的错误移动到这里:

go 复制代码
package common  
  
import (  
    "google.golang.org/grpc/codes"  
    "google.golang.org/grpc/status"  
)  
  
var (  
    ErrUserAlreadyExists = status.New(codes.AlreadyExists, "user already exists")  
    ErrUserNotFound = status.New(codes.NotFound, "user not found")  
)

注意 ,这里使用的是status.New而非status.Error

gRPC status和error的关系

gRPC提供了gRPC status和error互相转换的方法。调用status.Status上的Err()方法,会把他转换成一个error

查看源码会发现status.Error()函数就是先调用New()然后立即链式调用Err()转换成error

而想要把error转换回status时,则要调用FromError(err error) (s *Status, ok bool)这个函数。可以查看文档得知其用法pkg.go.dev/google.gola...

在API侧,为了判断RPC服务错误的类型,需要将调用返回的err转换成status,然后调用Code()或者Messgae()方法获取原来的信息。

因此为了少做一步转换,这里的公共错误保留了status.Status类型。与此同时 ,这个类型还支持使用WithDetials()函数附加自定义的错误信息,这个用法马上会讲到。

改写原来的错误处理

这里以createLogic.go为例,详细介绍一下修改的方法。

已知错误类型处理

首先,用common.ErrUserAlreadyExists.Err()替换掉原来当场新建的错误

go 复制代码
if count > 0 {  
    return nil, common.ErrUserAlreadyExists.Err()  
}

未知错误类型处理

对于未知的错误,没有办法给出固定的错误类型。但是这里要更正一下之前的错误status.Error()接收的虽然是一个int32的code,但是实际上gRPC团队对此有专门的定义,并不是随意设定的 。具体定义可见这里pkg.go.dev/google.gola...

gRPC的codes包中预置了这些数值,因此将位置错误都改写为status.Error(codes.Internal, err.Error())

go 复制代码
if err != nil {  
    return nil, status.Error(codes.Internal, err.Error())  
}

附加额外的错误信息

gRPC提供的status类型承载信息很有限,不能传递出确切的业务情况。

谷歌团队为此预留了一个Details字段,用于传递自定义的信息。golang版本的gRPC库刚好有相关实现,因此我们可以使用WithDetails函数来传入自定义的信息。

err = l.svcCtx.DB.Create(newUser).Error这句为例,我既想知道错误是在创建用户时发生的,又想知道错误的具体情况,因此先使用status.New(codes.Internal, "error creating user record")创建一个标准status,之后再添加详细信息:

go 复制代码
err = l.svcCtx.DB.Create(newUser).Error  
if err != nil {  
    st, _ := status.New(codes.Internal, "error creating user record").WithDetails(  
    &any.Any{  
        Value: []byte(err.Error()),  
    })  
    return nil, st.Err()  
}

这里使用了一个any.Any类型的结构体,内部类型是anypb.Anyany包来源于github.com/golang/protobuf/ptypes/any,IDE可能无法自动补全导入,需要手动设置一下。

这个包有一个Value字段,用于存储用户自定义的信息。这里我就简单得获得错误信息转为[]bytes后存入

WithDetails支持传递多个Any结构体,只需要将其作为二、三、四个参数输入即可,客户端在收到后需要遍历获取错误,马上会提到。

完善注册函数

现在回到API服务的app文件夹。这里以用户注册为例,介绍如何完成字段校验、RPC调用、错误处理、结果返回。

打开registerLogic.go文件,先完成基础的逻辑,要对输入的用户名和密码使用正则表达式判断合规性

使用正则表达式校验字段

go 复制代码
func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.RegisterResponse, err error) {  
    username := req.Username  
    password := req.Password  

    usernamePattern := "^[^\\s]{1,20}$"  
    passwordPattern := "^[!-~]{8,24}$"  

    if !utils.MatchRegexp(usernamePattern, username) || !utils.MatchRegexp(passwordPattern, password) {  
        return nil, utils.ApiError{  
            StatusCode: 422,  
            Code: 42201,  
            Message: "Invalid username or password",  
        }  
}

这里编写了一个工具函数MatchRegexp,判断给定的字符串是否符合给定的正则表达式,位于common/common.go

go 复制代码
func MatchRegexp(pattern string, value string) bool {  
    r := regexp.MustCompile(pattern)  
    return r.MatchString(value)  
}

调用微服务并处理错误

调用微服务

微服务的调用比较简单,因为有客户端预定义的方法,只要传入参数即可。请求传回的res是之前使用protobuf定义的CreateResponse类型。

go 复制代码
res, err := l.svcCtx.UserRpc.Create(l.ctx, &user.CreateRequest{  
    Username: username,  
    Password: password,  
})

编写错误判断函数

接下来重点处理错误。这个接口在调用时可能返回用户已经存在 的错误,因此要检查返回错误是否是这一类型。这里先介绍错误判断函数 MatchError(error, *status.Status)

这个函数接收userClient返回的错误,以及之前在user/common里定义的标准status,返回由err解析出的status,以及错误是否吻合。

go 复制代码
func MatchError(err error, target *status.Status) (*status.Status, bool) {  
    st, _ := status.FromError(err)  
    if st.Message() == target.Message() {  
        return st, true  
    }  
    return st, false  
}

注意 ,这里是不能用errors.Is或者==直接进行比较的,因为在网络传输过程中,RPC服务端和客户端的错误不在同一个内存地址,比较一定会返回false,所以这里只能用Message()或者Code()做比较。

处理错误

之后用使用这个函数来区分ErrUserAlreadyExists和未知错误。如果是用户已经存在,则返回正常的ApiError通知用户;否则,使用Details()函数获取所有的详细信息并打印出来,最后返回内部服务器错误并附上这个err

注意 ,这里给user/common包起了一个别名userCommon,否则会和app/common冲突。应该考虑将两个包合并起来,统一使用。

这里也用到了类型断言 ,因为go认为Details()返回的是[]interface{},需要显式转为*anypb.Any否则内部字段未知。

go 复制代码
if err != nil {  
    if st, match := common.MatchError(err, userCommon.ErrUserAlreadyExists); match {  
        return nil, common.ApiError{  
            StatusCode: 422,  
            Code: 42201,  
            Message: "用户名已被使用",  
        }  
    } else {  
        for index, item := range st.Details() {  
            detail := item.(*anypb.Any)  
            fmt.Printf("%d: %s\n", index, string(detail.Value))  
        }  

        return nil, common.ServerError{  
            ApiError: common.ApiError{  
                StatusCode: 500,  
                Code: 50000,  
                Message: "Internal Server Error",  
            },  
            Detail: err,  
        }  
    }  
}

返回注册成功的消息

业务逻辑的最后就是返回注册消息了,这里的UserId字段由res携带,取出并赋值即可。Token字段我们还未实现,因此留在这里占位:

go 复制代码
return &types.RegisterResponse{  
    BasicResponse: types.BasicResponse{  
        StatusCode: 0,  
        StatusMsg: "",  
    },  
    UserId: res.UserId,  
    Token: "not implemented",  
}, nil

加入JWT Token发放

下载软件包

软件的Github仓库地址在github.com/golang-jwt/...

现在已经推荐使用v5版本,于是安装:

shell 复制代码
go get -u github.com/golang-jwt/jwt/v5

定义token结构体

创建intermal/utils/jwt.go,我们将在里面编写JWT发放和验证的相关内容。

JWT token支持自定义载荷,可以携带自己想要的数据。这里我们只需携带UserId即可,因此定义一个UserClaims结构体,并用jwt.RegisteredClaims扩展出jwt的基础字段

go 复制代码
package utils  
  
import (  
    "github.com/golang-jwt/jwt/v5"  
)  
  
type UserClaims struct {  
    UserId int64 `json:"user_id"`  
    jwt.RegisteredClaims  
}

注意jwt.StandardClaims在v5版本中已经没有了,如果使用v4版本的jwt会提示该类型被弃用 。因此本文使用推荐的jwt.RegisteredClaims来替代

编写发放函数

CreateToken函数用于在登录和注册的时候发放token。他根据给定的UserId,签名密钥和有效时长生成token并签名。通常用当前时间作为签发和生效时间,而过期时间需要即时相加计算一下。

这部分操作与官网的教程几乎一致,可以参考pkg.go.dev/github.com/...

注意 ,不要忘记给duration乘上time.Second,否则发出来的token会当场过期

go 复制代码
func CreateToken(userId int64, secret string, expire int64) (string, error) {  
    userClaims := UserClaims{  
        UserId: userId,  
        RegisteredClaims: jwt.RegisteredClaims{  
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second)),  
            IssuedAt: jwt.NewNumericDate(time.Now()),  
            NotBefore: jwt.NewNumericDate(time.Now()),  
            Issuer: "app-api",  
        },  
    }  
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims)  
    signed, err := token.SignedString([]byte(secret))  
    return signed, err  
}

编写验证函数

ParseToken要做的事情比较简单,只要尝试反序列化Token即可。这里的错误处理和一般情况有所不同,是因为token有可能和err一起返回(比如token过期),但是仍然可以尝试获取携带的信息。因此,如果token不为nil,则尝试解析余下信息。最后返回userClaimserr,如果err == nil则认为解析成功

go 复制代码
func ParseToken(tokenString string, secret string) (*UserClaims, error) {  
    token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { 
        return []byte(secret), nil  
    })  
    if token == nil {  
        return nil, err  
    }  

    userClaims, ok := token.Claims.(*UserClaims)  
    if !ok {  
        return nil, err  
    }  
    return userClaims, err  
}

增加配置项

修改internal/config/config.go,增加JwtAuth项:

go 复制代码
type Config struct {  
    rest.RestConf  
    UserRpc zrpc.RpcClientConf  
    JwtAuth struct {  
        Secret string  
        Expire int64  
    }  
}

修改etc/app.yaml,添加配置项:

yaml 复制代码
JwtAuth:  
    Secret: ?xX7nYmfM<(4r%p  
    Expire: 1209600 # 14天

添加生成逻辑

调用CreateToken很简单,如果遇到err,就以ServerError返回:

go 复制代码
tokenString, err := utils.CreateToken(res.UserId, l.svcCtx.Config.JwtAuth.Secret, l.svcCtx.Config.JwtAuth.Expire)  
    if err != nil {  
    return nil, common.ServerError{  
        ApiError: common.ApiError{  
            StatusCode: 500,  
            Code: 50000,  
            Message: "Internal Server Error",  
        },  
        Detail: err,  
    }  
}

最后只要把tokenString放入请求响应就好。

测试注册函数

可以使用postman,apifox等调试工具发送请求,得到请求响应:

这样一来就完成了一个完整的注册函数。

完善登陆函数

这里仅贴出登录函数的实现,不作具体讲解:

go 复制代码
func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {  
    username := req.Username  
    password := req.Password  

    res, err := l.svcCtx.UserRpc.QueryByName(l.ctx, &user.QueryByNameRequest{  
        Username: username,  
    })  
    if err != nil {  
        if st, match := common.MatchError(err, userCommon.ErrUserNotFound); match {  
            return nil, common.ApiError{  
                StatusCode: 422,  
                Code: 42202,  
                Message: "用户名不存在",  
            }  
        } else {  
            for index, item := range st.Details() {  
                detail := item.(*anypb.Any)  
                fmt.Printf("%d: %s\n", index, string(detail.Value))  
            }  

            return nil, common.ServerError{  
                ApiError: common.ApiError{  
                    StatusCode: 500,  
                    Code: 50000,  
                    Message: "Internal Server Error",  
                },  
                Detail: err,  
                }  
            }  
        }  

    err = bcrypt.CompareHashAndPassword(res.Password, []byte(password))  
    if err != nil {  
        return nil, common.ApiError{  
            StatusCode: 422,  
            Code: 42203,  
            Message: "密码错误",  
        }  
    }  

    tokenString, err := utils.CreateToken(res.UserId, l.svcCtx.Config.JwtAuth.Secret, l.svcCtx.Config.JwtAuth.Expire)  
    if err != nil {  
        return nil, common.ServerError{  
            ApiError: common.ApiError{  
                StatusCode: 500,  
                Code: 50000,  
                Message: "Internal Server Error",  
            },  
        Detail: err,  
        }  
    }  

    return &types.LoginResponse{  
        BasicResponse: types.BasicResponse{  
            StatusCode: 0,  
            StatusMsg: "",  
        },  
        UserId: res.UserId,  
        Token: tokenString,  
    }, nil  
}

编写身份认证中间件

实际上,go-zero支持直接开启JWT令牌验证,并且会把解析到的键值放入ctx中,可以通过l.ctx.Value()获取,详见go-zero.dev/docs/tutori...

但是,青训营使用的API并不是标准的JWT规范,它将token字段用query参数的方式发给后端。因此,我们只能自己编写中间件。

修改API定义

获取用户信息的接口需要JWT鉴权。go-zero支持分组绑定中间件,因此把之前定义的GetUserInfo接口单独放在一个service代码块中,并且加入middleware : JwtAuth。冒号后面是给中间件起的名字

less 复制代码
@server(  
    group : user  
    prefix: /douyin/user  
    middleware: JwtAuth  
)  
service app {  
    @handler GetUserInfo  
    get / (GetUserInfoRequest) returns (GetUserInfoResponse)  
}

配置中间件

再次执行goctl api go --api app.api --dir=. --style=goZero,可以看到新增internal/middleware/jwtauthMiddleware.go。在开始编写逻辑之前还需要做一些准备工作。

配置serviceContext

internal/svc/serviceContext.go增加一项JwtAuth字段,使用模板生成的NewJwtAuthMiddleware产生中间件,传入config配置并获取其Handle函数。

go 复制代码
type ServiceContext struct {  
    Config config.Config  
    UserRpc userClient.User  
    JwtAuth rest.Middleware  
}  
  
func NewServiceContext(c config.Config) *ServiceContext {  
    return &ServiceContext{  
        Config: c,  
        UserRpc: userClient.NewUser(zrpc.MustNewClient(c.UserRpc)),  
        JwtAuth: middleware.NewJwtAuthMiddleware(c).Handle,  
    }  
}

修改中间件配置

转到internal/middleware/jwtauthMiddleware.go,需要接收传入的config配置并且将其添加到中间件的上下文:

go 复制代码
type JwtAuthMiddleware struct {  
    Config config.Config  
}  
  
func NewJwtAuthMiddleware(c config.Config) *JwtAuthMiddleware {  
    return &JwtAuthMiddleware{  
        Config: c,  
    }  
}

编写验证逻辑

中间件的逻辑是:

  1. 先尝试从URL获取query参数token,如果其不存在,则返回401错误
  2. 中间件尝试解析token。
    • 如果返回err,并且err的原因是token过期,则返回友好提示;余下err返回token无效提示。
    • 如果没有返回err,则说明token有效,不做处理
  3. 代码运行到最后,则放行请求

这里使用httpx.WriteJson来提前返回响应。函数接收一个http.ResponseWriter,HTTP状态码,以及一个interface类型的返回值。

判断token解析错误类型时要使用errors.Is,因为JWT这个库给错误外包装了一层错误,直接比较的话不会相等。想了解相关知识可以去搜索golang的错误链,Wrap以及Unwrap。

go 复制代码
func (m *JwtAuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {  
return func(w http.ResponseWriter, r *http.Request) {  

    token := r.URL.Query().Get("token")  

    if token == "" {  
        httpx.WriteJson(w, http.StatusUnauthorized, &types.BasicResponse{  
            StatusCode: 40101,  
            StatusMsg: "没有提供token",  
        })  
        return  
    }  
    _, err := utils.ParseToken(token, m.Config.JwtAuth.Secret)  
    if err != nil {  
        if errors.Is(err, jwt.ErrTokenExpired) {  
            httpx.WriteJson(w, http.StatusUnauthorized, &types.BasicResponse{  
                StatusCode: 40102,  
                StatusMsg: "token已过期",  
            })  
        } else {  
            httpx.WriteJson(w, http.StatusUnauthorized, &types.BasicResponse{  
                StatusCode: 40103,  
                StatusMsg: "token无效",  
            })  
        }  
        return  
    }  
    next(w, r)  
    }  
}

编写GetUserInfo函数

因为本文不会实现关注判断等业务逻辑,并且默认用户可以查看其他用户的信息,因此这个函数只需要调用一下RPC接口即可。其实这里的RPC错误处理重复了好几次有点繁琐,有改进空间。

go 复制代码
func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoRequest) (resp *types.GetUserInfoResponse, err error) {  
    queryId := req.UserId  
    res, err := l.svcCtx.UserRpc.QueryById(l.ctx, &user.QueryByIdRequest{  
        UserId: queryId,  
    })  
    if err != nil {  
        if st, match := common.MatchError(err, userCommon.ErrUserNotFound); match {  
            return nil, common.ApiError{  
                StatusCode: 422,  
                Code: 42202,  
                Message: "用户名不存在",  
            }  
        } else {  
            for index, item := range st.Details() {  
                detail := item.(*anypb.Any)  
                fmt.Printf("%d: %s\n", index, string(detail.Value))  
            }  

            return nil, common.ServerError{  
                ApiError: common.ApiError{  
                    StatusCode: 500,  
                    Code: 50000,  
                    Message: "Internal Server Error",  
                },  
                Detail: err,  
            }  
        }  
    }  
    return &types.GetUserInfoResponse{  
        BasicResponse: types.BasicResponse{  
            StatusCode: 200,  
            StatusMsg: "",  
        },  
        User: types.User{  
            Id: res.UserId,  
            Name: res.Username,  
        },  
    }, nil  
}

查看中间件效果

依然是使用API调试软件,请求成功时将会正常返回信息:

查找不存在的用户会被MatchError函数捕获,并返回ApiError.Response()

token留空、过期会显示对应错误:

删改token使其失效,可以得到无效的提示:

结语

自此,我们已经完成了字段校验、RPC调用、错误处理、结果返回等任务,并搭建起一套用JWT实现用户登录、注册、获取信息的系统。

go-zero的http服务不如gin容易上手,因此配置serviceContext、中间件、路由的时候需要额外做一系列事情

我认为错误处理是编码过程中最大的难点,RPC status和error的类型转换、额外信息传递非常陌生,导致实现过程中代码的复用性比较差,判断逻辑也比较复杂,这方面的设计可以更加好些。

相关推荐
CallBack8 个月前
Typora+PicGo+阿里云OSS搭建个人图床,纵享丝滑!
前端·青训营笔记
Taonce1 年前
站在Android开发者的角度认识MQTT - 源码篇
android·青训营笔记
AB_IN1 年前
打开抖音会发生什么 | 青训营
青训营笔记
monster1231 年前
结营感受(go) | 青训营
青训营笔记
翼同学1 年前
实践记录:使用Bcrypt进行密码安全性保护和验证 | 青训营
青训营笔记
hu1hu_1 年前
Git 的正确使用姿势与最佳实践(1) | 青训营
青训营笔记
星曈1 年前
详解前端框架中的设计模式 | 青训营
青训营笔记
tuxiaobei1 年前
文件上传漏洞 Upload-lab 实践(中)| 青训营
青训营笔记
yibao1 年前
高质量编程与性能调优实战 | 青训营
青训营笔记
小金先生SG1 年前
阿里云对象存储OSS使用| 青训营
青训营笔记