Go + gRPC-Gateway(V2) 構建微服務實戰系列,小程式登入鑑權服務:第二篇(內附開發 demo)

為少發表於2021-04-10

系列

  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
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)

相關文章