一. 基于 SSL/TLS 的通道加密
当存在跨网络边界的 RPC 调用时,往往需要通过 TLS/SSL 对传输通道进行加密,以防止请求和响应消息中的敏感数据泄漏。跨网络边界调用场景主要有三种:1. 后端微服务直接开放给端侧,例如手机 App、TV、多屏等,没有统一的 API Gateway/SLB 做安全接入和认证;
2. 后端微服务直接开放给 DMZ 部署的管理或者运维类 Portal;
3. 后端微服务直接开放给第三方合作伙伴 / 渠道。
除了跨网络之外,对于一些安全等级要求比较高的业务场景,即便是内网通信,只要跨主机 /VM/ 容器通信,都强制要求对传输通道进行加密。在该场景下,即便只存在内网各模块的 RPC 调用,仍然需要做 SSL/TLS。
目前使用最广的 SSL/TLS 工具 / 类库就是 OpenSSL,它是为网络通信提供安全及数据完整性的一种安全协议,囊括了主要的密码算法、常用的密钥和证书封装管理功能以及 SSL 协议。
多数 SSL 加密网站是用名为 OpenSSL 的开源软件包,由于这也是互联网应用最广泛的安全传输方法,被网银、在线支付、电商网站、门户网站、电子邮件等重要网站广泛使用。
二. 认证和鉴权
RPC 的认证和鉴权机制主要包含两点:1. 认证:对调用方身份进行识别,防止非法调用;
2. 鉴权:对调用方的权限进行校验,防止越权调用。
事实上,并非所有的 RPC 调用都必须要做认证和鉴权,例如通过 API Gateway 网关接入的流量,已经在网关侧做了鉴权和身份认证,对来自网关的流量 RPC 服务端就不需要重复鉴权。
另外,一些对安全性要求不太高的场景,可以只做认证而不做细粒度的鉴权。
三. gRPC 安全机制
谷歌提供了可扩展的安全认证机制,以满足不同业务场景需求,它提供的授权机制主要有四类:
1. 通道凭证:默认提供了基于 HTTP/2 的 TLS,对客户端和服务端交换的所有数据进行加密传输;2. 调用凭证:被附加在每次 RPC 调用上,通过 Credentials 将认证信息附加到消息头中,由服务端做授权认证;
3. 组合凭证:将一个频道凭证和一个调用凭证关联起来创建一个新的频道凭证,在这个频道上的每次调用会发送组合的调用凭证来作为授权数据,最典型的场景就是使用 HTTP S 来传输 Access Token;
4. Google 的 OAuth 2.0:gRPC 内置的谷歌的 OAuth 2.0 认证机制,通过 gRPC 访问 Google API 时,使用 Service Accounts 密钥作为凭证获取授权令牌。
四. gRPC 的 TLS 策略
gRPC 基于 HTTP/2 协议,默认会开启 SSL/TLS。gRPC 的 TLS 实现有两种策略:
1. 基于 OpenSSL 的 TLS2. 基于 Jetty ALPN/NPN 的 TLS
对于非安卓的后端 Java 应用,gRPC 强烈推荐使用 OpenSSL,原因如下:1. 性能更高:基于 OpenSSL 的 gRPC 调用比使用 JDK GCM 的性能高 10 倍以上;
2. 密码算法更丰富:OpenSSL 支持的密码算法比 JDK SSL 提供的更丰富,特别是 HTTP/2 协议使用的加密算法;
OpenSSL 支持 ALPN 回退到 NPN;
3. 不需要根据 JDK 的版本升级配套升级 ALPN 类库(Jetty 的 ALPN 版本与 JDK 特定版本配套使用)。
gRPC 的 HTTP/2 和 TLS 基于 Netty 框架实现,如果使用 OpenSSL,则需要依赖 Netty 的 netty-tcnative。
Netty 的 OpenSSL 有两种实现机制:Dynamic linked 和 Statically Linked。在开发和测试环境中,建议使用 Statically Linked 的方式(netty-tcnative-boringssl-static),它提供了对 ALPN 的支持以及 HTTP/2 需要的密码算法,不需要额外再集成 Jetty 的 ALPN 类库。从 1.1.33.Fork16 版本开始支持所有的操作系统,可以实现跨平台运行。
对于生产环境,则建议使用 Dynamic linked 的方式,原因如下:
很多场景下需要升级 OpenSSL 的版本或者打安全补丁,如果使用动态链接方式(例如 apt-ge),则应用软件不需要级联升级;
对于一些紧急的 OpenSSL 安全补丁,如果采用 Statically Linked 的方式,需要等待 Netty 社区提供新的静态编译补丁版本,可能会存在一定的滞后性。
netty-tcnative-boringssl-static 的 gradle 配置如下:
//ssl
compile 'io.netty:netty-tcnative-boringssl-static:2.0.8.Final'
五. 使用样例参考
服务端配置demo:
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;
/**
* @describe GRpcServer default ssl demo
* @author zhikai.chen
* @date 2018年5月7日 下午3:55:10
*/
public class GRpcServerDefaultSSL {
private static final Logger logger = LoggerFactory.getLogger(GRpcServerDefaultSSL.class);
private Server server;
private void start() throws IOException, CertificateException {
/* The port on which the server should run */
int port = 50051;
SelfSignedCertificate ssc = new SelfSignedCertificate();
//底层默认使用netty4.1的nio同步非阻塞模型
server = ServerBuilder.forPort(port).useTransportSecurity(ssc.certificate(), ssc.privateKey())
.addService(new PhoneServiceImp())
.build()
.start();
logger.info("Server started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
GRpcServerDefaultSSL.this.stop();
System.err.println("*** server shut down");
}
});
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
/**
* Await termination on the main thread since the grpc library uses daemon
* threads.
*/
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
/**
* Main launches the server from the command line.
* @throws CertificateException
*/
public static void main(String[] args) throws IOException, InterruptedException, CertificateException {
final GRpcServerDefaultSSL server = new GRpcServerDefaultSSL();
server.start();
server.blockUntilShutdown();
}
}
客户端配置demo:
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ylifegroup.protobuf.PhoneServiceGrpc;
import com.ylifegroup.protobuf.Phonebook.AddPhoneToUserRequest;
import com.ylifegroup.protobuf.Phonebook.AddPhoneToUserResponse;
import com.ylifegroup.protobuf.Phonebook.PhoneType;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
/**
* @describe GRpcClient Block demo
* @author zhikai.chen
* @date 2018年5月7日 下午4:00:58
*/
public class GRpcClientBlockSSL {
private static final Logger logger = LoggerFactory.getLogger(GRpcClientBlockSSL.class);
private final ManagedChannel channel;
private final PhoneServiceGrpc.PhoneServiceBlockingStub blockingStub;
/** Construct client connecting to gRPC server at {@code host:port}.
* @throws SSLException */
public GRpcClientBlockSSL(String host, int port) throws SSLException {
this(NettyChannelBuilder.forAddress(host, port).sslContext(
GrpcSslContexts.forClient().
ciphers(Http2SecurityUtil.CIPHERS,
SupportedCipherSuiteFilter.INSTANCE).
trustManager(InsecureTrustManagerFactory.INSTANCE).build()));
}
/** Construct client connecting to gRPC server at {@code host:port}.
* @throws SSLException */
public GRpcClientBlockSSL(String host, int port,boolean value) throws SSLException {
this(NettyChannelBuilder.forAddress(host, port).sslContext(SslContextBuilder.forClient()
.sslProvider(OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build()));
}
/** Construct client for accessing RouteGuide server using the existing channel. */
GRpcClientBlockSSL(ManagedChannelBuilder<?> channelBuilder) {
channel = channelBuilder.build();
blockingStub = PhoneServiceGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
/** add phone to user. */
public void addPhoneToUser(int uid, PhoneType phoneType, String phoneNubmer) {
logger.info("Will try to add phone to user " + uid);
AddPhoneToUserRequest request = AddPhoneToUserRequest.newBuilder().setUid(uid).setPhoneType(phoneType)
.setPhoneNumber(phoneNubmer).build();
AddPhoneToUserResponse response;
try {
response = blockingStub.addPhoneToUser(request);
} catch (StatusRuntimeException e) {
logger.warn("RPC failed: {0}", e.getStatus());
return;
}
logger.info("Result: " + response.getResult());
}
public static void main(String[] args) throws Exception {
GRpcClientBlockSSL client = new GRpcClientBlockSSL("localhost", 50051);
try {
client.addPhoneToUser(1, PhoneType.WORK, "13888888888");
client = new GRpcClientBlockSSL("localhost", 50051,true);
client.addPhoneToUser(1, PhoneType.WORK, "13888888888");
} finally {
client.shutdown();
}
}
}
运行效果图参考: