用户潮 發表於 2021-4-7 10:27:00

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

<p><img src="https://img2020.cnblogs.com/blog/436453/202104/436453-20210407102646016-492967673.png" alt="" loading="lazy"></p>
<h2 id="简介">简介</h2>
<p>小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。</p>
<h3 id="系列">系列</h3>
<ol>
<li>云原生 API 网关,gRPC-Gateway V2 初探</li>
</ol>
<h3 id="业务流程">业务流程</h3>
<p><img src="https://img2020.cnblogs.com/blog/436453/202104/436453-20210407102509270-259195076.jpg" alt="" loading="lazy"></p>
<ul>
<li>官方开发接入文档</li>
</ul>
<h2 id="初始化项目">初始化项目</h2>
<h3 id="开发环境">开发环境</h3>
<p><code>为少</code> 的本地开发环境</p>
<pre><code class="language-sh">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
</code></pre>
<h3 id="初始代码结构">初始代码结构</h3>
<p>使用 <code>go mod init server</code> 初始化 <code>Go</code> 项目,这里(<code>demo</code>)我直接采用 <code>server</code> 作为当前 <code>module</code> 名字。</p>
<p><code>go-grpc-gateway-v2-microservice</code></p>
<pre><code>├── auth // 鉴权微服务
│&nbsp;&nbsp; ├── api
│&nbsp;&nbsp; ├── ├── gen
│&nbsp;&nbsp; ├── ├── ├── v1 // 生成的代码将放到这里,v1 表示第一个 API 版本
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── auth.proto
│&nbsp;&nbsp; │&nbsp;&nbsp; └── auth.yaml
│&nbsp;&nbsp; ├── auth
│&nbsp;&nbsp; │&nbsp;&nbsp; └── auth.go // service 的具体实现
│&nbsp;&nbsp; ├── wechat
│&nbsp;&nbsp; └── main.go // 鉴权 gRPC server
├── gateway // gRPC-Gateway,反向代理到各个 gRPC Server
│&nbsp;&nbsp; └── main.go
├── gen.sh // 根据 `auth.proto` 生成代码的命令
└── go.mod
</code></pre>
<h2 id="领域authproto定义">领域(auth.proto)定义</h2>
<pre><code class="language-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);
}
</code></pre>
<h2 id="使用-grpc-gateway-暴露-restful-json-api">使用 gRPC-Gateway 暴露 RESTful JSON API</h2>
<h3 id="authyaml-定义"><code>auth.yaml</code> 定义</h3>
<pre><code class="language-yaml">type: google.api.Service
config_version: 3

http:
rules:
- selector: auth.v1.AuthService.Login
    post: /v1/auth/login
    body: "*"
