鉴权微服务数据持久化
使用 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(). SetUpsert(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 程序完成容器启动与销毁
具体源码放在(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 = func() 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