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

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

鉴权微服务数据持久化



使用 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,所以安装一下它(开发时,用它足够了)。


微信图片_20220611155852.png


使用 Playground 对 MongoDB 进行 CRUD


开发时,我们可以点击 Create New Playground 按钮,进行数据库相关的 CRUD 操作。


微信图片_20220611155855.png


初始化数据库和表


这里,数据库是grpc-gateway-auth,表是account


use('grpc-gateway-auth');
db.account.drop()
db.account.insertMany([
  {open_id: '123'},
  {open_id: '456'},
])
db.account.find()


微信图片_20220611155917.png


用户 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


微信图片_20220611155952.png


我们看到多出来一个 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
相关文章
|
2月前
|
监控 算法 NoSQL
Go 微服务限流与熔断最佳实践:滑动窗口、令牌桶与自适应阈值
🌟蒋星熠Jaxonic:Go微服务限流熔断实践者。分享基于滑动窗口、令牌桶与自适应阈值的智能防护体系,助力高并发系统稳定运行。
Go 微服务限流与熔断最佳实践:滑动窗口、令牌桶与自适应阈值
|
2月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
275 4
|
4月前
|
数据采集 数据挖掘 测试技术
Go与Python爬虫实战对比:从开发效率到性能瓶颈的深度解析
本文对比了Python与Go在爬虫开发中的特点。Python凭借Scrapy等框架在开发效率和易用性上占优,适合快速开发与中小型项目;而Go凭借高并发和高性能优势,适用于大规模、长期运行的爬虫服务。文章通过代码示例和性能测试,分析了两者在并发能力、错误处理、部署维护等方面的差异,并探讨了未来融合发展的趋势。
377 0
|
3月前
|
人工智能 负载均衡 API
Vercel 发布 AI Gateway 神器!可一键访问数百个模型,助力零门槛开发 AI 应用
大家好,我是Immerse,独立开发者、AGI实践者。分享编程、AI干货、开源项目与个人思考。关注公众号“沉浸式趣谈”,获取独家内容。Vercel新推出的AI Gateway,统一多模型API,支持自动切换、负载均衡与零加价调用,让AI开发更高效稳定。一行代码切换模型,告别接口烦恼!
426 1
Vercel 发布 AI Gateway 神器!可一键访问数百个模型,助力零门槛开发 AI 应用
|
2月前
|
JavaScript 前端开发 Java
【GoWails】Go做桌面应用开发?本篇文章带你上手Wails框架!一步步带你玩明白前后端双端的数据绑定!
wails是一个可以让你使用Go和Web技术编写桌面应用的项目 可以将它看作Go的快并且轻量级的Electron替代品。可以使用Go的功能,并结合现代化UI完成桌面应用程序的开发
563 4
|
6月前
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
380 61
|
6月前
|
开发框架 安全 前端开发
Go Web开发框架实践:模板渲染与静态资源服务
Gin 是一个功能强大的 Go Web 框架,不仅适用于构建 API 服务,还支持 HTML 模板渲染和静态资源托管。它可以帮助开发者快速搭建中小型网站,并提供灵活的模板语法、自定义函数、静态文件映射等功能,同时兼容 Go 的 html/template 引擎,具备高效且安全的页面渲染能力。
|
6月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
7月前
|
人工智能 缓存 安全
Go开发遇见的一次Data Race
本文通过一段 Go 语言代码示例,分析了并发编程中的数据竞争(Data Race)问题。代码实现了一个带缓存的内存存储系统,包含 `LRUCache` 和 `MemoryCache` 两个核心组件。尽管在 `MemoryCache` 的 `Set` 方法中加了锁保护,但由于直接调用 `LRUCache` 的 `GetLength` 方法时未加锁,导致底层数据结构在多 goroutine 环境下被同时读写,从而触发 Data Race。文章详细解析了问题根源,并提出了解决方案:为 `LRUCache` 的 `Add` 方法添加锁保护,确保并发安全。
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
688 6

热门文章

最新文章