本文包含 Node.JS 示例,但对其他语言也有帮助,让我们开始吧。
gRPC 连接类型,可以使用三种类型的 gRPC 连接:
1.不安全——所有数据未经加密传输。
2.服务器端 TLS — 类似浏览器的加密,其中只有服务器向客户端提供 TLS 证书。
3.相互 TLS — 最安全,服务器和客户端都相互提供证书。
创建自签名证书
如果没有CA 的证书或想在本地主机上使用 TLS,则这是一个可选步骤。
请注意,在生产环境中,大多数建议选择 CA 的证书以提高安全性,例如,可以从 Let’s Encrypt 获取免费证书。
rm *.pem
rm *.srl
rm *.cnf
# 1. Generate CA's private key and self-signed certificate
openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Test Org/OU=Test/CN=*.test/emailAddress=test@gmail.com"
echo "CA's self-signed certificate"
openssl x509 -in ca-cert.pem -noout -text
# 2. Generate web server's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=FR/ST=Ile de France/L=Paris/O=Server TLS/OU=Server/CN=*.tls/emailAddress=tls@gmail.com"
# Remember that when we develop on localhost, It’s important to add the IP:0.0.0.0 as an Subject Alternative Name (SAN) extension to the certificate.
echo "subjectAltName=DNS:*.tls,DNS:localhost,IP:0.0.0.0" > server-ext.cnf
# Or you can use localhost DNS and grpc.ssl_target_name_override variable
# echo "subjectAltName=DNS:localhost" > server-ext.cnf
# 3. Use CA's private key to sign web server's CSR and get back the signed certificate
openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf
echo "Server's signed certificate"
openssl x509 -in server-cert.pem -noout -text
# 4. Generate client's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=FR/ST=Alsace/L=Strasbourg/O=PC Client/OU=Computer/CN=*.client.com/emailAddress=client@gmail.com"
# Remember that when we develop on localhost, It’s important to add the IP:0.0.0.0 as an Subject Alternative Name (SAN) extension to the certificate.
echo "subjectAltName=DNS:*.client.com,IP:0.0.0.0" > client-ext.cnf
# 5. Use CA's private key to sign client's CSR and get back the signed certificate
openssl x509 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile client-ext.cnf
echo "Client's signed certificate"
openssl x509 -in client-cert.pem -noout -text
准备工作
让我们从实现gRPC 服务开始。首先,我们将定义服务协议缓冲区文件。
syntax = "proto3";
package tls_service.v1;
message SimpleMessage {
string id = 1;
}
service TLSService {
rpc Unary(SimpleMessage) returns (SimpleMessage);
}
其次,我们需要生成类型、服务和客户端定义。对于 TypeScript,我更喜欢使用 ts-proto,但你可以根据语言选择任何你喜欢的工具。
protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=env=node,outputServices=grpc-js --ts_proto_out=./src/generated ./proto/tls_service.proto
现在可以在Node.js上实现server程序了。
gRPC
我们将为服务器端和客户端使用官方的@grpc/grpc-js包。
Server-Side TLS
服务器端 TLS 只需要服务器证书及其私钥。
Server
import { Server, ServerCredentials } from '@grpc/grpc-js';
import * as fs from 'fs';
import * as path from 'path';
import { TLSServiceServer, TLSServiceService } from './generated/proto/tls_service';
const TLSService: TLSServiceServer = {
unary(call, callback) {
callback(null, call.request);
},
};
function getServerCredentials(): ServerCredentials {
const serverCert = fs.readFileSync(path.resolve(__dirname, '../certs/server-cert.pem'));
const serverKey = fs.readFileSync(path.resolve(__dirname, '../certs/server-key.pem'));
const serverCredentials = ServerCredentials.createSsl(
null,
[
{
cert_chain: serverCert,
private_key: serverKey,
},
],
false
);
return serverCredentials;
}
function main() {
const server = new Server();
const serverCredentials = getServerCredentials();
server.addService(TLSServiceService, TLSService);
server.bindAsync('0.0.0.0:4000', serverCredentials, () => {
server.start();
console.log('gRPC server started on 0.0.0.0:4000');
});
}
main();
Client
现在我们可以实现客户端。这里要注意,如果你的TLS证书是用CA签发的(不是Self-Signed),就不需要在客户端提供这个证书; 它应该自动工作。
import { ChannelCredentials } from '@grpc/grpc-js';
import * as fs from 'fs';
import * as path from 'path';
import { TLSServiceClient } from './generated/proto/tls_service';
function getChannelCredentials(): ChannelCredentials {
const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem'));
// If you use CA root certificate
// const channelCredentials = ChannelCredentials.createSsl();
// If you use Self-Signed root certificate you need to provide it
const channelCredentials = ChannelCredentials.createSsl(rootCert);
return channelCredentials;
}
function main() {
const credentials = getChannelCredentials();
const client = new TLSServiceClient('0.0.0.0:4000', credentials);
client.unary({ id: 'test' }, (error, response) => {
// eslint-disable-next-line no-console
console.log('response: ', response);
});
}
main();
Mutual TLS
Mutual TLS 需要根证书、服务器证书及其私钥。根证书将在此处用于检查客户端证书是否已签名,并且服务器可以信任客户端。
Server
与Server-Side部分一样,仅更改了 getServerCredentials 功能。
function getServerCredentials(): ServerCredentials {
const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem'));
const serverCert = fs.readFileSync(path.resolve(__dirname, '../certs/server-cert.pem'));
const serverKey = fs.readFileSync(path.resolve(__dirname, '../certs/server-key.pem'));
const serverCredentials = ServerCredentials.createSsl(
rootCert,
[
{
cert_chain: serverCert,
private_key: serverKey,
},
],
true
);
return serverCredentials;
}
Client
与Server-Side部分一样,仅更改了 getChannelCredentials 函数。
function getChannelCredentials(): ChannelCredentials {
const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem'));
const clientCert = fs.readFileSync(path.resolve(__dirname, '../certs/client-cert.pem'));
const clientKey = fs.readFileSync(path.resolve(__dirname, '../certs/client-key.pem'));
const channelCredentials = ChannelCredentials.createSsl(rootCert, clientKey, clientCert);
return channelCredentials;
}
覆盖 SSL 目标名称
@grpc/grpc-js
包还提供了一些可以设置的有用选项。grpc.ssl_target_name_override
— 当代理后面的实际服务器和 CN 不匹配时,这很有帮助。要设置通道选项,需要将它们传递给客户端构造函数的第三个参数。
gRPC-Web
言而总之
你应该知道的事情:
1.工作方案:Client ↔ Proxy [HTTP(S) gRPC-Web] ↔ Server (gRPC)
2.有两种实现——官方 gRPC-Web 和@improbable-eng/grpc-web。
3.目前 gRPC-Web 仅支持通过 HTTP(S) 的一元请求和服务器流请求。
此外,@improbable-eng/grpc-web 支持客户端和双向流式传输,带有实验性的 websocket 传输。 这不是 gRPC-Web 规范的一部分,不建议用于生产。
4.有两个代理——来自官方 gRPC-Web 的带有 gRPC-Web 过滤器的 Envoy 和来自 @improbable-eng 的 grpcwebproxy。
5.可以将任一客户端与任一代理一起使用。
6.客户端有不同的通信传输。官方 gRPC-web 仅支持 XMLHttpRequest。@improbable-eng/grpc-web 还支持 Fetch(如果可用就使用它)并且可以使用自定义传输进行扩展,例如 Node.js。
@improbable-eng/grpc-web 包在过去 10 个月没有更新,所以如果你没有计划使用 gRPC-Web 自定义传输或在 Node.js 环境中,我认为你应该选择官方 gRPC-Web client。我在官方包中创建了功能请求以添加 Node.js support 链接
在这个例子中,我们将使用在 docker 中运行的 Envoy 代理。
Server-Side TLS
Server
我们需要在代理后面启动我们的 gRPC 服务。 因此无需更改,只需使用之前讨论的服务器端 TLS 启动服务即可。之后,需要设置 envoy 并启动它。
services:
envoy-server:
image: envoyproxy/envoy:v1.22.0
ports:
- 8080:8080
volumes:
- ./envoy-server.yaml:/etc/envoy/envoy.yaml:ro
- ./certs/server-cert.pem:/etc/server-cert.pem
- ./certs/server-key.pem:/etc/server-key.pem
docker-compose.yaml
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: simple_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
# https://www.envoyproxy.io/docs/envoy/v1.15.0/api-v3/extensions/transport_sockets/tls/v3/tls.proto#extensions-transport-sockets-tls-v3-downstreamtlscontext
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain:
# Certificate must be PEM-encoded
filename: /etc/server-cert.pem
private_key:
filename: /etc/server-key.pem
clusters:
- name: simple_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
# address: host.docker.internal - for macOS
# address: 0.0.0.0 - for others
address: host.docker.internal
port_value: 4000
# http2_protocol_options: {} # Force HTTP/2
# Your grpc server communicates over TLS. You must configure the transport
# socket. If you care about the overhead, you should configure the grpc
# server to listen without TLS. If you need to listen to grpc-web and grpc
# over HTTP/2 both you can also proxy your TCP traffic with the envoy.
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
envoy-server.yaml
docker-compose up envoy-server
这样带有Server-Side TLS的 gRPC-Web 代理在 https://0.0.0.0:8080 就可用了。
Mutual TLS
Server
这里的 hack 是使用Server-Side TLS启动 gRPC 服务,但在envoy侧需要检查由受信任的 CA 签名的客户端证书。
services:
envoy-mutual:
image: envoyproxy/envoy:v1.22.0
ports:
- 8080:8080
volumes:
- ./envoy-mutual.yaml:/etc/envoy/envoy.yaml:ro
- ./certs/ca-cert.pem:/etc/ca-cert.pem
- ./certs/server-cert.pem:/etc/server-cert.pem
- ./certs/server-key.pem:/etc/server-key.pem
docker-compose.yaml
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: simple_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
# https://www.envoyproxy.io/docs/envoy/v1.15.0/api-v3/extensions/transport_sockets/tls/v3/tls.proto#extensions-transport-sockets-tls-v3-downstreamtlscontext
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
tls_certificates:
- certificate_chain:
# Certificate must be PEM-encoded
filename: /etc/server-cert.pem
private_key:
filename: /etc/server-key.pem
validation_context:
only_verify_leaf_cert_crl: true
trusted_ca:
filename: /etc/ca-cert.pem
clusters:
- name: simple_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
# address: host.docker.internal - for macOS
# address: 0.0.0.0 - for others
address: host.docker.internal
port_value: 4000
# http2_protocol_options: {} # Force HTTP/2
# Your grpc server communicates over TLS. You must configure the transport
# socket. If you care about the overhead, you should configure the grpc
# server to listen without TLS. If you need to listen to grpc-web and grpc
# over HTTP/2 both you can also proxy your TCP traffic with the envoy.
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
validation_context:
trusted_ca:
filename: /etc/ca-cert.pem
envoy-mutual.yaml
docker-compose up envoy-mutual
这样带有 Mutual TLS 的 gRPC-Web 代理在 https://0.0.0.0:8080 就可用了。
测试 gRPC 和 gRPC-Web 请求
最近我发布了一个名为 ezy 的多平台桌面 gRPC / gRPC-Web 客户端。 我每天都在使用 gRPC,但没有用于 gRPC 测试的功能齐全的 UI/UX 完美客户端,所以我尝试创建一个。
该客户端具有对 gRPC / gRPC-Web 的全面支持。你可以尝试一下。