gRPC如何做身份认证?

当上线gRPC服务到生产环境的时候,首先需要考虑的就是数据的安全,那么如何保证呢,下面以python为例,进行简单介绍。

RPC的认证方式

RPC服务一般在服务内网使用,不过也有存在于外网的情况,不论是哪种RPC服务,走http2.0或是其他基于TCP实现socket的协议,在部署到生产环境的时候还是需要考虑身份认证加密的,以此来保证数据的安全。

基于SSL/TLS的通道加密

通常身份认证机制是通过SSL/TLS对传输通道加密,以防止请求和响应消息中的敏感数据泄露。使用的场景主要有三种:

  • 后端微服务直接开放给端侧,例如手机app、tv、多屏等,没有统一的API Gateway/SLB做安全接入和认证
  • 后端微服务直接开放给DMZ部署的管理或者运维类Portal
  • 后端微服务直接开放给第三方合作伙伴/渠道

除了上述常用的跨网络场景之外,对于一些安全等级要求比较高的业务场景,即便是内网通信,只要跨主机/VM/容器等,都强制要求对传输通道进行加密。在该场景下,即便只存在内网各模块的RPC调用,仍然需要进行加密。
针对敏感数据的单独加密
有些RPC调用并不涉及敏感数据的传输,或者敏感字段占比较低,为了最大程度的提升吞吐量,降低调用时延,通常会采用HTTP/TCP+敏感字段单独加密的方式。既保证了敏感信息的传输安全,同时也降低了采用SSL/TLS加密通道带来的性能损耗,对于JDK原生的SSL类库,这种性能提升尤为明显。

采用该方案主要有两个缺点:

  • 对敏感信息的识别可能存在偏差,容易遗漏或者过度保护,需要解读数据和隐私保护方面的法律法规,而且不同国家对敏感数据的定义也不同,这会为识别带来很多困难。
  • 接口升级时容易遗漏,例如开发新增字段,忘记识别是否未敏感数据。

gRPC认证的的具体流程

对于gRPC,SSL/TLS协议也是基本的身份加密认证方法,SSL/TLS协议采用公钥加密,客户端向服务端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
SSL/TLS分为单向认证和双向认证,在实际业务中,单向认证使用较多,即客户端认证服务端,服务端不认证客户端,认证流程如下:

  • 客户端向服务端传送客户端SSL协议的版本号,支持的加密算法种类,产生的随机数,以及其他可选信息
  • 服务端返回握手应答,向客户端传送确认SSL协议的版本号、加密算法的种类、随机数以及其他相关信息
  • 服务端向客户端发送自己的公钥
  • 客户端对服务端的证书进行认证,服务端的合法性校验包括:证书是否过期、发行服务器证书的CA是否可靠、发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”、服务器证书上的域名是否和服务器的实际域名相匹配等
  • 客户端随机生成一个用于后面通讯的“对称密码”,然后用服务端的公钥对其加密,将加密后的“预主密码”传给服务端
  • 服务端用自己的私钥解开加密的“预主密码”,然后执行一系列步骤来产生主密码
  • 客户端向服务端发出信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知服务器客户端的握手过程结束
  • 服务端向客户端发出信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知客户端服务端的握手过程结束
  • SSL的握手部分结束,SSL安全通道简历,客户端和服务端开始使用相同的对称密钥对数据进行加密,然后通过socket进行传输

具体示例

生成证书

1
openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 3650 -out server.crt

在执行生成证书的过程中,需要填入Country NameState or Province NameLocality NameOrganization NameOrganization Unit NameCommon NameEmail Address等等,这些可以按需填入,或者留空也行。

注:其中的Common Name支持在客户端连接的时候指定连接的名字,可以自己定义之后填上,否则留空的话可能自动获取不到

代码实现

下面模拟一个任务的grpc调用,服务端将流式响应任务的进度信息
proto定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "proto3";

service Task {
rpc StartScan(TaskInfoRequest) returns(stream CommonResponse);
}

message TaskInfoRequest {
string task_id = 1;
string data = 2;
}

message CommonResponse {
int32 code = 1;
string msg = 2;
string progress = 3;
}

注:执行下述命令生成python的proto序列化协议源代码
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. task.proto

服务端代码server.py如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import random
import time
from concurrent import futures

import grpc

from proto import task_pb2, task_pb2_grpc


class TaskServicer(task_pb2_grpc.TaskServicer):

def StartScan(self, request, context):
progress = 0
while progress < 100:
resp = {
"code": 200,
"msg": "succeed",
"progress": str(progress)
}
feature = task_pb2.CommonResponse(**resp)
tmp = random.randint(1, 10)
progress += tmp
time.sleep(5)
yield feature


def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=[
# ("grpc.max_send_message_length", 100 * 1024 * 1024),
# ("grpc.max_receive_message_length", 100 * 1024 * 1024)
])
task_pb2_grpc.add_TaskServicer_to_server(TaskServicer(), server)

with open("./server.key", "rb") as f:
private_key = f.read()
with open("./server.crt", "rb") as f:
certificate_chain = f.read()

server_credentials = grpc.ssl_server_credentials(((private_key, certificate_chain), ))
server.add_secure_port("[::]:50051", server_credentials)

server.start()
print("gRPC服务端已开启,端口为50051...")
server.wait_for_termination()


if __name__ == '__main__':
serve()

客户端代码client.py如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import grpc

from proto import task_pb2, task_pb2_grpc


def get_progress(*host):
with open("server.crt", "rb") as f:
trusted_certs = f.read()
credentials = grpc.ssl_channel_credentials(root_certificates=trusted_certs)

with grpc.secure_channel("localhost:50001", credentials) as channel:
stub = task_pb2_grpc.TaskStub(channel)
req = {
"task_id": "24dsad",
"data": "12"
}
features = stub.StartScan(task_pb2.TaskInfoRequest(**req))
for feature in features:
print(feature.progress)


def run():
with grpc.insecure_channel("localhost:50001") as channel:
stub = task_pb2_grpc.TaskStub(channel)
req = {
"task_id": "1",
"data": "my test"
}
features = stub.StartScan(task_pb2.TaskInfoRequest(**req))
for feature in features:
print(feature.progress)


if __name__ == '__main__':
run()

按照上述实现,即完成了gRPC的认证加密。

抓包分析

运行上述的代码,进行抓包如下,确认gRPC通信已通过SSL/TLS加密认证。