1、系列目录


  • 【kratos入门实战教程】0-商城项目介绍
  • 【kratos入门实战教程】1-kratos项目搭建和开发环境配置
  • 【kratos入门实战教程】2-实现注册登陆业务

2、概览

通过本篇文章,读者将会掌握kratos的一般开发流程,涵盖了从接口定义、自定义配置,到业务逻辑实现,再到数据库存储的一整套流程。注册登陆业务是非常高频常见的业务,几乎所有的系统都有注册和登陆的功能。本文展示的注册登陆业务非常基本、非常简单。因此,只需要用户数据库和采用一些主流的JWT认证策略。

2、开干

2.1、定义接口

在api目录下,新建account/v1目录,然后添加account.proto文件。

kratos的接口统一使用protobuf定义的,无论是走grpc协议还是走http协议。把接口的定义和具体的协议隔离出来,并用统一的定义语言定义接口入参和回参以及其他的信息。kratos提供了http协议的protoc插件,在生成grpc的代码时,也会生成http的代码。定义http接口的方式使用的是google的规范。作为入门级的教程,这里就不再详述了。


然后添加注册和登陆的接口:

syntax = "proto3";package account.v1;import "google/api/annotations.proto";option go_package = "tt-shop/api/account/v1;v1";service Account {rpc Login (LoginRequest) returns (LoginResponse) {option (google.api.http) = {post: "/account/login"body: "*"};}rpc Register (RegisterRequest) returns (RegisterResponse) {option (google.api.http) = {post: "/account/register"body: "*"};}
}message LoginRequest {string phone = 1;string password = 2;
}message LoginResponse {string token = 1;
}message RegisterRequest {string phone = 1;string password = 2;
}message RegisterResponse {
}

最后执行命令(在项目的根目录下):

make api

命令执行完后就能看到目录生成了多个源文件,包括protobuf生成的、grpc生成的和kratos的http插件生成的。

2.2、建立用户数据库

建立用户表:

create table user
(id       int(64) not null auto_increment,username varchar(64)  not null,password varchar(128) not null,phone    varchar(18),nickname varchar(20),PRIMARY KEY (id)
) engine=innodb default charset=utf8mb4;

2.3、添加认证配置

项目使用的是JWT认证方式,加密JWT token需要一些秘钥、过期时间等等。这些额外的信息不会直接硬编码到代码中,而是写到配置文件中,做成可配置的。在这一小细节,读者将会掌握如何在kratos项目中添加配置。
kratos的配置声明是放在internal/conf/conf.proto,在proto文件中添加如下配置:


message Auth {string jwt_secret = 1;google.protobuf.Duration expire_duration = 2;
}

然后Bootstrap的message增加认证配置项:

message Bootstrap {Server server = 1;Data data = 2;Auth auth = 3;
}

然后在项目的目录下执行命令:make config,就能看到如下输出:

❯ make config
protoc --proto_path=./internal \--proto_path=./third_party \--go_out=paths=source_relative:./internal \internal/conf/conf.proto

此时,在定义配置的目录下就能看到新编译出来的pb文件。

然后在configs/config.yaml增加新增的认证配置auth:

server:http:addr: 0.0.0.0:8000timeout: 1sgrpc:addr: 0.0.0.0:9000timeout: 1s
data:database:driver: mysqlsource: root:root@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Localredis:addr: 127.0.0.1:6379read_timeout: 0.2swrite_timeout: 0.2s
# 认证配置
auth:jwt_secret: "secret"expire_duration: 3600s

2.4、实现注册登陆业务

在上一篇教程中介绍过kratos的分层结构,业务在biz层,所以这里先从biz实现(这不是一种规范,读者可以按照自己的习惯来做)。在biz目录新建account.go,存放account业务的源码。

2.4.1、定义用户对象

现阶段使用贫血模型(相对于充血模型而言),因为贫血模型比较直观好理解,适合入门。如果读者比较熟悉充血模型,建议看看番外篇。

type User struct {ID       int64  // 用户IDUsername string // 用户名Password string // 密码Nickname string // 昵称Avatar   string // 头像
}

2.4.2、定义用户数据仓库操作接口

这里定义两个接口,分别执行获取指定id的用户和保存用户信息的操作。这里通过在biz层定义一层接口实现了依赖反转(正常biz需要调data提供的方法来操作用户的数据,依赖反转后,是biz要求data实现哪些接口,biz依赖的是接口,而不是data的实现)。

