go-zero教程——User rpc - Login

5,637 阅读7分钟

User rpc创建

cdFoodGuides 目录下。创建 rpc 文件夹

mkdir -p usermanage/rpc/user && cd usermanage/rpc/user

rpc/user 目录下编写user.proto文件

goctl rpc template -o user.proto

编写 user.proto 文件

syntax = "proto3";

package user;

message LoginRequest {
  string email = 1;
  string password = 2;
}

message RegisterRequest {
  string Username = 1;
  string Email = 2;
  string Password = 3;
}

message UserinfoRequest {
  string Userid = 1;
  string Token = 2;
}

message Response {
  int64 id = 1;
  string email = 2;
  string accessToken = 3;
  int64 accessExpire = 4;
  int64 refreshAfter = 5;
}

service User {
  rpc Login(LoginRequest) returns(Response);
  rpc Register(RegisterRequest) returns(Response);
  rpc Userinfo(UserinfoRequest) returns(Response);
}

我们定义了三个接口 :Login Register UserInfo

生成 user-rpc 服务

goctl rpc proto -src user.proto -dir .

查看一下 rpc/user 目录

➜  user git:(master) ✗ tree
.
├── etc
│   └── user.yaml
├── internal
│   ├── config
│   │   └── config.go
│   ├── logic
│   │   ├── loginlogic.go
│   │   ├── registerlogic.go
│   │   └── userinfologic.go
│   ├── server
│   │   └── userserver.go
│   └── svc
│       └── servicecontext.go
├── user
│   └── user.pb.go
├── user.go
├── user.proto
└── userclient
    └── user.go

8 directories, 11 files
➜  user git:(master) ✗

API Gateway 代码调用 user rpc 服务

编辑 api/etc 下的 user-api.yaml 文件,新增 user.rpc 配置

Name: user-api
Host: 0.0.0.0
Port: 8888

User:
  Etcd:
    Hosts:
      - localhost:2379
    Key: user.rpc

编辑 api/internal/config 下的 config.go 文件,新增 User 变量

type Config struct {
	rest.RestConf
	User zrpc.RpcClientConf
}

编辑 api/internal/svc 下的 servicecontext.go 文件,新增 User 变量 ,新增实例化代码。

type ServiceContext struct {
	Config config.Config
	User  userclient.User
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		User: userclient.NewUser(zrpc.MustNewClient(c.User)),
	}
}

编辑 api/internal/logic 下的 loginlogic.go 文件,新增 调用 user rpclogin 方法

func (l *LoginLogic) Login(req types.LoginRequest) (*types.LoginResponse, error) {
	// todo: add your logic here and delete this line
	resp,err := l.svcCtx.User.Login(l.ctx, &user.LoginRequest{
		Email: req.Email,
		Password: req.Password,
	});
	if err != nil {
		return nil, err
	}

	token := types.JwtToken{
		AccessToken: resp.AccessToken,
		AccessExpire: resp.AccessExpire,
		RefreshAfter: resp.RefreshAfter,
	}

	response := types.UserReply{
		Id: resp.Id,
		Email: resp.Email,
		JwtToken: token,
	}

	return &types.LoginResponse{
		response,
	},nil
}

定义数据库表结构,并生成CRUD+cache代码

usermanage 下创建 model 文件夹。

mkdir -p model & cd model

model 下新建 user.sql 文件并编写如下内容。

CREATE TABLE `user` (
                        `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户Id',
                        `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名称',
                        `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码',
                        `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
                        `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
                        `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                        PRIMARY KEY (`id`),
                        UNIQUE KEY `name_index` (`name`),
                        UNIQUE KEY `email_index` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

model 目录下执行如下命令生成 CRUD+cache 代码,-c 表示使用 redis cache

goctl model mysql ddl -c -src user.sql -dir .

查看 model 目录结构

➜  model git:(master) ✗ tree
.
├── user.sql
├── usermodel.go // CRUD+cache代码
└── vars.go      // 定义常量和变量

在本机 mysql 中创建 foodguides 数据库,并新建 user 表。

新增一条数据

INSERT INTO `foodguides`.`user`(`id`, `name`, `password`, `email`) VALUES (1, 'Ningxi', 'd89617870c6f8a028f5728be69cc09d4cd3585b4651b7f206f1cd674bb4351ec', 'ningxi@ningxi.com');

rpc 代码调用 crud+cache 代码

编辑 rpc/user/etc 下的 user.yaml 文件,新增如下内容。

注意这里的 mysql 使用的是 ningxi-compose 跑的 docker 容器,因此参数需要注意。

DataSource: root:2e70F5E6@(localhost:13306)/foodguides?parseTime=true
Table: user
Cache:
  - Host: localhost:16379
  
AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
AccessExpire: 86400
Salt: ^&yh

编辑 rpc/user/internal/config 下的 config.go 文件,新增 DataSource Cache AccessSecret AccessExpire 变量

type Config struct {
	zrpc.RpcServerConf
	DataSource string
	Cache cache.CacheConf
    AccessSecret string
	AccessExpire int64
    Salt string
}

编辑 rpc/user/internal/svc 下的 serviceContext.go 文件,新增 Model 变量 ,新增实例化代码。

type ServiceContext struct {
	Config config.Config
	Model model.UserModel
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		Model: model.NewUserModel(sqlx.NewMysql(c.DataSource),c.Cache),
	}
}

