安全认证
认证
gRPC 被设计成可以利用插件的形式支持多种授权机制。本文档对多种支持的授权机制提供了一个概览,并且用例子来论述对应API,最后就其扩展性作了讨论。
支持的授权机制
SSL/TLS
gRP 集成 SSL/TLS 并对服务端授权所使用的 SSL/TLS 进行了改良,对客户端和服务端交换的所有数据进行了加密。对客户端来讲提供了可选的机制提供凭证来获得共同的授权。
OAuth 2.0
gRPC 提供通用的机制(后续进行描述)来对请求和应答附加基于元数据的凭证。当通过 gRPC 访问 Google API 时,会为一定的授权流程提供额外的获取访问令牌的支持,这将通过以下代码例子进行展示。
警告:Google OAuth2 凭证应该仅用于连接 Google 的服务。把 Google 对应的 OAuth2 令牌发往非 Google 的服务会导致令牌被窃取用作冒充客户端来访问 Google 的服务。
API
为了减少复杂性和将混乱最小化, gRPC 以一个统一的凭证对象来进行工作。
凭证可以是以下两类:
- 频道凭证, 被附加在 频道上, 比如 SSL 凭证。
- 调用凭证, 被附加在调用上(或者 C++ 里的 客户端上下文)。 凭证可以用组合频道凭证来进行组合。一个组合频道凭证可以将一个频道凭证和一个调用凭证关联创建一个新的频道凭证。结果在这个频道上的每次调用会发送组合的调用凭证来作为授权数据。 例如,一各频道凭证可以由一个Ssl 凭证和一个访问令牌凭证生成。结果是在这个频道上的每次调用都会发送对应的访问令牌。 调用凭证可以用 组合凭证来组装。组装后的 调用凭证应用到一个客户端上下文里,将触发发送这两个调用凭证的授权数据。
服务端认证加密使用的 SSL/TLS
这是个最简单的认证场景:一个客户端仅仅想认证服务器并且加密所有数据。
// Create a default SSL ChannelCredentials object.
auto channel_creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
// Create a channel using the credentials created in the previous step.
auto channel = grpc::CreateChannel(server_name, creds);
// Create a stub on the channel.
std::unique_ptrGreeter::Stub stub(Greeter::NewStub(channel));
// Make actual RPC calls on the stub.
grpc::Status s = stub->sayHello(&context, *request, response);
对于高级的用例比如改变根 CA 或使用客户端证书,可以在发送给工厂方法的 SslCredentialsOptions 参数里的相应选项进行设置。
通过 Google 进行认证
gRPC应用可以使用一个简单的API来创建一个可以工作在不同部署场景下的凭证。
auto creds = grpc::GoogleDefaultCredentials();
// Create a channel, stub and make RPC calls (same as in the previous example)
auto channel = grpc::CreateChannel(server_name, creds);
std::unique_ptrGreeter::Stub stub(Greeter::NewStub(channel));
grpc::Status s = stub->sayHello(&context, *request, response);
这个应用使用的频道凭证对象就像 Google 计算引擎
(GCE)里运行的应用一样使用服务账号。在前面的案例里,服务账号的密钥从环境变量 GOOGLE_APPLICATION_CREDENTIALS 对应的文件里加载。这些密钥被用来生成承载令牌附加在在相应频道的每次 RPC 调用里。
对于 GCE 里运行的应用,可以在虚拟机设置的时候为其配置一个默认的服务账号和相应的 OAuth2 范围。在运行时,这个凭证被用来与认证系统通讯来获取 OAuth2 访问令牌并且把令牌用作在相应的频道上的 RPC 调用。
扩展 gRPC 支持其他的认证机制
相应的凭证插件 API 允许开发者开发自己的凭证插件。
MetadataCredentialsPlugin 抽象类包含需要被开发者创建的子类实现的纯虚方法 GetMetadata。
MetadataCredentialsFromPlugin 方法可以从 MetadataCredentialsPlugin 创建一个 调用者凭证。 这类有个简单的凭证插件例子,是通过在自定义头了设置一个认证票据。
class MyCustomAuthenticator : public grpc::MetadataCredentialsPlugin {
public:
MyCustomAuthenticator(const grpc::string& ticket) : ticket_(ticket) {}
grpc::Status GetMetadata(
grpc::string_ref service_url, grpc::string_ref method_name,
const grpc::AuthContext& channel_auth_context,
std::multimap<grpc::string, grpc::string>* metadata) override {
metadata->insert(std::make_pair("x-custom-auth-ticket", ticket_));
return grpc::Status::OK;
}
private:
grpc::string ticket_;
};
auto call_creds = grpc::MetadataCredentialsFromPlugin(
std::unique_ptr<grpc::MetadataCredentialsPlugin>(
new MyCustomAuthenticator("super-secret-ticket")));
更深层次的集成可以通过在将 gRPC 的凭证实现以插件的形式集成进核心层。gRPC 内部也允许用其他加密机制来替换 SSL/TLS 。
例子
这些授权机制将会在所有 gRPC 支持的语言里提供。以下的一些节里展示了上文提到的认证和授权在每种语言里如何实现:很快将会推出更多语言的支持。
- 通过 SSL/TLS 进行服务端授权和加密(Ruby)
# Base case - No encryption
stub = Helloworld::Greeter::Stub.new('localhost:50051', :this_channel_is_insecure)
...
# With server authentication SSL/TLS
creds = GRPC::Core::Credentials.new(load_certs) # load_certs typically loads a CA roots file
stub = Helloworld::Greeter::Stub.new('localhost:50051', creds)
不同语言可参考-http://doc.oschina.net/grpc?t=58010
通讯协议
HTTP2 协议上的 gRPC
本文档作为 gRPC 在 HTTP2 草案17框架上的实现的详细描述,假设你已经熟悉 HTTP2 的规范。产品规则采用的是ABNF(http://tools.ietf.org/html/rfc5234) 语法
大纲
以下是 gRPC 请求和应答消息流中一般的消息顺序:
- 请求 → 请求报头 *有定界符的消息 EOS
- 应答 → 应答报头 *有定界符的消息 EOS
- 应答 → (应答报头 *有定界符的消息 跟踪信息) / 仅仅跟踪时
请求
请求 → 请求报头 *界定的消息 EOS
请求报头是通过报头+联系帧方式以 HTTP2 报头来发送的。
-
请求报头 → 调用定义 *自定义元数据
-
调用定义 → 方法模式路径TE [授权] [超时] [内容类型] [消息类型] [消息编码] [接受消息类型] [用户代理]
-
方法 → “:method POST”
-
模式 → “:scheme ” (“http” / “https”)
-
路径 → “:path” {开放的 API 对应的方法路径}
-
Authority → “:authority” {授权的对应的虚拟主机域名}
-
TE → “te” “trailers” # 用来检测不兼容的代理
-
超时 → “grpc-timeout” 超时时间值 超时时间单位
-
超时时间值 → {至少8位数字正整数的 ASCII 码字符串}
-
超时时间单位 → 时 / 分 / 秒 / 毫秒 / 微秒 / 纳秒
-
时 → “H”
-
分 → “M”
-
秒 → “S”
-
毫秒 → “m”
-
微秒 → “u”
-
纳秒 → “n”
-
内容类型 → “content-type” “application/grpc” [(“+proto” / “+json” / {自定义})]
-
内容编码 → “gzip” / “deflate” / “snappy” / {自定义}
-
消息编码 → “grpc-encoding” Content-Coding
-
接受消息编码 → “grpc-accept-encoding” Content-Coding *("," Content-Coding)
-
用户代理 → “user-agent” {结构化的用户代理字符串}
-
消息类型 → “grpc-message-type” {消息模式的类型名}
-
自定义数据 → 二进制报头 / ASCII 码报头
-
二进制报头 → {以“-bin”结尾小写的报头名称的 ASCII 码 } {以 base64 进行编码的值}
-
ASCII 码报头 → {小写报头名称的 ASCII 码} {值}
-
HTTP2 需要一个在其他报头之前以“:”开始的保留报头。额外的实现应该在保留报头后面马上传送超时信息,并且应该在发送自定义元数据前发送调用定义报头。
-
如果超时信息被遗漏,服务端会认为是无限时长的超时。客户端实现可以根据发布需要自由地发送一个默认最小超时时间。
-
自定义元数据是应用层定义的任意的键值对集合。除了 HTTP2 报头部总长度的传输限制外,唯一的约束就是以“grpc-”开始的报头名称是为将来使用保留的。
注意 HTTP2 并不允许随意使用字节序列来作为报头值,所以二进制的报头值必须使用 Base64 来编码,参见https://tools.ietf.org/html/rfc4648#section-4。 实现必须接受填充的和非填充的值,并且发出非填充的值。应用以“-bin”结尾的名称来定义二进制报头。运行时库在报头被发送和接收时,用这个后缀来检测二进制报头并且正确地在报头被发送和接收时进行 Base64 编码和解码。
界定的消息的重复序列通过数据帧来进行传输。
- 界定的消息 → 压缩标志 消息长度 消息
- 压缩标志 → 0 / 1 # 编码为 1 byte 的无符号整数
- 消息长度 → {消息长度} # 编码为 4 byte 的无符号整数
- 消息 → *{二进制字节}
压缩标志 值为1 表示消息的二进制序列通过消息编码报头声明的机制进行压缩,为0表示消息的字节码没有进行编码。压缩上下文不在消息编辑间维护,声明必须为流中的每个消息创建一个新的上下文。假如 压缩标志 被遗漏了,那么压缩标志 必须为0。
对请求来讲,EOS (end-of-stream)以最后接收到的数据帧出现 END_STREAM 标志为准。
在请求流需要关闭但是没有数据继续发送的情况下,代码必须发送包含这个标志的空数据帧。
应答
- 应答 → (应答报头 界定的消息 跟踪信息) / 仅仅跟踪
- 应答报头 → HTTP 状态 [消息编码] [消息接受编码] 内容类型 *自定义元数据
- 仅仅跟踪 → HTTP 状态 内容类型 跟踪消息
- 跟踪消息 → 状态 [状态消息] *自定义元数据
- HTTP状态 → “:status 200”
- 状态 → “grpc-status” <状态码的 ASCII 字符串>
- 状态消息 → “grpc-message” <状态描述文本对应的 ASCII 字符串>
应答报头 和 仅仅跟踪 分别在一个HTTP2报头帧块里发送。大多数应答期望既有报头又有跟踪消息,但是调用允许仅仅跟踪生成一个立即的错误。假如状态码是 OK 的话,则必须在跟踪消息里发送状态。
对于应答来讲,通过在最后一个接收的包含跟踪信息的报头帧里提供一个 END_STREAM 标志来表明流结束。
实现应当会让中断的部署在应答里发送一个非200的HTTP状态码和一系列非GRPC内容类型并且省略状态和状态消息。
当发生这种情况时实现应当合成状态和状态消息来扩散到应用层。
例子
单项调用HTTP2帧序列例子
请求
HEADERS (flags = END_HEADERS)
:method = POST
:scheme = http
:path = /google.pubsub.v2.PublisherService/CreateTopic
:authority = pubsub.googleapis.com
grpc-timeout = 1S
content-type = application/grpc+proto
grpc-encoding = gzip
authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v
DATA (flags = END_STREAM)
<Delimited Message>
应答
HEADERS (flags = END_HEADERS)
:status = 200
grpc-encoding = gzip
DATA
<Delimited Message>
HEADERS (flags = END_STREAM, END_HEADERS)
grpc-status = 0 # OK
trace-proto-bin = jher831yy13JHy3hc
用户代理
当协议不需要一个用户代理时,建议客户端提供一个结构化的用户代理字符串来对要调用的库、版本和平台提供一个基本的描述来帮助在异质的环境里进行问题诊断。库开发者建议使用以下结构:
User-Agent → “grpc-” Language ?(“-” Variant) “/” Version ?( “ (“ *(AdditionalProperty “;”) “)” )
grpc-java/1.2.3
grpc-ruby/1.2.3
grpc-ruby-jruby/1.3.4
grpc-java-android/0.9.1 (gingerbread/1.2.4; nexus5; tmobile)
HTTP2 传输映射
-
流识别
所有的 GRPC 调用需要定义指定一个内部 ID。我们将在这个模式里使用 HTTP2 流 ID 来作为调用标识。注意:这些 ID 在一个打开的 HTTP2 会话里是前后关联的,在一个处理多个 HTTP2 会话的进程里不是唯一的,也不能被用作 GUID。 -
数据帧
数据帧边界与界定消息的边界无关,实现时不应假定它们有一致性。 -
错误
当应用错误或运行时错误在 PRC 调用过程中出现时,状态和状态消息应当通过跟踪消息发送。
在有些情况下可能消息流的帧已经中断,RPC 运行时会选择使用 RST_STREAM 帧来给对方表示这种状态。RPC 运行时声明应当将 RST_STREAM 解释为流的完全关闭,并且将错误传播到应用层。
以下为从 RST_STREAM 错误码到 GRPC 的错误码的映射:
HTTP2编码 | GRPC 编码 |
---|---|
NO_ERROR(0) | INTERNAL -一个显式的GRPC OK状态应当被发出,但是这个也许在某些场景里会被侵略性地使用 |
PROTOCOL_ERROR(1) | INTERNAL |
INTERNAL_ERROR(2) | INTERNAL |
FLOW_CONTROL_ERROR(3) | INTERNAL |
SETTINGS_TIMEOUT(4) | INTERNAL |
STREAM_CLOSED | 无映射,因为没有打开的流来传播。实现应记录。 |
FRAME_SIZE_ERROR | INTERNAL |
REFUSED_STREAM | UNAVAILABLE-表示请求未作处理且可以重试,可能在他处重试。 |
CANCEL(8) | 当是由客户端发出时映射为调用取消,当是由服务端发出时映射为 CANCELLED。注意服务端在需要取消调用时应仅仅使用这个机制,但是有效荷载字节顺序是不完整的 |
COMPRESSION_ERROR | INTERNAL |
CONNECT_ERROR | INTERNAL |
ENHANCE_YOUR_CALM | RESOURCE_EXHAUSTED…并且运行时提供有额外的错误详情,表示耗尽资源是带宽 |
INADEQUATE_SECURITY | PERMISSION_DENIED… 并且有额外的信息表明许可被拒绝,因为对调用来说协议不够安全 |
安全
HTTP2 规范当使用 TLS 时强制使用 TLS 1.2 及以上的版本,并且在部署上对允许的密码施加一些额外的限制以避免已知的比如需要 SNI 支持的问题。并且期待 HTTP2 与专有的传输安全机制相结合,这些传输机制的规格说明不能提供有意义的建议。
连接管理
-
GOAWAY 帧
服务端发出这种帧给客户端表示服务端在相关的连接上不再接受任何新流。这种帧包含服务端最后成功接受的流的ID。客户端应该认为任何在最后成功的流后面初始化的任意流为 UNAVAILABLE,并且在别处重试这些调用。客户端可以自由地在已经接受的流上继续工作直到它们完成或者连接中断。
服务端应该在终止连接前发送 GOAWAY 帧,以可靠地通知客户端哪些工作已经被服务端接受并执行。 -
PING 帧
客户端和服务端均可以发送一个 PING 帧,对方必须精确回显它们所接收到的信息。这可以被用来确认连接仍然是活动的,并且能够提供估计端对端延迟估计的方法。假如服务端初始的 PING 在最后期限仍然没有收到运行时所期待的应答的话,所有未完成的调用将会被以取消状态关闭。一个客户端期满的初始的PING则会导致所有的调用被以用不可用状态关闭。注意PING的频率高度依赖于网络环境,实现可以根据网络和应用需要,自由地调整PING频率。 -
连接失败
假如客户端检测到连接失败,所有的调用都会被以不可用状态关闭。而服务端侧则所有已经打开的调用都会被以取消状态关闭。
附录 A - Protobuf 上的 GRPC
用 protobuf 定义的服务接口可以通过 protoc 的代码生成扩展简单地映射成 GRPC ,以下定义了所用的映射:
- 路径 → / 服务名 / {方法名}
- 服务名 → ?( {proto 包名} “.” ) {服务名}
- 消息类型 → {全路径 proto 消息名}
- 内容类型 → “application/grpc+proto”