前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇(内附开发 demo)

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

作者头像
为少
发布2021-05-27 18:18:23
1.2K0
发布2021-05-27 18:18:23
举报
文章被收录于专栏:黑客下午茶

简介

小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

系列

  1. 云原生 API 网关,gRPC-Gateway V2 初探

业务流程

  • 官方开发接入文档

初始化项目

开发环境

为少 的本地开发环境

代码语言:javascript
复制
go version
# go version go1.14.14 darwin/amd64
protoc --version
# libprotoc 3.15.7
protoc-gen-go --version
# protoc-gen-go v1.26.0
protoc-gen-go-grpc --version
# protoc-gen-go-grpc 1.1.0
protoc-gen-grpc-gateway --version

初始代码结构

使用 go mod init server 初始化 Go 项目,这里(demo)我直接采用 server 作为当前 module 名字。

go-grpc-gateway-v2-microservice

代码语言:javascript
复制
├── auth // 鉴权微服务
│   ├── api
│   ├── ├── gen
│   ├── ├── ├── v1 // 生成的代码将放到这里,v1 表示第一个 API 版本
│   │   ├── auth.proto
│   │   └── auth.yaml
│   ├── auth
│   │   └── auth.go // service 的具体实现
│   ├── wechat 
│   └── main.go // 鉴权 gRPC server
├── gateway // gRPC-Gateway,反向代理到各个 gRPC Server
│   └── main.go
├── gen.sh // 根据 `auth.proto` 生成代码的命令
└── go.mod

领域(auth.proto)定义

代码语言:javascript
复制
syntax = "proto3";
package auth.v1;
option go_package="server/auth/api/gen/v1;authpb";

// 客户端发送一个 code
message LoginRequest {
    string code = 1;
}

// 开发者服务器返回一个自定义登录态(token)
message LoginResponse {
    string access_token = 1;
    int32 expires_in = 2; // 按 oauth2 约定走
}

service AuthService {
    rpc Login (LoginRequest) returns (LoginResponse);
}

使用 gRPC-Gateway 暴露 RESTful JSON API

auth.yaml 定义

代码语言:javascript
复制
type: google.api.Service
config_version: 3

http:
  rules:
  - selector: auth.v1.AuthService.Login
    post: /v1/auth/login
    body: "*"

根据配置生成代码

使用 gen.sh 生成 gRPC-Gateway 相关代码

代码语言:javascript
复制
PROTO_PATH=./auth/api
GO_OUT_PATH=./auth/api/gen/v1

protoc -I=$PROTO_PATH --go_out=paths=source_relative:$GO_OUT_PATH auth.proto
protoc -I=$PROTO_PATH --go-grpc_out=paths=source_relative:$GO_OUT_PATH auth.proto
protoc -I=$PROTO_PATH --grpc-gateway_out=paths=source_relative,grpc_api_configuration=$PROTO_PATH/auth.yaml:$GO_OUT_PATH auth.proto

运行:

代码语言:javascript
复制
sh gen.sh

成功后,会生成 auth.pb.goauth_grpc.pb.goauth.pb.gw.go 文件,代码结构如下:

代码语言:javascript
复制
├── auth
│   ├── api
│   ├── ├── gen
│   ├── ├── ├── v1
│   ├── ├── ├── ├── auth.pb.go // 生成的 golang 相关的 protobuf 代码
│   ├── ├── ├── ├── auth_grpc.pb.go  // 生成 golang 相关的 gRPC Server 代码
│   ├── ├── ├── ├── auth.pb.gw.go // 生成 golang 相关的 gRPC-Gateway 代码
│   │   ├── auth.proto
│   │   └── auth.yaml
│   ├── auth
│   │   └── auth.go
│   ├── wechat 
│   └── main.go
├── gateway
│   └── main.go
├── gen.sh
└── go.mod

整理一下包:

代码语言:javascript
复制
go mod tidy

初步实现 Auth gRPC Service Server

实现 AuthServiceServer 接口

我们查看生成 auth_grpc.pb.go 代码,找到 AuthServiceServer 定义:

代码语言:javascript
复制
……
// AuthServiceServer is the server API for AuthService service.
// All implementations must embed UnimplementedAuthServiceServer
// for forward compatibility
type AuthServiceServer interface {
	Login(context.Context, *LoginRequest) (*LoginResponse, error)
	mustEmbedUnimplementedAuthServiceServer()
}
……

我们在 auth/auth/auth.go 进行它的实现:

关键代码解读:

代码语言:javascript
复制
// 定义 Service 结构体
type Service struct {
	Logger         *zap.Logger
	OpenIDResolver OpenIDResolver
	authpb.UnimplementedAuthServiceServer
}
// 这里作为使用者来说做一个抽象
// 定义与微信第三方服务器通信的接口
type OpenIDResolver interface {
	Resolve(code string) (string, error)
}
// 具体的方法实现
func (s *Service) Login(c context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse, error) {
	s.Logger.Info("received code",
		zap.String("code", req.Code))
	// 调用微信服务器,拿到用户的唯一标识 openId	
	openID, err := s.OpenIDResolver.Resolve(req.Code)
	if err != nil {
		return nil, status.Errorf(codes.Unavailable,
			"cannot resolve openid: %v", err)
	}
	// 调试代码,先这样写
	return &authpb.LoginResponse{
		AccessToken: "token for open id " + openID,
		ExpiresIn:   7200,
	}, nil
}