编辑 rpc/user/internal/logic 下的 loginlogic.go 文件, 新增如下代码

func (l *LoginLogic) Login(in *user.LoginRequest) (*user.Response, error) {
	res, err := l.svcCtx.Model.FindOneByEmail(in.Email)
	if err == nil {
		passwords := ningxi.PasswordEncrypt(l.svcCtx.Config.Salt,in.Password)
		if passwords == res.Password {
			now := time.Now().Unix()
			accessExpire := l.svcCtx.Config.AccessExpire
			jwtToken, err := l.getJwtToken(l.svcCtx.Config.AccessSecret, now, accessExpire, res.Id)
			if err != nil {
				return nil, err
			}
			response := user.Response{
				Email: res.Email,
				Id: res.Id,
				AccessToken: jwtToken,
				AccessExpire: now + accessExpire,
				RefreshAfter: now + accessExpire/2,
			}
			return &response, nil
		} else {
			return nil, errors.New("密码错误")
		}

	}
	return nil, err
}

我在 loginlogic 中增加了 token 生成、密码加密验证的相关代码,这样当用户登录成功时,我们将为用户生成一个 token 返回给客户端。

修改 api response 返回格式

我希望客户端接口请求数据的返回格式是这样子的

{
    "code": 1,
    "message": "",
    "result": {
        "id": 1,
        "username": "",
        "email": "ningxi@ningxi.com",
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE0NzAzOTQsImlhdCI6MTYxMTM4Mzk5NCwidXNlcklkIjoxfQ.8EJU0XDZ535NZvtCPgyOg9RVw3FAdG5AJktHYcjEGo0",
        "accessExpire": 1611470394,
        "refreshAfter": 1611427194
    }
}

foodguides 文件夹下 新增 ningxi 文件夹,并创建 ningxi.go 文件,新增如下代码

import (
	"fmt"
	"golang.org/x/crypto/scrypt"
)

type HttpResponse struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Result interface{} `json:"result"`
}

func SuccessResponse(resData interface{},message string) HttpResponse {
	return HttpResponse{Code:1,Message: message,Result: resData}
}

func FailureResponse(resData interface{},message string,code int) HttpResponse {
	return HttpResponse{Code:code,Message: message,Result: resData}
}

func PasswordEncrypt(salt,password string) string {
	dk,_ := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32)
	return fmt.Sprintf("%x",string(dk))
}

编辑 api/internal/handler 下的loginhandler.go 新增如下代码

func LoginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.LoginRequest
		if err := httpx.Parse(r, &req); err != nil {
			httpx.OkJson(w, ningxi.FailureResponse(nil,err.Error(),1000))
			return
		}

		l := logic.NewLoginLogic(r.Context(), ctx)
		resp, err := l.Login(req)
		if err != nil {
			httpx.OkJson(w, ningxi.FailureResponse(nil,err.Error(),1000))
		} else {
			httpx.OkJson(w, ningxi.SuccessResponse(resp,""))
		}
	}
}

启动服务

启动服务,注意 在启动服务前,需要确保 上一篇文章用到的 ningxi-compose 正常运行起来。

启动 user rpc 服务, 运行成功后,user rpc 则运行在本机的 8080 端口

➜  FoodGuides git:(master) ✗ go run usermanage/rpc/user/user.go -f usermanage/rpc/user/etc/user.yaml
Starting rpc server at 127.0.0.1:8080...

启动 user api 服务, 运行成功后,user api 则运行在本机的 8888 端口

➜  FoodGuides git:(master) ✗ go run usermanage/api/user.go -f usermanage/api/etc/user-api.yaml
Starting server at 0.0.0.0:8888...

api 测试 得到如下数据则说明 服务运行正常

➜  ~ curl http://localhost:8888/users/login -X POST -d '{"email": "ningxi@ningxi.com","password": "809161"}' --header "Content-Type: application/json"
{"code":1,"message":"","result":{"id":1,"username":"","email":"ningxi@ningxi.com","accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE0OTU3MjAsImlhdCI6MTYxMTQwOTMyMCwidXNlcklkIjoxfQ.EN9og9owK5eW--qUIJCf0UvMKNaeenVac1lmMQFcHSM","accessExpire":1611495720,"refreshAfter":1611452520}}%
➜  ~