type UserRepo interface {// FetchByUsername 获取指定用户名的用户的信息,如果用户不存在,则返回 ErrUserNotExist。FetchByUsername(ctx context.Context, username string) (user *User, err error)// Save 保存用户信息并返回用户的id。Save(ctx context.Context, user *User) (id int64, err error)
}

2.4.3、实现注册功能

接下来,我们定义一个account的用例,通过用例对象提供注册的功能。account用例对象依赖了用户数据仓库接口UserRepo,需要使用UserRepo来操作用户数据,例如查询。同时,用户的密码加密也抽取到一独立的加密服务中。因为使用了依赖注入工具,所以这些使用到的依赖都不需要自己去构建初始化,只要作为参数传入构造函数即可(事实上,go并有所谓的构造函数,这里为了方便描述)。
然后,account用例对象提供一个注册的方法,实现基本的注册逻辑。这里只是简单的注册需求,有些公司会有特别的安全需求,例如密码长度和包含的符号类型,读者可自行拓展。

注意了!生成环境严禁数据库存储密码明文!!!!!

以下是具体的代码:

type AccountUseCase struct {authConfig     *conf.AuthencryptService EncryptServiceuserRepo       UserRepologger         *log.Helper
}//NewAccountUseCase 创建一个AccountUseCase,依赖作为参数传入
func NewAccountUseCase(logger log.Logger, authConfig *conf.Bootstrap, userRepo UserRepo, encryptService EncryptService) *AccountUseCase {return &AccountUseCase{encryptService: encryptService,userRepo:       userRepo,logger:         log.NewHelper(logger),authConfig:     authConfig.Auth,}
}
//Register 注册
func (a *AccountUseCase) Register(ctx context.Context, username, pwd string) (err error) {// 校验参数if username == "" || pwd == "" {return fmt.Errorf("注册失败:%w", ErrRegisterParamEmpty)}// 判断用户是否已经注册一次了user, err := a.userRepo.FetchByUsername(ctx, username)if err != nil && !errors.Is(err, ErrUserNotExist) {log.Errorf("注册失败,参数[username: %s,pwd:%s],err:%v", username, pwd, err)return fmt.Errorf("注册失败")}if user != nil {return fmt.Errorf("用户已经存在")}// 加密密码encrypt, err := a.encryptService.Encrypt(ctx, []byte(pwd))if err != nil {log.Errorf("注册失败,参数[username: %s,pwd:%s],err:%v", username, pwd, err)return fmt.Errorf("注册失败")}_, err = a.userRepo.Save(ctx, &User{Username: username,Password: string(encrypt),})if err != nil {return fmt.Errorf("注册失败:%w", err)}return nil
}

2.4.5、实现登陆功能

实现了注册业务后,我们来实现登陆的业务。登陆本质上就是一个获取/发放访问凭证行为。这里我们使用的是JWT,所以接口只需返回token就ok了。(篇幅问题,不设计太多的业务需求,有兴趣的读者可以私信作者一起探讨)

给account用例对象添加处理登陆的方法:

//Login 登录,认证成功返回token,认证失败返回错误
func (a *AccountUseCase) Login(ctx context.Context, username, password string) (token string, err error) {// 校验参数if username == "" || password == "" {return "", fmt.Errorf("登录失败:%w", ErrRegisterParamEmpty)}// 获取用户信息user, err := a.userRepo.FetchByUsername(ctx, username)if err != nil {return "", fmt.Errorf("登录失败:%w", err)}// 校验密码encrypt, err := a.encryptService.Encrypt(ctx, []byte(password))if err != nil {return "", fmt.Errorf("登录失败:%w", err)}if user.Password != string(encrypt) {return "", fmt.Errorf("登录失败:%w", ErrPasswordWrong)}// 生成tokenclaims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.authConfig.GetExpireDuration().AsDuration())), // 设置token的过期时间})token, err = claims.SignedString([]byte(a.authConfig.GetJwtSecret()))if err != nil {a.logger.Errorf("登录失败,生成token失败:%v", err)return "", fmt.Errorf("登录失败")}return token, nil
}

2.4.6、总结

到这里,我们就完成了注册登陆的业务了。读者可能会好奇数据库都没有,怎么算是完成了?因为就业务本身而言,和底层的存储、中间件或者其他服务并没有关系。在kratos的工程结构下,业务是收敛在biz层,并不会和其他层耦合。数据库的部分会在本文的后续内容中展示,读者如果想直接看单元测试,可以直接看本系列的单元测试的文章。这里为了先出效果,让读者有亲身感受,就跳过单元测试了,但是在现实生产环境中,请遵守相应的开发规范进行单元测试。

