Kratos框架
约 3054 字大约 10 分钟
2025-05-31
1.protobuf
Protocol Buffers(简称 protobuf)是 Google 开发的一种轻量级的、高效的序列化数据的机制。 主要目的:提高数据的传输和存储效率,特别适用于高效的网络通信和存储应用
- 特点:高效简洁、支持多种数据类型
- 使用场景:远程调用、数据存储、消息传递
- 语法定义:protobuf 的数据结构定义通常保存在 .proto 文件中,一个简单举例: syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
message AddressBook {
repeated Person people = 1;
}
定义了一个 Person 消息,包含三个字段(名字、ID、电子邮件),以及一个 AddressBook 消息,它包含多个 Person 对象
2.protoc
protoc 是 Protocol Buffers 的编译器,用于将 .proto 文件编译成特定语言的代码。通过 protoc 编译器,可以将 protobuf 定义文件转换成对应编程语言的类或数据结构,这些类可以用来序列化和反序列化数据。
2.1 安装 protoc
需求:安装指定版本 protobuf 25.0 下载链接:https://github.com/protocolbuffers/protobuf/releases/tag/v25.0
下载之后,解压缩,进入终端执行
cd protoc-25.0-osx-universal_binary
cp -r include/ /usr/local/include/
cp -r bin/ /usr/local/bin/
查看是否安装成功、查看版本号
protoc
protoc --version
2.2 安装 protoc-gen-go
安装命令如下 请注意,go 版本在 1.17 以下使用 go get;在 1.17 及以上使用 go install 进行安装
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
由于使用 go get 或 go install 命令安装时,protoc-gen-go 依赖会安装在 $GOPATH/go/bin目录里面,我们需要将其复制到 /usr/local/bin 里。
// 找到 GOPATH 目录
go env GOPATH
cd GOPATH 目录/bin
cp protoc-gen-go /usr/local/bin
1.5 举例
编译 .proto 文件命令(将 .proto 文件编译为目标语言的代码): // --go_out=. 表示将生成的 Go 代码输出到当前目录 // (可选,--java_out、python_out、cpp_out) protoc --go_out=. addressbook.proto
举例:有一个 addressbook.proto 文件,如下:
- syntax = "proto3"; option go_package = "./myProto"; // 指定生成文件的 Go 包路径 message Person { string name = 1; int32 id = 2; string email = 3; } message AddressBook { repeated Person people = 1; }
使用 protoc 编译器生成 Go 语言的代码:
protoc --go_out=. addressbook.proto
3.Kratos框架
3.1 介绍
Kratos 框架是一套轻量级 Go 微服务框架,包含大量微服务相关框架及工具。Kratos 官方文档:https://go-kratos.dev/docs/
3.2 第一个 Demo
- 前提条件:安装了 go、protoc 、protoc-gen-go
- 安装 kratos 命令工具 go install github.com/go-kratos/kratos/cmd/kratos/v2@latest 注意,和 protoc-gen-go 一样,需要复制 $GOPATH/bin 中的 kratos 文件夹到 /usr/local/bin 中
- 创建项目
使用默认模板创建项目
kratos new helloworld
如在国内环境拉取失败, 可 -r 指定源
kratos new helloworld -r https://gitee.com/go-kratos/kratos-layout.git
进入项目目录
cd helloworld
拉取项目依赖
go mod download
安装依赖
go get github.com/google/wire/cmd/wire@latest
生成所有proto源码、wire等等
go generate ./...
运行项目
kratos run
测试 HTTP 接口:
curl 'http://127.0.0.1:8000/helloworld/kratos'
输出:
{ "message": "Hello kratos" }
测试 错误 接口:
curl 'http://127.0.0.1:8000/helloworld/error'
输出
{ "code": 404, "reason": "USER_NOT_FOUND", "message": "user not found: error", "metadata": {} }
3.3 项目结构解析
各层的作用总结如下:
层 | 作用 | 与 Java 的对应关系 |
---|---|---|
api | 定义和编译 proto 文件 | |
cmd | 使用wire进行自动注入 运行整个程序的main函数 | |
configs | 编写配置文件 | resource / application.yaml |
internal / biz | domain 层,定义 DO 和操作数据库的方法接口 | domain 层 |
internal / conf | utils 层,工具类 | |
internal / data | 操作数据库数据 | dao 层,数据库操作 |
internal / service | 处理业务数据 | service 层 |
server | @restcontroller | |
third_party | 第三方调用相关的代码 |
4.生成接口代码步骤
4.1 定义接口
- 需求:实现登录接口 新建 v2 文件夹,并新建 user.proto 文件,代码如下
syntax = "proto3";
package v2;
// google。api.http:自动生成接口
import "google/api/annotations.proto";
// 生成代码的包路径
option go_package = "simple-kratos/api/helloworld/v2;v2";
service User {
// 定义一个登录接口
rpc Login (LoginRequest) returns (LoginResponse) {
option (google.api.http) = {
post: "/user/login",
body: "*",
};
}
}
// 登录请求
message LoginRequest {
string username = 1;
string password = 2;
}
// 登录后返回的对象信息
message LoginResponse {
int64 code = 1;
string msg = 2;
string token = 3;
}
4.2 生成 client 客户端代码
client 客户端生成的代码 == api 下 的接口文件 回到根目录下,执行以下命令
// 生成 客户端代码
kratos proto client api/helloworld/v2/user.proto
执行完毕之后,api / helloworld / v2 包下新增三个文件:
- user.pb.go:包含基础结构体信息、返回结构体信息
- user_grpc.pb.go:包含 grpc 通信代码
- user_http.pb.go:包含 http 通信代码
4.3 生成 server 服务代码
-t :指定生成代码的路径
kratos proto server api/helloworld/v2/user.proto -t internal/service
执行以上代码,找到 internal/service 文件夹,多了一个 user.go 文件,自动实现了 Login 方法
为了方便测试,我们改写 Login 登录逻辑,暂时不接入数据库,后续再加,先写死返回数据
func (s *UserService) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
// 自动生成的代码
// return &pb.LoginResponse{}, nil
return &pb.LoginResponse{
Code: 200,
Msg: "请求成功",
Token: "123456"}, nil
}
4.4 注册 service
打开internal/service/service.go文件,在newSet后面添加上 NewUserService 即可 [图片]
4.5 注册 http 和 grpc
以下只演示 注册 http ,注册 grpc 类比 目的:将 user 的接口暴露给 http 和 grpc,供外部可以访问 打开internal/server/http.go文件,添加以下代码 [图片]
4.6 自动注入
kratos生成的项目在 cmd/项目名称 目录下有一个 wire_gen.go 文件,这是执行wire自动注入生成的文件。终端进入并执行该命令,然后打开 cmd/user-center/wire_gen.go 文件,就可以看到刚才添加的 userService 被自动注入进来了 [图片]
4.7 启动项目测试
执行一下命令启动项目
kratos run
登录接口测试:
5.登录接口接入数据库
5.1 安装依赖
#安装mysql依赖
go get gorm.io/driver/mysql
#安装grom依赖
go get gorm.io/gorm
5.2 初始化数据库连接
在 data.go 新增初始化数据库连接,并将 NewObEngine 注册 到 data/data.go 的 ProviderSet 中
5.3 编写 biz 层
在 biz 文件夹新增 user.go ,并将 用户使用实例方法,即 NewUserUseCase 注册到 biz/biz.go 的 ProviderSet 中 package biz
import (
"context"
pb "simple-kratos/api/helloworld/v2"
)
/**
1、定义model对象
2、定义查询 user 相关信息的repository
3、为 repository 编写查询相关信息的抽象方法
4、实例化 repository
5、声明可用于自动注入的UseCase
6、编写调用 respository(dao) 层实现的方法
*/
/*
* User 的Model
*/
type User struct {
Id string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Password string `json:"password"`
AvatarImage string `json:"avatar_image"`
Age int `json:"age"`
Sex int `json:"sex"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
}
/*
* 编写关于User操作dao查询的方法
*/
type UserRepo interface {
// 根据用户名和密码查找用户
FindUserByUsernameAndPassword(ctx context.Context, loginReq *pb.LoginRequest) (acc *User, err error)
}
/*
* 定义User使用实例,用于自动注入User实例
*/
type UserUseCase struct {
userRepo UserRepo
}
/*
* UserUseCase 用户使用实例
*/
func NewUserUseCase(userRepo UserRepo) *UserUseCase {
return &UserUseCase{userRepo: userRepo}
}
/*
* service层编写方法去调用dao层的过渡方法
*/
func (uc *UserUseCase) GetUserByUsernameAndPassword(ctx context.Context, loginReq *pb.LoginRequest) (acc *User, err error) {
res, err := uc.userRepo.FindUserByUsernameAndPassword(ctx, loginReq)
return res, err
}
5.5 编写 dao 层
在 data 文件夹新增 user.go ,并将 NewUserRepo 注册到 data/data.go 的 ProviderSet 中
package data
import (
"context"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/gorm"
v2 "simple-kratos/api/helloworld/v2"
"simple-kratos/internal/biz"
)
/**
1、引用前面注册的userRepo
2、实现在biz里面定义的repository的抽象方法
*/
type userRepo struct {
data *Data
log *log.Helper
}
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
return &userRepo{
data: data,
log: log.NewHelper(logger),
}
}
// 实现在biz里面定义的repository的抽象方法
func (acc userRepo) FindUserByUsernameAndPassword(ctx context.Context, loginReq *v2.LoginRequest) (res *biz.User, err error) {
user := &biz.User{}
fmt.Println("准备查找用户", user)
err = acc.data.db.Table("user").Where("username = ? and password = ?", loginReq.Username, loginReq.Password).First(&user).Error
if err != nil {
// 判断是否是记录未找到的错误
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("用户不存在")
}
return nil, err
}
return user, nil
}
5.5 编写 service 层逻辑
调用 biz 层登录方法
5.6 自动注入
前面我们已经注册了 biz.go 的 NewUserRepo 和 data.go 的 NewUserUseCase 终端切换到 cmd/项目名,执行 wire,查看 wire_gen.go,发现 userRepo 和 userUseRepo 已经自动注入了
5.7 各层调用小结
- api 层,绑定路由和业务函数关系
- service 层,将请求参数转换为 DO 对象
- biz 层,定义 DO 和声明 data 层的方法,并调用 dao 层的数据库操作
- dao 层,实现 biz 层声明的方法,进行增删查改操作
6.注册接口
6.1 定义并生成接口
打开终端,在 cmd 下执行 make api 命令,生成 关于注册接口的代码
1. 在 user.proto 新增以下内容
// 注册接口
rpc Register (RegisterRequest) returns (UserInfoResponse) {
option (google.api.http) = {
post: "/user/register",
body: "*",
};
}
// 注册请求
message RegisterRequest {
string username = 1;
string nickname = 2;
string password = 3;
}
// 用户信息
message UserInfoResponse {
int64 id = 1;
string username = 2;
string nickname = 3;
string password = 4;
int64 avatar_image = 5;
string age = 6;
int64 sex = 7;
}
6.3 编写 service 层
该层处理请求参数,将其转为 data 参数 新增 Register 方法:
func (s *UserService) Register(ctx context.Context, req *v2.RegisterRequest) (*v2.UserInfoResponse, error) {
// 将请求参数转为用户实体,调用 biz 层的 RegisterUser 方法
user, err := s.uc.RegisterUser(ctx, &core.User{
Username: req.Username,
Nickname: req.Nickname,
Password: req.Password,
})
if err != nil {
return nil, err
}
userInfoRsp := v2.UserInfoResponse{
Username: user.Username,
Nickname: user.Nickname,
Password: user.Password,
}
// 返回用户信息
return &userInfoRsp, nil
}
6.4 编写 biz 层
该层主要工作如下:
- 声明 data 层操作数据库的方法
- 新增 注册用户 方法,并调用 data 层操作数据库的方法
type UserRepo interface {
// 根据用户名和密码查找用户
FindUserByUsernameAndPassword(ctx context.Context, loginReq *v2.LoginRequest) (*core.User, error)
// 插入用户
InsertUser(ctx context.Context, user *core.User) (*core.User, error)
}
/*
* service层去调用dao层的过渡方法:注册用户
*/
func (uc *UserUseCase) RegisterUser(ctx context.Context, u *core.User) (*core.User, error) {
return uc.userRepo.InsertUser(ctx, u)
}
6.5 编写 data 层
该层主要工作如下:
实现操作数据库的方法
// 插入用户
func (r userRepo) InsertUser(ctx context.Context, user *core.User) (*core.User, error) {
var u core.User
// 1、检查用户名是否已使用
result := r.data.db.Table("user").Where("username = ?", user.Username).First(&user)
if result.RowsAffected == 1 {
return nil, fmt.Errorf("用户名已使用,请更改")
}
u.Username = user.Username
u.Password = user.Password
u.Nickname = user.Nickname
u.CreateTime = time.Now()
u.UpdateTime = time.Now()
// 插入用户
res := r.data.db.Table("user").Create(&u)
if res.Error != nil {
return nil, fmt.Errorf("数据插入失败")
}
// 返回用户信息
return &core.User{
Username: user.Username,
Nickname: user.Nickname,
Password: user.Password,
}, nil
}
6.6 启动测试
启动项目
kratos run
用户名已使用情况:
用户名未使用情况:
8.使用 JWT 认证中间件
8.1 JWT 介绍
Json web token (JWT) 是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,特别适用于分布式站点的单点登录(SSO)场景。
8.2 JWT 构成
JWT 又三部分组成,分别为 头部(header)、载荷(payload)、签证(signature)
头部
alg:声明加密的算法
typ:声明类型 将头部进行base64加密,构成 JWT 的第一部分
载荷:存储有效信息
标准中注册的声明
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
2. 公共声明:一般添加用户的相关信息
3. 私有声明
将载荷进行base64加密,构成 JWT 的第二部分
- 签证 5. header( base 64加密后的) 6. payload( base 64加密后的) 7. secret 签证部分需要经过 base64 加密后的 头部 header 和 载荷 payload 使用,连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分
认证小结
- 前端发送登陆请求
- 登陆请求处理
- 验证用户名密码是否正确
- auth.生成 JWT 的 Token
- 返回 token 给前端
- 其他正常的请求
- JWT 中间件进行令牌认证信息校验
- 中间件解析 JWT 中间件的 Payload 信息,根据用户信息以及操作名进行权鉴
// token 认证白名单,即不需要认证 func NewTokenWhiteListMatcher() selector.MatchFunc {
whiteList := make(map[string]struct{})
whiteList["/user/login"] = struct{}{}
whiteList["/user/register"] = struct{}{}
return func(ctx context.Context, operation string) bool {
if _, ok := whiteList[operation]; ok {
return false
}
return true
}