grpc java 安全性设计SSL/TLS

一.  基于 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 的 TLS

2.  基于 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();
        }
    }

}


运行效果图参考:





  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值