簡介
小程序可以通過微信官方提供的登錄能力方便地獲取微信提供的用戶身份標識,快速建立小程序內的用戶體系。
系列
業務流程
初始化項目
開發環境
為少
的本地開發環境
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,
})
Refs
我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)