简介
小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。
系列
业务流程
- 官方开发接入文档
初始化项目
开发环境
为少
的本地开发环境
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
├── 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)定义
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
定义
type: google.api.Service config_version: 3 http: rules: - selector: auth.v1.AuthService.Login post: /v1/auth/login body: "*"
根据配置生成代码
使用 gen.sh
生成 gRPC-Gateway
相关代码
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
运行:
sh gen.sh
成功后,会生成 auth.pb.go
,auth_grpc.pb.go
,auth.pb.gw.go
文件,代码结构如下:
├── 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
整理一下包:
go mod tidy
初步实现 Auth gRPC Service Server
实现 AuthServiceServer
接口
我们查看生成 auth_grpc.pb.go
代码,找到 AuthServiceServer
定义:
…… // 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
进行它的实现:
关键代码解读:
// 定义 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
。当然,不用这个库,自己写也挺简单。
go get -u github.com/medivhzhan/weapp/v2
我们在 auth/wechat/wechat.go
进行它的实现:
关键代码解读:
// 相同的 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
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
// 创建一个可取消的上下文(如:请求发到一半可随时取消) 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) }
测试
// 发送 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, })