理解服务是怎么跑起来的

老样子还是要理解一下服务的执行流程

  • user/etc 下的 user.yaml 文件。该文件配置了 user rpc 服务所需的一些变量,这个与 api 服务是一样的。
  • user 下的 user.proto 文件。该文件定义了 user rpc 服务所提供的接口信息,之后的接口增加同样是在这里处理。然后 调用 goctl 重新生成服务。
  • user 下的 user.go 文件。该文件是 user rpc 服务的入口文件,一切都是从这里开始。
  • user/user 下的 user.pb.go 文件。该文件是 user.proto 生成 rpc 服务文件,里面包含了 rpcclientserver 的实现。

userclient 文件夹

userclient 下的 user.go文件是 rpc 服务 中 client 的具体实现代码,该文件实现了 user.pb.go 中的 三个接口

User interface {
	Login(ctx context.Context, in *LoginRequest) (*Response, error)
	Register(ctx context.Context, in *RegisterRequest) (*Response, error)
	Userinfo(ctx context.Context, in *UserinfoRequest) (*Response, error)
}

api 服务中的 serviceContext.go 中 初始化 userclient 时,api 服务就具备了调用这三个接口的能力

type ServiceContext struct {
	Config config.Config
	User  userclient.User
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		User: userclient.NewUser(zrpc.MustNewClient(c.User)),
	}
}

internal 文件夹

rpc 服务中的内部实现代码都放在了该文件夹下面。

internal/config 下的 config.go文件。你会发现,该文件的定义和 user.yaml 的定义类似。是的。user.yamluser.go 入口文件在 main 方法里,就被解析成了 config 对象。所以他们的值是一一对应的。

internal/logic 下的 xxxlogic.go 文件,这里是最终业务逻辑实现的地方。

internal/svc 下的 servicecontext.go 文件。该文件保存了 rpc 服务的 config 对象,数据库连接对象 Model。通过 UserServer 对象 传递到 logic 对象中。

internal/sercer 下的 userserver.go 文件。该文件是 user rpc 服务中 server 的 具体实现代码,userserver 保存了 svc.ServiceContext 对象,并且实现了 三个接口

func (s *UserServer) Login(ctx context.Context, in *user.LoginRequest) (*user.Response, error) {
	l := logic.NewLoginLogic(ctx, s.svcCtx)
	return l.Login(in)
}

func (s *UserServer) Register(ctx context.Context, in *user.RegisterRequest) (*user.Response, error) {
	l := logic.NewRegisterLogic(ctx, s.svcCtx)
	return l.Register(in)
}

func (s *UserServer) Userinfo(ctx context.Context, in *user.UserinfoRequest) (*user.Response, error) {
	l := logic.NewUserinfoLogic(ctx, s.svcCtx)
	return l.Userinfo(in)
}

api 服务通过 userclient 调用 rpc 服务时,userserver 将触发相对应的方法,并最终调用相对应的 logic 方法。

调用过程梳理

以客户端调用 login 接口为例。

client 部分

api 服务 在启动时,初始化了 userclient 对象,于是 api 服务具备了调用 user rpc 服务的能力

type ServiceContext struct {
	Config config.Config
	User  userclient.User
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		User: userclient.NewUser(zrpc.MustNewClient(c.User)),
	}
}

server 部分

user.go 入口文件 通过 yaml 配置文件,实例化 config 对象。

var configFile = flag.String("f", "etc/user.yaml", "the config file")

func main() {
	flag.Parse()

	var c config.Config
	conf.MustLoad(*configFile, &c)
}

实例化 ServiceContext 对象

ctx := svc.NewServiceContext(c)

实例化 UserServer 对象

srv := server.NewUserServer(ctx)

实例化 rpc 服务

s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
	user.RegisterUserServer(grpcServer, srv)
})

user.RegisterUserServer(grpcServer, srv) 里做了类似 api 服务中路由实现的功能。

启动 rpc 服务

s.Start()

api 服务通过 client 调用 login 方式时,rpc server 将触发 userserve 中 的 login 方法。

func (s *UserServer) Login(ctx context.Context, in *user.LoginRequest) (*user.Response, error) {
	l := logic.NewLoginLogic(ctx, s.svcCtx)
	return l.Login(in)
}

login 方法中调用 logic 中 的 login 方法

处理完数据后,接口逐层响应回去,最终完成客户端接口的调用。

本片内容相对较长,其中关于 JwtTokenrpcclientserver 等相关知识,大家可以自行查阅资料能更好理解教程中的内容。

上一篇《go-zero教程——User API Gateway》

下一篇《go-zero教程——User rpc - Register》