<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(&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(&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>
有疑问加站长微信联系(非本文作者))