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

系列

  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 操作 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 = 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

我是为少
微信:uuhells123
公众号:黑客下午茶
加我微信(互相学习交流),关注公众号(获取更多学习资料~)
posted @ 2021-04-10 22:57  为少  阅读(203)  评论(0编辑  收藏