这里有一个非常重要的编程理念,用好可以事半功倍。接口定义由使用者定义而不是实现者,如这里的 OpenIDResolver 接口。

实现 OpenIDResolver 接口

这里用到了社区的一个第三方库,这里主要用来完成开发者服务器向微信服务器换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识 UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。当然,不用这个库,自己写也挺简单。

代码语言:javascript
复制
go get -u github.com/medivhzhan/weapp/v2

我们在 auth/wechat/wechat.go 进行它的实现:

关键代码解读:

代码语言:javascript
复制
// 相同的 Service 实现套路再来一遍
// AppID & AppSecret 要可配置,是从外面传进来的
type Service struct {
	AppID     string
	AppSecret string
}
func (s *Service) Resolve(code string) (string, error) {
	resp, err := weapp.Login(s.AppID, s.AppSecret, code)
	if err != nil {
		return "", fmt.Errorf("weapp.Login: %v", err)
	}
	if err = resp.GetResponseError(); err != nil {
		return "", fmt.Errorf("weapp response error: %v", err)
	}
	return resp.OpenID, nil
}

配置 Auth Service gRPC Server

auth/main.go

代码语言:javascript
复制
func main() {
	logger, err := zap.NewDevelopment()
	if err != nil {
		log.Fatalf("cannot create logger: %v", err)
	}
    // 配置服务器监听端口
	lis, err := net.Listen("tcp", ":8081")
	if err != nil {
		logger.Fatal("cannot listen", zap.Error(err))
	}
    
    // 新建 gRPC server
	s := grpc.NewServer()
	// 配置具体 Service
	authpb.RegisterAuthServiceServer(s, &auth.Service{
		OpenIDResolver: &wechat.Service{
			AppID:     "your-app-id",
			AppSecret: "your-app-secret",
		},
		Logger: logger,
	})
	// 对外开始服务
	err = s.Serve(lis)
	if err != nil {
	    logger.Fatal("cannot server", zap.Error(err))   
	}
}

初步实现 API Gateway

gateway/main.go

代码语言:javascript
复制
// 创建一个可取消的上下文(如:请求发到一半可随时取消)
c := context.Background()
c, cancel := context.WithCancel(c)
defer cancel()

mux := runtime.NewServeMux(runtime.WithMarshalerOption(
	runtime.MIMEWildcard,
	&runtime.JSONPb{
		MarshalOptions: protojson.MarshalOptions{
			UseEnumNumbers: true, // 枚举字段的值使用数字
			UseProtoNames:  true, 
			// 传给 clients 的 json key 使用下划线 `_`
			// AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
			// 这里说明应使用 access_token
		},
		UnmarshalOptions: protojson.UnmarshalOptions{
			DiscardUnknown: true, // 忽略 client 发送的不存在的 poroto 字段
		},
	},
))
err := authpb.RegisterAuthServiceHandlerFromEndpoint(
	c,
	mux,
	"localhost:8081",
	[]grpc.DialOption{grpc.WithInsecure()},
)
if err != nil {
	log.Fatalf("cannot register auth service: %v", err)
}

err = http.ListenAndServe(":8080", mux)
if err != nil {
	log.Fatalf("cannot listen and server: %v", err)
}

测试

代码语言:javascript
复制
// 发送 res.code 到后台换取 openId, sessionKey, unionId
wx.request({
  url: "http://localhost:8080/v1/auth/login",
  method: "POST",
  data: { code: res.code },
  success: console.log,
  fail: console.error,
})

Refs

  • Demo: go-grpc-gateway-v2-microservice
    • https://github.com/Hacker-Linner/go-grpc-gateway-v2-microservice
  • gRPC-Gateway
    • https://github.com/grpc-ecosystem/grpc-gateway
  • gRPC-Gateway Docs
    • https://grpc-ecosystem.github.io/grpc-gateway
代码语言:javascript
复制
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑客下午茶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
    • 系列
      • 业务流程
      • 初始化项目
        • 开发环境
          • 初始代码结构
          • 领域(auth.proto)定义
          • 使用 gRPC-Gateway 暴露 RESTful JSON API
            • auth.yaml 定义
            • 根据配置生成代码
              • 使用 gen.sh 生成 gRPC-Gateway 相关代码
              • 初步实现 Auth gRPC Service Server
                • 实现 AuthServiceServer 接口
                  • 实现 OpenIDResolver 接口
                    • 配置 Auth Service gRPC Server
                    • 初步实现 API Gateway
                    • 测试
                    • Refs
                    相关产品与服务
                    云服务器
                    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档