2.5、实现用户数据仓库

在本小节,我们将会在data层实现前面在biz层定义的用户的数据仓库的接口。

这里再说一下,biz层是最核心的一层,对外层是没有依赖的,但是biz层需要访问数据,直观上需要依赖data层,但实际上并不需要。因为使用了接口,实现了依赖反转,在biz层定义了接口,让data层依赖biz层去实现接口。

在orm框架的选型上,我们使用的是Gorm。对于Gorm不做过多的介绍和入门讲解了,网上已经有很多很好的教程了。
废话就说到了这了,接下来让我们把视线切换到在data目录下,我们新建一个user.go的文件,用来存放用户数据仓库的实现逻辑。在data.go中,我们实现接入mysql的逻辑。
data.go:

package dataimport ("github.com/go-kratos/kratos/v2/log""github.com/google/wire""gorm.io/driver/mysql""gorm.io/gorm""yy-shop/internal/conf"
)// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewUserRepo)// Data .
type Data struct {db *gorm.DB
}// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {db, err := gorm.Open(mysql.Open(c.GetDatabase().GetSource()), &gorm.Config{})if err != nil {return nil, nil, err}cleanup := func() {log.NewHelper(logger).Info("closing the data resources")}return &Data{db: db,}, cleanup, nil
}

user.go:

package dataimport ("context""github.com/go-kratos/kratos/v2/log""gorm.io/gorm""yy-shop/internal/biz"
)type userRepo struct {data  *Datalog   *log.Helpertable *gorm.DB
}func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {return &userRepo{data:  data,log:   log.NewHelper(logger),table: data.db.Table("user"),}
}func (u *userRepo) FetchByUsername(ctx context.Context, username string) (user *biz.User, err error) {user = &biz.User{}u.table.WithContext(ctx).First(user, "username = ?", username)if user.ID == 0 {return nil, biz.ErrUserNotExist}return user, nil
}func (u *userRepo) Save(ctx context.Context, user *biz.User) (id int64, err error) {result := u.table.WithContext(ctx).Create(user)if result.Error != nil {return 0, result.Error}return user.ID, nil
}

注意:被操作的实体是定义在biz层的,但是按照kratos的设计,存储到数据库的实体应该是data层自己定义的,data层应该要做一次ACl,即防腐层处理。不过有时候最优的未必是最好的,这里业务本身并不复杂,编码就没必要搞复杂,这里就一把梭了。

2.6、实现注册登陆接口

kratos默认就支持了HTTP和GRPC两种协议的接口,作为教程,两种接口我们都会实现。接口和协议分别对应的是service和server,这样也比较好理解,接口是一种服务,而具体的协议是一种 “服务器”。server依赖service提供具体协议的服务,这样底层就能复用而无需做两套了。

2.6.1、实现Service

在service目录下新建account.go文件,定义一个账号服务accountService,实现上面使用protobuf定义的Account service。逻辑非常简单,这一层主要是做一个错误的转换。
文件内容如下:

package serviceimport ("context""github.com/go-kratos/kratos/v2/errors""github.com/go-kratos/kratos/v2/log"v1 "yy-shop/api/account/v1""yy-shop/internal/biz"
)type accountService struct {v1.UnimplementedAccountServerlog *log.Helperauc *biz.AccountUseCase
}func NewAccountService(logger log.Logger, auc *biz.AccountUseCase) v1.AccountServer {return &accountService{log: log.NewHelper(logger),auc: auc,}
}func (a *accountService) Login(ctx context.Context, request *v1.LoginRequest) (*v1.LoginResponse, error) {token, err := a.auc.Login(ctx, request.GetPhone(), request.GetPassword())if err != nil {return nil, errors.New(500, "登录失败", err.Error())}return &v1.LoginResponse{Token: token,}, nil
}func (a *accountService) Register(ctx context.Context, request *v1.RegisterRequest) (*v1.RegisterResponse, error) {err := a.auc.Register(ctx, request.GetPhone(), request.GetPassword())if err != nil {return nil, errors.New(500, "注册失败", err.Error())}return &v1.RegisterResponse{}, nil
}

2.6.2、实现Server

server目录下,我们能看到分别代表grpchttp的接入点的文件:grpc.gohttp.go。在使用上,kratos的http接入点和grpc的在使用上是没有太大的区别的,代码结构几乎是相同的,只是调用的包不一样而已。如下所示:

grpc.go

package serverimport ("github.com/go-kratos/kratos/v2/log""github.com/go-kratos/kratos/v2/middleware/recovery""github.com/go-kratos/kratos/v2/transport/grpc"account_v1 "yy-shop/api/account/v1""yy-shop/internal/conf"
)// NewGRPCServer new a gRPC server.
func NewGRPCServer(c *conf.Server, logger log.Logger,as account_v1.AccountServer,
) *grpc.Server {var opts = []grpc.ServerOption{grpc.Middleware(recovery.Recovery(),),}if c.Grpc.Network != "" {opts = append(opts, grpc.Network(c.Grpc.Network))}if c.Grpc.Addr != "" {opts = append(opts, grpc.Address(c.Grpc.Addr))}if c.Grpc.Timeout != nil {opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))}srv := grpc.NewServer(opts...)account_v1.RegisterAccountServer(srv, as)return srv
}

http.go

package serverimport ("github.com/go-kratos/kratos/v2/log""github.com/go-kratos/kratos/v2/middleware/recovery""github.com/go-kratos/kratos/v2/transport/http"account_v1 "yy-shop/api/account/v1""yy-shop/internal/conf"
)// NewHTTPServer new a HTTP server.
func NewHTTPServer(c *conf.Server, logger log.Logger,as account_v1.AccountServer,
) *http.Server {var opts = []http.ServerOption{http.Middleware(recovery.Recovery(),),}if c.Http.Network != "" {opts = append(opts, http.Network(c.Http.Network))}if c.Http.Addr != "" {opts = append(opts, http.Address(c.Http.Addr))}if c.Http.Timeout != nil {opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))}srv := http.NewServer(opts...)account_v1.RegisterAccountHTTPServer(srv, as)return srv
}

2.7、依赖注入

读到这里,业务的编码就算是完成了,但是还要把每一层串起来,整个项目才能正常跑起来。细心的读者可能已经发现了,在前面处理连接mysql的逻辑的时候,在data.go文件中出现了一个var ProviderSet = wire.NewSet(NewData, NewUserRepo)的语句。没错,kratos是结合了wire做依赖注入的,在每个目录下都有一个和目录名称一致的源文件,该文件的其中一个作用就是声明wire的provider。

  • biz.go
// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(NewAccountUseCase, NewEncryptService)
  • data.go
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewUserRepo)
  • service.go
// ProviderSet is service providers.
var ProviderSet = wire.NewSet(NewAccountService)
  • server.go
// ProviderSet is server providers.
var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer)

最终,这些声明的provider会在cmd/yy-shop/wire.go中使用:

