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

系列

  1. 雲原生 API 網關,gRPC-Gateway V2 初探
  2. Go + gRPC-Gateway(V2) 構建微服務實戰系列,小程序登陸鑑權服務:第一篇

鑑權微服務數據持久化

使用 Docker 快速本地搭建 MongoDB 4.4.5 環境

拉取鏡像git

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

啓動github

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)到了本機的 :27017mongodb

MongoDB for VS Code

由於爲少的開發環境是 VS Code,因此安裝一下它(開發時,用它足夠了)。docker

使用 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 是否存在,存在就直接返回當前記錄,不存在就插入並返回當前插入的記錄。

對應數據庫操做指令就是以下:app

db.account.findAndModify({
  query: {
    open_id: "abcdef"
  },
  update: {
    $setOnInsert: {
      _id: ObjectId("607132dcfbe32307260f728a"),
      open_id: "abcdef"
    }
  },
  upsert: true,
  new: true // 返回新插入的記錄
})

注意:tcp

  • 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
公衆號:黑客下午茶
加我微信(互相學習交流),關注公衆號(獲取更多學習資料~)
相關文章
相關標籤/搜索