Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第二篇(内附开发 demo)
2021-04-10 22:57
1341 查看
系列
鉴权微服务数据持久化
使用 Docker 快速本地搭建 MongoDB 4.4.5 环境
拉取镜像
docker pull mongo:4.4.5 # .... # Digest: sha256:67018ee2847d8c35e8c7aeba629795d091f93c93e23d3d60741fde74ed6858c4 # Status: Image is up to date for mongo:4.4.5 # docker.io/library/mongo:4.4.5
启动
docker run -p 27017:27017 -d mongo:4.4.5 docker ps # e6e8e350e749 mongo:4.4.5 ... 0.0.0.0:27017->27017/tcp ...
OK,我们看到成功映射了容器端口(
27017/tcp)到了本机的
:27017。
MongoDB for VS Code
因为
为少的开发环境是
VS Code,所以安装一下它(开发时,用它足够了)。
使用 Playground 对 MongoDB 进行 CRUD
开发时,我们可以点击
Create New Playground按钮,进行数据库相关的
CRUD操作。
初始化数据库和表
这里,数据库是
grpc-gateway-auth,表是
account。
use('grpc-gateway-auth'); db.account.drop() db.account.insertMany([ {open_id: '123'}, {open_id: '456'}, ]) db.account.find()
用户 OpenID 查询/插入业务逻辑(MongoDB 指令分析)
一句话描述:
- 在
account
集合中查找用户open_id
是否存在,存在就直接返回当前记录,不存在就插入并返回当前插入的记录。
对应数据库操作指令就是如下:
db.account.findAndModify({ query: { open_id: "abcdef" }, update: { $setOnInsert: { _id: ObjectId("607132dcfbe32307260f728a"), open_id: "abcdef" } }, upsert: true, new: true // 返回新插入的记录 })
注意:
- 将
upsert
设为true
。满足查询条件的记录存在时,不执行$setOnInsert
中的操作。满足条件的记录不存在时,执行$setOnInsert
操作。
编码实战
为微服务提供一个轻量级 DAO
具体源码放在(
dao/mongo):
....... ....... type Mongo struct { col *mongo.Collection newObjID func() primitive.ObjectID } func NewMongo(db *mongo.Database) *Mongo { // 返回个引用出去,根据需要(测试时)外部可随时改 `col` 和 `newObjID` 值 return &Mongo{ col: db.Collection("account"), // 给个初值 newObjID: primitive.NewObjectID, } } ....... .......
编写具体的查询/插入业务逻辑
通过
OpenID查询关联的账号
ID。具体源码放在(
dao/mongo):
func (m *Mongo) ResolveAccountID(c context.Context, openID string) (string, error) { insertedID := m.newObjID() // 对标上面的查询/插入指令 res := m.col.FindOneAndUpdate(c, bson.M{ openIDField: openID, }, mgo.SetOnInsert(bson.M{ mgo.IDField: insertedID, // mgo.IDField -> "_id", openIDField: openID, // openIDField -> "open_id" }), options.FindOneAndUpdate(). SetUps ad8 ert(true). SetReturnDocument(options.After)) if err := res.Err(); err != nil { return "", fmt.Errorf("cannot findOneAndUpdate: %v", err) } var row mgo.ObjID err := res.Decode(&row) if err != nil { return "", fmt.Errorf("cannot decode result: %v", err) } return row.ID.Hex(), nil }
Go 操作容器搭建真实的持久化 Unit Tests 环境
Go操作
Docker容器进行单元测试。拒绝
Mock,即时搭建/销毁真实的
DAO Unit Tests环境。
单元测试期间,使用 Go 程序完成容器启动与销毁
具体源码放在(
dao/mongo.go):
func RunWithMongoInDocker(m *testing.M, mongoURI *string) int { c, err := client.NewClientWithOpts() if err != nil { panic(err) } ctx := context.Background() resp, err := c.ContainerCreate(ctx, &container.Config{ Image: image, ExposedPorts: nat.PortSet{ containerPort: {}, }, }, &container.HostConfig{ PortBindings: nat.PortMap{ containerPort: []nat.PortBinding{ { HostIP: "0.0.0.0", // 127.0.0.1 HostPort: "0", // 随机挑一个端口 }, }, }, }, nil, nil, "") if err != nil { panic(err) } containerID := resp.ID defer func() { err := c.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{Force: true}) if err != nil { panic(err) } }() err = c.ContainerStart(ctx, containerID, types.ContainerStartOptions{}) if err != nil { panic(err) } inspRes, err := c.ContainerInspect(ctx, containerID) if err != nil { panic(err) } hostPort := inspRes.NetworkSettings.Ports[containerPort][0] *mongoURI = fmt.Sprintf("mongodb://%s:%s", hostPort.HostIP, hostPort.HostPort) return m.Run() }
编写表格驱动单元测试
具体源码放在(
dao/mongo_test.go):
func TestResolveAccountID(t *testing.T) { c := context.Background() mc, err := mongo.Connect(c, options.Client().ApplyURI(mongoURI)) if err != nil { t.Fatalf("cannot connect mongodb: %v", err) } m := NewMongo(mc.Database("grpc-gateway-auth")) // 初始化两条数据 _, err = m.col.InsertMany(c, []interface{}{ bson.M{ mgo.IDField: mustObjID("606f12ff0ba74007267bfeee"), openIDField: "openid_1", }, bson.M{ mgo.IDField: mustObjID("606f12ff0ba74007267bfeef"), openIDField: "openid_2", }, }) if err != nil { t.Fatalf("cannot insert initial values: %v", err) } // 注意,我猛将 `newObjID` 生成的 ID 变成固定了~ m.newObjID = fu 15a8 nc() primitive.ObjectID { return mustObjID("606f12ff0ba74007267bfef0") } // 定义表格测试 case cases := []struct { name string openID string want string }{ { name: "existing_user", openID: "openid_1", want: "606f12ff0ba74007267bfeee", }, { name: "another_existing_user", openID: "openid_2", want: "606f12ff0ba74007267bfeef", }, { name: "new_user", openID: "openid_3", want: "606f12ff0ba74007267bfef0", }, } for _, cc := range cases { t.Run(cc.name, func(t *testing.T) { id, err := m.ResolveAccountID(context.Background(), cc.openID) if err != nil { t.Errorf("failed resolve account id for %q: %v", cc.openID, err) } if id != cc.want { t.Errorf("resolve account id: want: %q; got: %q", cc.want, id) } }) } } func mustObjID(hex string) primitive.ObjectID { objID, err := primitive.ObjectIDFromHex(hex) if err != nil { panic(err) } return objID } func TestMain(m *testing.M) { os.Exit(mongotesting.RunWithMongoInDocker(m, &mongoURI)) }
运行测试
我们点击测试函数(
TestResolveAccountID)上方的
run test
我们看到多出来一个
Mongo DB容器。
联调
测试通过后,一般联调是没有问题的。
具体代码
auth/auth/auth.go
type Service struct { Mongo *dao.Mongo // 肚子里多一个数据访问层 Logger *zap.Logger OpenIDResolver OpenIDResolver authpb.UnimplementedAuthServiceServer } func (s *Service) Login(c context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse, error) { s.Logger.Info("received code", zap.String("code", req.Code)) openID, err := s.OpenIDResolver.Resolve(req.Code) if err != nil { return nil, status.Errorf(codes.Unavailable, "cannot resolve openid: %v", err) } accountID, err := s.Mongo.ResolveAccountID(c, openID) // 查询/插入操作 if err != nil { s.Logger.Error("cannot resolve account id", zap.Error(err)) return nil, status.Error(codes.Internal, "") } return &authpb.LoginResponse{ AccessToken: "token for open id " + accountID, ExpiresIn: 7200, }, nil }
具体代码
auth/main.go
authpb.RegisterAuthServiceServer(s, &auth.Service{ OpenIDResolver: &wechat.Service{ AppID: "your-app-id", AppSecret: "your-app-secret", }, Mongo: dao.NewMongo(mongoClient.Database("grpc-gateway-auth")), Logger: logger, })
运行
Service:
go run auth/main.go
gRPC-Gateway:
go run gateway/main.go
Refs
我是为少 微信:uuhells123 公众号:黑客下午茶 加我微信(互相学习交流),关注公众号(获取更多学习资料~)
相关文章推荐
- WCF开发实战系列二:使用IIS发布WCF服务
- 微信小程序:支付系列专辑(开发指南+精品Demo)
- java 开发实战经典 练习题 第12章 第7题 完成系统登录程序 从命令行输入用户名和密码
- [转]使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【八】——Web Api的安全性
- WCF开发实战系列二:使用IIS发布WCF服务
- 微信小程序开发环境(阿里云服务搭建+可运行的demo)
- Android官方开发文档Training系列课程中文版:构建第一款安卓应用之程序运行
- WCF开发实战系列四:使用Windows服务发布WCF服务
- WCF开发实战系列二:使用IIS发布WCF服务(转)
- [go-web开发小试验] 1-第一个demo程序
- 微信小程序电影推荐demo实战开发小结(附源码及思维导图) ... ...
- WCF开发实战系列三:自运行WCF服务
- WCF开发实战系列一:创建第一个WCF服务
- WCF开发实战系列三:自运行WCF服务
- 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【八】——Web Api的安全性
- 实战SpringCloud响应式微服务系列教程(第九章)使用Spring WebFlux构建响应式RESTful服务
- 如何在Visual Studio 2017中使用C# 7+语法 构建NetCore应用框架之实战篇(二):BitAdminCore框架定位及架构 构建NetCore应用框架之实战篇系列 构建NetCore应用框架之实战篇(一):什么是框架,如何设计一个框架 NetCore入门篇:(十二)在IIS中部署Net Core程序
- WCF开发实战系列一:创建第一个WCF服务
- 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【外传】——Attribute Routing
- WCF开发实战系列一:创建第一个WCF服务