// wireApp init kratos application.
func wireApp(*conf.Server, *conf.Data, *conf.Bootstrap, log.Logger) (*kratos.App, func(), error) {panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

上面的都准备好后,最后只要在命令行下执行命令wire ./...就可以生成初始化项目的代码。

3、测试

当读者读到这里了,我们的应用就可以启动并通过postman(或者其他读者熟悉的工具)进行接口测试了。在测试前,我们先来启动服务,使用goland的读者,需要配置run configuration。因为服务启动的时候需要读取配置文件,而Goland默认的启动路径是在项目的根目录下,读者需要配置work directory到cmd目录下,如下图所示:

配置后启动,可以看到启动日志就说明启动成功了:

注意:本地的开发环境在上一篇的环境搭建已经启动过了

然后,我们打开postman,请求注册接口,返回的是200状态码即请求成功,如下图所示:

如果注册失败,返回的就不是200状态码了,并且会有具体的错误原因,如下图所示:

上面成功注册了一个用户后,我们就可以调试登录接口了,如下图所示:

注意:kratos提倡的错误处理可能和读者熟悉的不一样,kratos提倡使用http标准的错误码响应错误,这样是有好处的,具体的讨论会在系列的文章中说明,敬请期待。

【kratos入门实战教程】2-实现注册登陆业务相关推荐

  1. 【kratos入门实战教程】1-kratos项目搭建和开发环境配置

    1.系列目录 [kratos入门实战教程]0-商城项目介绍 [kratos入门实战教程]1-kratos项目搭建和开发环境配置 [kratos入门实战教程]2-实现注册登陆业务 2.概览 经过上一篇的 ...

  2. Kratos入门实战 一 项目创建和介绍

    Kratos 入门教程 简介.设计理念.项目结构参照官方文档,示例系统及版本 go1.19.1 darwin/amd64 一.开干 开启 GO111MODULE go env -w GO111MODU ...

  3. Python之Numpy入门实战教程(2):进阶篇之线性代数

    Numpy.Pandas.Matplotlib是Python的三个重要科学计算库,今天整理了Numpy的入门实战教程.NumPy是使用Python进行科学计算的基础库. NumPy以强大的N维数组对象 ...

  4. Python之Numpy入门实战教程(1):基础篇

    Numpy.Pandas.Matplotlib是Python的三个重要科学计算库,今天整理了Numpy的入门实战教程.NumPy是使用Python进行科学计算的基础库. NumPy以强大的N维数组对象 ...

  5. 视频教程-深度学习与PyTorch入门实战教程-深度学习

    深度学习与PyTorch入门实战教程 新加坡国立大学研究员 龙良曲 ¥399.00 立即订阅 扫码下载「CSDN程序员学院APP」,1000+技术好课免费看 APP订阅课程,领取优惠,最少立减5元 ↓ ...

  6. Spring Boot 入门实战教程

    Spring Boot 2.0 入门实战教程 开发环境:JDK1.8或以上 源码下载:https://pan.baidu.com/s/1Z771VDiuabDBJJV445xLeA 欢迎访问我的个人博 ...

  7. SharePoint Online 入门实战教程-杨建宇(霖雨)-专题视频课程

    SharePoint Online 入门实战教程-1053人已学习 课程介绍         本次课程以SharePoint Online国际版为环境,为大家介绍SharePoint Online的基 ...

  8. 视频教程-SharePoint 2019 入门实战教程-企业信息化

    SharePoint 2019 入门实战教程 大家好,我是霖雨,从2010年开始致力于SharePoint相关的技术研究,精通SharePoint环境搭建.实施.开发.运维.排错等相关技术,从2014 ...

  9. Flutter入门实战教程:从0到1仿写web版掘金App (完结)

    前言 准确的说,这是去年十一月份就写好的教程, 虽然迟迟未上线(拖了半年),但是非常感谢购买的老铁们~ 虽然心中很不爽, 但是回头想想,也是的确写的比较仓促,但是当时自己在写的过程中,的确能学到很多东 ...

最新文章

  1. 【Groovy】Groovy 方法调用 ( 使用闭包创建接口对象 | 接口中有一个函数 | 接口中有多个函数 )
  2. 浅谈JavaScript 函数作用域当中的“提升”现象
  3. 中国CMOS图像传感器行业运行状况与应用前景调研报告2022版
  4. 牛客网_PAT乙级_1020完美数列(25)【vector sort 最后一个测试用例超时】
  5. python 时间差模块_python利用datetime模块计算时间差
  6. 2020研究生数学建模结果_关于举办2020年全国研究生数学建模大赛的通知
  7. 列表相关元素及其属性
  8. 程序员最讨厌的9句话
  9. 后台代码和前台显示一样a href=' + URL + ' 使用转义字符
  10. 数组对象的filter方法
  11. 货币化物联网:实现收益
  12. memcached 分布式锁 java_分布式锁的三种实现方式
  13. C++STL优先队列小根堆大根堆自定义的应用
  14. 树莓派做服务器装什么系统安装,树莓派 安装 群晖系统安装教程
  15. eclipse 2018 安装html、jsp、JavaScript编辑器
  16. 【渝粤题库】广东开放大学 建筑设备 形成性考核
  17. 一个月攻克托业--复旦大学考生
  18. matlab f检验 f值,excel检验【Excel回归分析中的F检验】
  19. 学习笔记25 --贴有图片或者二维码的gazebo模型创建
  20. Java基础知识面试题(2021最新)

热门文章

  1. 如何连接到console口进行防火墙/交换机的管理
  2. 尚鼎峰:刚做抖音需要养号吗?抖音如何快速增加粉丝?
  3. 动态背景组件(vue-particles)
  4. 机器学习_决策树(信息熵,决策树,决策树优化,剪枝)
  5. QT5.12 Ui界面开发项目:QOpenGLShaderProgram::uniformLocation(model): shader program is not linked
  6. [学习开发板]iTOP-4412开发板AVIN驱动配置
  7. win10如何设置休眠选项
  8. 我的hexo站点主题config.yml文件配置
  9. 毕业论文内容指导与格式检查指南
  10. 机器人迷城手机版_机械迷城免费版完整版下载_机械迷城手机游戏无需付费版下载 安卓版 V4.1.1 - 罐头手游网...