Go gRPC进阶-go-grpc-middleware使用(八)

939496716 · · 878 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

<div id="cnblogs_post_body" class="blogpost-body cnblogs-markdown"> <h3 id="前言">前言</h3> <p>上篇介绍了gRPC中TLS认证和自定义方法认证,最后还简单介绍了gRPC拦截器的使用。gRPC自身只能设置一个拦截器,所有逻辑都写一起会比较乱。本篇简单介绍<a href="https://github.com/grpc-ecosystem/go-grpc-middleware">go-grpc-middleware</a>的使用,包括<code>grpc_zap</code>、<code>grpc_auth</code>和<code>grpc_recovery</code>。</p> <h3 id="go-grpc-middleware简介">go-grpc-middleware简介</h3> <p>go-grpc-middleware封装了认证(auth), 日志( logging), 消息(message), 验证(validation), 重试(retries) 和监控(retries)等拦截器。</p> <ul> <li>安装 <code>go get github.com/grpc-ecosystem/go-grpc-middleware</code></li> <li>使用</li> </ul> <pre><code class="language-go hljs"><span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/grpc-ecosystem/go-grpc-middleware"</span> myServer := grpc.NewServer( grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( grpc_ctxtags.StreamServerInterceptor(), grpc_opentracing.StreamServerInterceptor(), grpc_prometheus.StreamServerInterceptor, grpc_zap.StreamServerInterceptor(zapLogger), grpc_auth.StreamServerInterceptor(myAuthFunction), grpc_recovery.StreamServerInterceptor(), )), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( grpc_ctxtags.UnaryServerInterceptor(), grpc_opentracing.UnaryServerInterceptor(), grpc_prometheus.UnaryServerInterceptor, grpc_zap.UnaryServerInterceptor(zapLogger), grpc_auth.UnaryServerInterceptor(myAuthFunction), grpc_recovery.UnaryServerInterceptor(), )), ) </code></pre> <p><code>grpc.StreamInterceptor</code>中添加流式RPC的拦截器。<br> <code>grpc.UnaryInterceptor</code>中添加简单RPC的拦截器。</p> <h3 id="grpc_zap日志记录">grpc_zap日志记录</h3> <p>1.创建zap.Logger实例</p> <pre><code class="language-go hljs"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ZapInterceptor</span><span class="hljs-params">()</span> *<span class="hljs-title">zap</span>.<span class="hljs-title">Logger</span></span> { logger, err := zap.NewDevelopment() <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { log.Fatalf(<span class="hljs-string">"failed to initialize zap logger: %v"</span>, err) } grpc_zap.ReplaceGrpcLogger(logger) <span class="hljs-keyword">return</span> logger } </code></pre> <p>2.把zap拦截器添加到服务端</p> <pre><code class="language-go hljs">grpcServer := grpc.NewServer( grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( grpc_zap.StreamServerInterceptor(zap.ZapInterceptor()), )), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( grpc_zap.UnaryServerInterceptor(zap.ZapInterceptor()), )), ) </code></pre> <p>3.日志分析</p> <p><img src="https://img2020.cnblogs.com/blog/1508611/202004/1508611-20200421150239484-1399156155.png" alt=""><br> 各个字段代表的意思如下:</p> <pre><code class="language-json hljs">{ <span class="hljs-attr">"level"</span>: <span class="hljs-string">"info"</span>, // string zap log levels <span class="hljs-attr">"msg"</span>: <span class="hljs-string">"finished unary call"</span>, // string log message <span class="hljs-attr">"grpc.code"</span>: <span class="hljs-string">"OK"</span>, // string grpc status code <span class="hljs-attr">"grpc.method"</span>: <span class="hljs-string">"Ping"</span>, / string method name <span class="hljs-attr">"grpc.service"</span>: <span class="hljs-string">"mwitkow.testproto.TestService"</span>, // string full name of the called service <span class="hljs-attr">"grpc.start_time"</span>: <span class="hljs-string">"2006-01-02T15:04:05Z07:00"</span>, // string RFC3339 representation of the start time <span class="hljs-attr">"grpc.request.deadline"</span>: <span class="hljs-string">"2006-01-02T15:04:05Z07:00"</span>, // string RFC3339 deadline of the current request if supplied <span class="hljs-attr">"grpc.request.value"</span>: <span class="hljs-string">"something"</span>, // string value on the request <span class="hljs-attr">"grpc.time_ms"</span>: <span class="hljs-number">1.345</span>, // float32 run time of the call in ms <span class="hljs-attr">"peer.address"</span>: { <span class="hljs-attr">"IP"</span>: <span class="hljs-string">"127.0.0.1"</span>, // string IP address of calling party <span class="hljs-attr">"Port"</span>: <span class="hljs-number">60216</span>, // int port call is coming in on <span class="hljs-attr">"Zone"</span>: <span class="hljs-string">""</span> // string peer zone for caller }, <span class="hljs-attr">"span.kind"</span>: <span class="hljs-string">"server"</span>, // string client | server <span class="hljs-attr">"system"</span>: <span class="hljs-string">"grpc"</span>, // string <span class="hljs-attr">"custom_field"</span>: <span class="hljs-string">"custom_value"</span>, // string user defined field <span class="hljs-attr">"custom_tags.int"</span>: <span class="hljs-number">1337</span>, // int user defined tag on the ctx <span class="hljs-attr">"custom_tags.string"</span>: <span class="hljs-string">"something"</span> // string user defined tag on the ctx } </code></pre> <p>4.把日志写到文件中</p> <p>上面日志是在控制台输出的,现在我们把日志写到文件中,修改<code>ZapInterceptor</code>方法。</p> <pre><code class="language-go hljs"><span class="hljs-keyword">import</span> ( grpc_zap <span class="hljs-string">"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"</span> <span class="hljs-string">"go.uber.org/zap"</span> <span class="hljs-string">"go.uber.org/zap/zapcore"</span> <span class="hljs-string">"gopkg.in/natefinch/lumberjack.v2"</span> ) <span class="hljs-comment">// ZapInterceptor 返回zap.logger实例(把日志写到文件中)</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ZapInterceptor</span><span class="hljs-params">()</span> *<span class="hljs-title">zap</span>.<span class="hljs-title">Logger</span></span> { w := zapcore.AddSync(&amp;lumberjack.Logger{ Filename: <span class="hljs-string">"log/debug.log"</span>, MaxSize: <span class="hljs-number">1024</span>, <span class="hljs-comment">//MB</span> LocalTime: <span class="hljs-literal">true</span>, }) config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder core := zapcore.NewCore( zapcore.NewJSONEncoder(config), w, zap.NewAtomicLevel(), ) logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(<span class="hljs-number">1</span>)) grpc_zap.ReplaceGrpcLogger(logger) <span class="hljs-keyword">return</span> logger } </code></pre> <h3 id="grpc_auth认证">grpc_auth认证</h3> <p>go-grpc-middleware中的grpc_auth默认使用<code>authorization</code>认证方式,以authorization为头部,包括<code>basic</code>, <code>bearer</code>形式等。下面介绍<code>bearer token</code>认证。<code>bearer</code>允许使用<code>access key</code>(如JSON Web Token (JWT))进行访问。</p> <p>1.新建grpc_auth服务端拦截器</p> <pre><code class="language-go hljs"><span class="hljs-comment">// TokenInfo 用户信息</span> <span class="hljs-keyword">type</span> TokenInfo <span class="hljs-keyword">struct</span> { ID <span class="hljs-keyword">string</span> Roles []<span class="hljs-keyword">string</span> } <span class="hljs-comment">// AuthInterceptor 认证拦截器,对以authorization为头部,形式为`bearer token`的Token进行验证</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">AuthInterceptor</span><span class="hljs-params">(ctx context.Context)</span> <span class="hljs-params">(context.Context, error)</span></span> { token, err := grpc_auth.AuthFromMD(ctx, <span class="hljs-string">"bearer"</span>) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err } tokenInfo, err := parseToken(token) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, grpc.Errorf(codes.Unauthenticated, <span class="hljs-string">" %v"</span>, err) } <span class="hljs-comment">//使用context.WithValue添加了值后,可以用Value(key)方法获取值</span> newCtx := context.WithValue(ctx, tokenInfo.ID, tokenInfo) <span class="hljs-comment">//log.Println(newCtx.Value(tokenInfo.ID))</span> <span class="hljs-keyword">return</span> newCtx, <span class="hljs-literal">nil</span> } <span class="hljs-comment">//解析token,并进行验证</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">parseToken</span><span class="hljs-params">(token <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(TokenInfo, error)</span></span> { <span class="hljs-keyword">var</span> tokenInfo TokenInfo <span class="hljs-keyword">if</span> token == <span class="hljs-string">"grpc.auth.token"</span> { tokenInfo.ID = <span class="hljs-string">"1"</span> tokenInfo.Roles = []<span class="hljs-keyword">string</span>{<span class="hljs-string">"admin"</span>} <span class="hljs-keyword">return</span> tokenInfo, <span class="hljs-literal">nil</span> } <span class="hljs-keyword">return</span> tokenInfo, errors.New(<span class="hljs-string">"Token无效: bearer "</span> + token) } <span class="hljs-comment">//从token中获取用户唯一标识</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">userClaimFromToken</span><span class="hljs-params">(tokenInfo TokenInfo)</span> <span class="hljs-title">string</span></span> { <span class="hljs-keyword">return</span> tokenInfo.ID } </code></pre> <p>代码中的对token进行简单验证并返回模拟数据。</p> <p>2.客户端请求添加<code>bearer token</code></p> <p>实现和上篇的自定义认证方法大同小异。gRPC 中默认定义了 <code>PerRPCCredentials</code>,是提供用于自定义认证的接口,它的作用是将所需的安全认证信息添加到每个RPC方法的上下文中。其包含 2 个方法:</p> <ul> <li><code>GetRequestMetadata</code>:获取当前请求认证所需的元数据</li> <li><code>RequireTransportSecurity</code>:是否需要基于 TLS 认证进行安全传输</li> </ul> <p>接下来我们实现这两个方法</p> <pre><code class="language-go hljs"><span class="hljs-comment">// Token token认证</span> <span class="hljs-keyword">type</span> Token <span class="hljs-keyword">struct</span> { Value <span class="hljs-keyword">string</span> } <span class="hljs-keyword">const</span> headerAuthorize <span class="hljs-keyword">string</span> = <span class="hljs-string">"authorization"</span> <span class="hljs-comment">// GetRequestMetadata 获取当前请求认证所需的元数据</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(t *Token)</span> <span class="hljs-title">GetRequestMetadata</span><span class="hljs-params">(ctx context.Context, uri ...<span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>, error)</span></span> { <span class="hljs-keyword">return</span> <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{headerAuthorize: t.Value}, <span class="hljs-literal">nil</span> } <span class="hljs-comment">// RequireTransportSecurity 是否需要基于 TLS 认证进行安全传输</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(t *Token)</span> <span class="hljs-title">RequireTransportSecurity</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span> } </code></pre> <blockquote> <p>注意:这里要以<code>authorization</code>为头部,和服务端对应。</p> </blockquote> <p>发送请求时添加token</p> <pre><code class="language-go hljs"><span class="hljs-comment">//从输入的证书文件中为客户端构造TLS凭证</span> creds, err := credentials.NewClientTLSFromFile(<span class="hljs-string">"../tls/server.pem"</span>, <span class="hljs-string">"go-grpc-example"</span>) <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { log.Fatalf(<span class="hljs-string">"Failed to create TLS credentials %v"</span>, err) } <span class="hljs-comment">//构建Token</span> token := auth.Token{ Value: <span class="hljs-string">"bearer grpc.auth.token"</span>, } <span class="hljs-comment">// 连接服务器</span> conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&amp;token)) </code></pre> <blockquote> <p>注意:Token中的Value的形式要以<code>bearer token值</code>形式。因为我们服务端使用了<code>bearer token</code>验证方式。</p> </blockquote> <p>3.把grpc_auth拦截器添加到服务端</p> <pre><code class="language-go hljs">grpcServer := grpc.NewServer(cred.TLSInterceptor(), grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( grpc_auth.StreamServerInterceptor(auth.AuthInterceptor), grpc_zap.StreamServerInterceptor(zap.ZapInterceptor()), )), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( grpc_auth.UnaryServerInterceptor(auth.AuthInterceptor), grpc_zap.UnaryServerInterceptor(zap.ZapInterceptor()), )), ) </code></pre> <p>写到这里,服务端都会拦截请求并进行<code>bearer token</code>验证,使用<code>bearer token</code>是规范了与<code>HTTP</code>请求的对接,毕竟gRPC也可以同时支持<code>HTTP</code>请求。</p> <h3 id="grpc_recovery恢复">grpc_recovery恢复</h3> <p>把gRPC中的<code>panic</code>转成<code>error</code>,从而恢复程序。</p> <p>1.直接把grpc_recovery拦截器添加到服务端</p> <p>最简单使用方式</p> <pre><code class="language-go hljs">grpcServer := grpc.NewServer(cred.TLSInterceptor(), grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( grpc_auth.StreamServerInterceptor(auth.AuthInterceptor), grpc_zap.StreamServerInterceptor(zap.ZapInterceptor()), grpc_recovery.StreamServerInterceptor, )), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( grpc_auth.UnaryServerInterceptor(auth.AuthInterceptor), grpc_zap.UnaryServerInterceptor(zap.ZapInterceptor()), grpc_recovery.UnaryServerInterceptor(), )), ) </code></pre> <p>2.自定义错误返回</p> <p>当<code>panic</code>时候,自定义错误码并返回。</p> <pre><code class="language-go hljs"><span class="hljs-comment">// RecoveryInterceptor panic时返回Unknown错误吗</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">RecoveryInterceptor</span><span class="hljs-params">()</span> <span class="hljs-title">grpc_recovery</span>.<span class="hljs-title">Option</span></span> { <span class="hljs-keyword">return</span> grpc_recovery.WithRecoveryHandler(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(p <span class="hljs-keyword">interface</span>{})</span> <span class="hljs-params">(err error)</span></span> { <span class="hljs-keyword">return</span> grpc.Errorf(codes.Unknown, <span class="hljs-string">"panic triggered: %v"</span>, p) }) } </code></pre> <p>添加grpc_recovery拦截器到服务端</p> <pre><code class="language-go hljs">grpcServer := grpc.NewServer(cred.TLSInterceptor(), grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( grpc_auth.StreamServerInterceptor(auth.AuthInterceptor), grpc_zap.StreamServerInterceptor(zap.ZapInterceptor()), grpc_recovery.StreamServerInterceptor(recovery.RecoveryInterceptor()), )), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( grpc_auth.UnaryServerInterceptor(auth.AuthInterceptor), grpc_zap.UnaryServerInterceptor(zap.ZapInterceptor()), grpc_recovery.UnaryServerInterceptor(recovery.RecoveryInterceptor()), )), ) </code></pre> <h3 id="总结">总结</h3> <p>本篇介绍了<code>go-grpc-middleware</code>中的<code>grpc_zap</code>、<code>grpc_auth</code>和<code>grpc_recovery</code>拦截器的使用。<code>go-grpc-middleware</code>中其他拦截器可参考<a href="https://github.com/grpc-ecosystem/go-grpc-middleware">GitHub</a>学习使用。</p> <p>教程源码地址:<a href="https://github.com/Bingjian-Zhu/go-grpc-example">https://github.com/Bingjian-Zhu/go-grpc-example</a></p> </div>

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

878 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传