</code></pre>
<h2 id="根据配置生成代码">根据配置生成代码</h2>
<h3 id="使用-gensh-生成-grpc-gateway-相关代码">使用 <code>gen.sh</code> 生成 <code>gRPC-Gateway</code> 相关代码</h3>
<pre><code class="language-sh">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
</code></pre>
<p>运行:</p>
<pre><code class="language-sh">sh gen.sh
</code></pre>
<p>成功后,会生成 <code>auth.pb.go</code>,<code>auth_grpc.pb.go</code>,<code>auth.pb.gw.go</code> 文件,代码结构如下:</p>
<pre><code>├── auth
│&nbsp;&nbsp; ├── api
│&nbsp;&nbsp; ├── ├── gen
│&nbsp;&nbsp; ├── ├── ├── v1
│&nbsp;&nbsp; ├── ├── ├── ├── auth.pb.go // 生成的 golang 相关的 protobuf 代码
│&nbsp;&nbsp; ├── ├── ├── ├── auth_grpc.pb.go// 生成 golang 相关的 gRPC Server 代码
│&nbsp;&nbsp; ├── ├── ├── ├── auth.pb.gw.go // 生成 golang 相关的 gRPC-Gateway 代码
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── auth.proto
│&nbsp;&nbsp; │&nbsp;&nbsp; └── auth.yaml
│&nbsp;&nbsp; ├── auth
│&nbsp;&nbsp; │&nbsp;&nbsp; └── auth.go
│&nbsp;&nbsp; ├── wechat
│&nbsp;&nbsp; └── main.go
├── gateway
│&nbsp;&nbsp; └── main.go
├── gen.sh
└── go.mod
</code></pre>
<p>整理一下包:</p>
<pre><code class="language-sh">go mod tidy
</code></pre>
<h2 id="初步实现-auth-grpc-service-server">初步实现 Auth gRPC Service Server</h2>
<h3 id="实现-authserviceserver-接口">实现 <code>AuthServiceServer</code> 接口</h3>
<p>我们查看生成 <code>auth_grpc.pb.go</code> 代码,找到 <code>AuthServiceServer</code> 定义:</p>
<pre><code class="language-go">……
// 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()
}
……
</code></pre>
<p>我们在 <code>auth/auth/auth.go</code> 进行它的实现:</p>
<p>关键代码解读:</p>
<pre><code class="language-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 &amp;authpb.LoginResponse{
                AccessToken: "token for open id " + openID,
                ExpiresIn:   7200,
        }, nil
}
</code></pre>
<p>这里有一个非常重要的编程理念,用好可以事半功倍。<strong>接口定义由使用者定义而不是实现者</strong>,如这里的 <code>OpenIDResolver</code> 接口。</p>
<h3 id="实现-openidresolver-接口">实现 <code>OpenIDResolver</code> 接口</h3>
<p>这里用到了社区的一个第三方库,这里主要用来完成开发者服务器向微信服务器换取 用户唯一标识 <code>OpenID</code> 、 用户在微信开放平台帐号下的唯一标识 <code>UnionID</code>(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 <code>session_key</code>。<br>
当然,不用这个库,自己写也挺简单。</p>
<pre><code class="language-sh">go get -u github.com/medivhzhan/weapp/v2
</code></pre>
<p>我们在 <code>auth/wechat/wechat.go</code> 进行它的实现:</p>
<p>关键代码解读:</p>
<pre><code class="language-go">// 相同的 Service 实现套路再来一遍
// AppID &amp; 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
}
</code></pre>
<h3 id="配置-auth-service-grpc-server">配置 Auth Service gRPC Server</h3>
<p><code>auth/main.go</code></p>
<pre><code class="language-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, &amp;auth.Service{
                OpenIDResolver: &amp;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))   
        }
}
</code></pre>
<h2 id="初步实现-api-gateway">初步实现 API Gateway</h2>
<p><code>gateway/main.go</code></p>
<pre><code class="language-go">// 创建一个可取消的上下文(如:请求发到一半可随时取消)
c := context.Background()
c, cancel := context.WithCancel(c)
defer cancel()

mux := runtime.NewServeMux(runtime.WithMarshalerOption(
        runtime.MIMEWildcard,
        &amp;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)
}
</code></pre>
<h2 id="测试">测试</h2>
<pre><code class="language-ts">// 发送 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,
})
</code></pre>
<p><img src="https://img2020.cnblogs.com/blog/436453/202104/436453-20210407102526395-1301070744.png" alt="" loading="lazy"></p>
<h2 id="refs">Refs</h2>
<ul>
<li>Demo: go-grpc-gateway-v2-microservice</li>
<li>gRPC-Gateway</li>
<li>gRPC-Gateway Docs</li>
</ul>
<pre><code class="language-sh">我是为少
微信:uuhells123
公众号:黑客下午茶
加我微信(互相学习交流),关注公众号(获取更多学习资料~)
</code></pre><br><br>
来源:https://www.cnblogs.com/hacker-linner/p/14626613.html
頁: [1]
查看完整版本: Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇(内附开发 demo)