前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第二篇(内附开发 demo)

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

作者头像
为少
发布2021-05-27 18:54:50
1.1K0
发布2021-05-27 18:54:50
举报

系列

  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().
		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

Refs

  • Demo: go-grpc-gateway-v2-microservice
    • https://github.com/Hacker-Linner/go-grpc-gateway-v2-microservice
  • gRPC-Gateway
    • https://github.com/grpc-ecosystem/grpc-gateway
  • gRPC-Gateway Docs
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑客下午茶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 系列
  • 鉴权微服务数据持久化
    • 使用 Docker 快速本地搭建 MongoDB 4.4.5 环境
      • MongoDB for VS Code
        • 使用 Playground 对 MongoDB 进行 CRUD
          • 初始化数据库和表
            • 用户 OpenID 查询/插入业务逻辑(MongoDB 指令分析)
            • 编码实战
              • 为微服务提供一个轻量级 DAO
                • 编写具体的查询/插入业务逻辑
                • Go 操作容器搭建真实的持久化 Unit Tests 环境
                  • 单元测试期间,使用 Go 程序完成容器启动与销毁
                    • 编写表格驱动单元测试
                      • 运行测试
                      • 联调
                        • 运行
                        • Refs
                        相关产品与服务
                        云数据库 MongoDB
                        腾讯云数据库 MongoDB(TencentDB for MongoDB)是腾讯云基于全球广受欢迎的 MongoDB 打造的高性能 NoSQL 数据库,100%完全兼容 MongoDB 协议,支持跨文档事务,提供稳定丰富的监控管理,弹性可扩展、自动容灾,适用于文档型数据库场景,您无需自建灾备体系及控制管理系统。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档