您的位置:首页 > 移动开发 > 微信开发

Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第二篇(内附开发 demo)

2021-04-10 22:57 1341 查看

系列

  1. 云原生 API 网关,gRPC-Gateway V2 初探
  2. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇

鉴权微服务数据持久化

使用 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
公众号:黑客下午茶
加我微信(互相学习交流),关注公众号(获取更多学习资料~)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