zoukankan      html  css  js  c++  java
  • 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
    公众号:黑客下午茶
    加我微信(互相学习交流),关注公众号(获取更多学习资料~)
    
  • 相关阅读:
    sgu209:Areas(计算几何)
    altium designer电气符号和包的常用元素
    wxWidgets谁刚开始学习指南(5)——使用wxSmith可视化设计
    zoj 3820 Building Fire Stations(二分法+bfs)
    iOS 注册或登录页面(UILable,UITextField,UIButton)
    [ACM] POJ 2689 Prime Distance (筛选范围大素数)
    数字签名和数字证书技术简介(两)
    [Node] Using dotenv to config env variables
    [TypeStyle] Compose CSS classes using TypeStyle
    [Node] Use babel-preset-env with Native Node Features and Also Use Babel Plugins
  • 原文地址:https://www.cnblogs.com/hacker-linner/p/14642517.html
Copyright © 2011-2022 走看看