客户端 JWT 身份验证
为了安全地验证传入的实时客户端连接,Centrifugo 可以使用应用程序后端颁发的 JSON Web 令牌 (JWT)。此过程允许 Centrifugo 安全地识别应用程序中的用户 ID。此外,您的应用程序可以在 JWT 声明中包含额外信息,然后 Centrifugo 可以使用这些信息。本章将解释如何创建和使用此类连接令牌。
如果您不想使用 JWT,请考虑使用代理功能。它允许将来自 Centrifugo 的连接请求代理到应用程序的后端终端节点进行身份验证。
在涉及大量重新连接的情况下,使用 JWT 进行身份验证可能很有用。由于身份验证信息在令牌中编码,因此这可以显著减少应用程序会话后端的负载。有关更多详细信息,请参阅我们的博客文章。
连接后,客户端应提供包含多个预定义凭据声明的连接 JWT。下图说明了这一点:
有关在客户端处理连接令牌的更多信息,请参阅客户端 SDK 规范。
目前,Centrifugo 支持 HMAC、RSA 和 ECDSA JWT 算法,特别是 HS256、HS384、HS512、RSA256、RSA384、RSA512、EC256、EC384 和 EC512。
在这里,我们将演示使用客户端的 Javascript Centrifugo 客户端和 PyJWT Python 库在后端生成连接令牌的示例代码段。
要将 HMAC 密钥添加到 Centrifugo,请将token_hmac_secret_key
插入配置文件:
{
...
"token_hmac_secret_key": "<YOUR-SECRET-STRING-HERE>"
}
要添加 RSA 公钥(必须是 PEM 编码的字符串),请添加token_rsa_public_key
选项,例如:
{
...
"token_rsa_public_key": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZ..."
}
要添加 ECDSA 公钥(必须是 PEM 编码的字符串),请添加token_ecdsa_public_key
选项,例如:
{
...
"token_ecdsa_public_key": "-----BEGIN PUBLIC KEY-----\nxyz23adf..."
}
连接 JWT 声明
对于连接 JWT,Centrifugo 使用 RFC 7519 中定义的一些标准声明,以及特定于 Centrifugo 的自定义声明。
子
此标准 JWT 声明必须包含当前应用程序用户的 ID(字符串形式)。
如果用户未在应用程序中进行身份验证,但您希望允许他们连接到 Centrifugo,则可以在 sub
声明中使用空字符串作为用户 ID。这有利于匿名访问。在这种情况下,您可能需要启用相应的通道命名空间选项,以允许匿名用户的协议功能。
exp (英文)
此声明指定令牌过期的 UNIX 时间戳(以秒为单位)。它是一个标准的 JWT 声明 - 不同编程语言的所有 JWT 库都提供了一个 API 来设置它。
如果不包含 exp
声明,Centrifugo 不会使连接过期。包含时,特殊算法将识别过去具有 exp
的连接并启动连接刷新机制。刷新机制允许扩展连接。如果刷新失败,Centrifugo 最终将关闭客户端连接,在连接令牌中提供新的有效凭证和当前凭证之前,不会再次接受该连接。
连接过期机制可用于不希望用户在应用程序中被禁止或停用后继续订阅频道的情况。它还通过设置合理短的过期时间来保护用户免受令牌泄漏。
请谨慎选择 exp
值;值太短可能会导致应用程序频繁地点击刷新请求,而值太长可能会导致用户连接停用延迟。这是一个平衡问题。
有关连接过期的更多详细信息,请参阅下文。
IAT公司
这表示颁发令牌的 UNIX 时间(以秒为单位)。请参阅 RFC 7519 中的定义。此声明是可选的,但与 Centrifugo PRO 的令牌吊销功能结合使用可能是有利的。
JTI
这是令牌的唯一标识符。请参阅 RFC 7519 中的定义。此声明是可选的,但与 Centrifugo PRO 的令牌吊销功能结合使用时可能有益。
澳元
默认情况下,Centrifugo 不检查 JWT 受众 (rfc7519 aud claim)。
但是你可以通过设置字符串选项来强制token_audience
此检查:
{
"token_audience": "centrifugo"
}
设置 token_audience
也会影响订阅 Token(用于频道 Token 授权)。如果您需要将连接令牌配置和订阅令牌配置分开,请查看单独的订阅令牌配置功能。
国际空间站
默认情况下,Centrifugo 不检查 JWT 颁发者(rfc7519 iss 声明)。
但是你可以通过设置字符串选项来强制token_issuer
此检查:
{
"token_issuer": "my_app"
}
设置 token_issuer
也会影响订阅 Token(用于频道 Token 授权)。如果您需要将连接令牌配置和订阅令牌配置分开,请查看单独的订阅令牌配置功能。
信息
此可选声明提供有关客户端与 Centrifugo 连接的其他信息。这些信息将包括在内:
- 在线状态数据
- 加入/离开事件
以及客户端渠道出版物
B64信息
对于使用二进制 Protobuf 协议并要求信息
为自定义字节的用户,应使用此字段。
它包含字节的 base64
编码表示形式。Centrifugo 会在收到时将 base64 解码回字节,并将结果合并到上述各个位置。
渠道
这是一个可选的字符串数组,用于标识客户端将订阅的服务器端通道。更多详细信息可以在 server-side subscriptions 的文档中找到。
请务必注意,channels
声明有时会被用户误解为频道权限列表。它没有达到那个目的。相反,使用此声明会导致客户端在连接时自动订阅指定的频道,因此无需从客户端调用 subscribe
API。有关更多信息,请参阅服务器端订阅文档。
潜艇
此可选声明是带有选项的通道映射,与 channels
声明相比,它为服务器端订阅提供了更详细的方法,因为它允许通过 options 使用附加信息和数据对每个通道进行注释。
术语 subs
是订阅的简写。它不应与前面提到的 sub 声明混淆,后者
是用于提供用户 ID(subject 的缩写)的标准 JWT 声明。尽管它们的名称相似,但这些声明在连接 JWT 中具有不同的用途。
例:
{
...
"subs": {
"channel1": {
"data": {"welcome": "welcome to channel1"}
},
"channel2": {
"data": {"welcome": "welcome to channel2"}
}
}
}
订阅选项:
田 | 类型 | 自选 | 描述 |
---|---|---|---|
信息 | JSON 对象 | 是的 | 自定义频道信息 |
B64信息 | 字符串 | 是的 | Base64 中的自定义频道信息 - 传递二进制频道信息 |
数据 | JSON 对象 | 是的 | 要在 Connect 回复中的订阅上下文中返回的自定义 JSON 数据 |
B64数据 | 字符串 | 是的 | 与 data 相同,但在 Base64 中发送二进制数据 |
覆盖 | SubscribeOptionOverride | 是的 | 允许基于每个连接动态覆盖 Centrifugo 配置中定义的某些通道选项(请参阅下面的可用字段) |
SubscribeOptionOverride
允许对某些通道命名空间选项进行按连接覆盖:
田 | 类型 | 自选 | 描述 |
---|---|---|---|
存在 | 布尔值 | 是的 | 从命名空间选项覆盖 状态 |
join_leave | 布尔值 | 是的 | 从命名空间选项覆盖 join_leave |
force_recovery | 布尔值 | 是的 | 从命名空间选项覆盖 force_recovery |
force_positioning | 布尔值 | 是的 | 从命名空间选项覆盖 force_positioning |
force_push_join_leave | 布尔值 | 是的 | 覆盖命名空间选项中的 force_push_join_leave |
BoolValue
是一个类似于这样的对象:
{
"value": true/false
}
元
meta
是附加到连接的附加 JSON 对象(例如,{“key”: “value”}
)。它与 info
不同,因为它永远不会在 presence 和 join/leave 事件中向客户披露;它只能在服务器端访问。它可以包含在从 Centrifugo 到应用程序后端的代理调用中(请参阅 proxy_include_connection_meta
选项)。在 Centrifugo PRO 中,有一个连接
API 方法,可在连接描述对象中返回此元数据。
expire_at
尽管 Centrifugo 通常使用 exp
声明来管理连接过期,但在某些情况下,您可能希望将令牌过期检查与连接过期时间分开。当 expire_at
声明包含在 JWT 中时,Centrifugo 使用它来确定连接过期时间,而 JWT 过期时间仍使用 exp
声明进行验证。
expire_at
是一个 UNIX 时间戳,指示连接何时应过期。
要在将来的特定时间使连接过期,请将其设置为该时间。
若要防止连接过期,请将其设置为0
(仍将检查令牌exp
声明) 。
连接过期
如前所述,连接令牌中的 exp
声明旨在使客户端连接在某个时间点过期。以下是 Centrifugo 确定连接即将过期时的过程。
首先,通过为连接 JWT 提供 exp
声明来激活 Centrifugo 中的客户端过期机制:
import jwt
import time
token = jwt.encode({"sub": "42", "exp": int(time.time()) + 10*60}, "secret", algorithm="HS256")
print(token)
假设 exp
声明设置为 10 分钟后过期,则客户端将使用此令牌连接到 Centrifugo。Centrifugo 将在指定的持续时间内保持连接。时间过后,Centrifugo 允许客户端有一段宽限期(默认为 25 秒),以便使用包含更新后的 exp
的新有效令牌刷新其凭证。
初始连接时,客户端会在 connect 响应中收到一个 ttl
值,指示在必须使用新凭证启动刷新命令之前剩余的秒数。Centrifugo SDK 在内部处理此 TTL
并自动开始刷新过程。
SDK 提供了挂接到此流程的机制,并提供了获取新令牌的函数。由开发人员决定如何从后端加载新令牌 - 在 Web 浏览器中,这通常是一个简单的 fetch
请求,响应可能如下所示:
{
"token": token
}
您应该提供在最初呈现页面时颁发的相同连接 JWT,但具有更新且有效的 exp
。然后,我们的 SDK 会将此令牌发送到 Centrifugo 服务器,并且连接将延长新 exp
中设置的时间段。
当您从应用程序后端加载新令牌时,必须通过应用程序的会话机制来促进用户身份验证。因此,您知道要为谁生成更新的令牌。
示例:创建连接 JWT
让我们看看如何在 Python 中生成连接 HS256 JWT:
最简单的代币
- 蟒
- NodeJS 的
import jwt
token = jwt.encode({"sub": "42"}, "secret").decode()
print(token)
const jose = require('jose');
(async function main() {
const secret = new TextEncoder().encode('secret')
const alg = 'HS256'
const token = await new jose.SignJWT({ sub: '42' })
.setProtectedHeader({ alg })
.sign(secret)
console.log(token);
})();
请注意,我们在此处使用 Centrifugo config 中的 token_hmac_secret_key
的值(在本例中token_hmac_secret_key
值只是 secret
)。唯一必须知道 HMAC 密钥的两个是生成 JWT 和 Centrifugo 的应用程序后端。您绝不应向用户透露 HMAC 密钥。
然后,您可以将此令牌传递给您的客户端,并在连接到 Centrifugo 时使用它:
var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket", {
token: token
});
centrifuge.connect();
有关在客户端使用连接令牌和处理令牌过期的更多详细信息,请参阅实时 SDK API 规范。
具有过期时间的令牌
有效期为 5 分钟的 HS256 令牌:
- 蟒
- NodeJS 的
import jwt
import time
claims = {"sub": "42", "exp": int(time.time()) + 5*60}
token = jwt.encode(claims, "secret", algorithm="HS256").decode()
print(token)
const jose = require('jose')
(async function main() {
const secret = new TextEncoder().encode('secret')
const alg = 'HS256'
const token = await new jose.SignJWT({ sub: '42' })
.setProtectedHeader({ alg })
.setExpirationTime('5m')
.sign(secret)
console.log(token);
})();
包含其他连接信息的令牌
让我们附加用户名:
- 蟒
- NodeJS 的
import jwt
claims = {"sub": "42", "info": {"name": "Alexander Emelin"}}
token = jwt.encode(claims, "secret", algorithm="HS256").decode()
print(token)
const jose = require('jose')
(async function main() {
const secret = new TextEncoder().encode('secret')
const alg = 'HS256'
const token = await new jose.SignJWT({ sub: '42', info: {"name": "Alexander Emelin"} })
.setProtectedHeader({ alg })
.setExpirationTime('5m')
.sign(secret)
console.log(token);
})();
示例:使用 JWT 连接
要与 JWT 连接,应在建立实时连接时将其从客户端传递给 Centrifugo。
我们的双向 SDK 提供了设置初始令牌的选项,以及用于设置函数以加载新连接令牌的选项(需要处理过期令牌的刷新)。请参阅客户端 SDK 规范中的示例。
我们的单向传输接受 JWT 作为 connect 有效负载的一部分。对于每种单向传输,connect payload 传递到 Centrifugo 的方式不同。
调查 JWT 的问题
您可以使用 jwt.io 站点来调查令牌的内容。此外,服务器日志通常包含一些有用的信息。
JSON Web 密钥支持
Centrifugo 支持 JSON Web 密钥 (JWK) 规范。这意味着可以通过向 Centrifugo 提供加载 JWK 的端点来提高 JWT 安全性(通过查看 JWT 的 kid
标头)。
可以通过向 Centrifugo (HTTP address) 提供token_jwks_public_endpoint
字符串选项来启用机制。
设置token_jwks_public_endpoint
后,将使用从 JWKS 终端节点加载的 JSON Web 密钥集验证所有令牌。这使得无法使用非基于 JWK 的令牌来连接和订阅专用通道。
阅读我们博客中有关将 Centrifugo 与 Keycloak SSO 结合使用的教程。在这种情况下,使用从 Keycloak 的 JWKS 端点加载的公钥来验证连接令牌。
目前,Centrifugo 将从终端节点加载的密钥缓存一小时。
Centrifugo 将通过发出 GET HTTP 请求从 JWKS 端点加载密钥,超时时间为 1 秒,如果失败,则重试一次(目前不可配置)。
Centrifugo 支持 JWK 令牌的以下密钥类型 (kty
):
RSA
EC
(自 Centrifugo v5.1.0 起)
基于 Ed25519 的OKP
(自 Centrifugo v5.2.1 起)
启用后,JWKS 将用于连接和通道订阅令牌。
动态 JWKs 端点
可以使用 Go regexp 命名组从 iss
和 aud
JWT 声明中提取变量,然后使用在 iss
或 aud
匹配期间提取的变量在令牌验证时动态构建 JWKS 端点。在这种情况下,可以在 config 中将 JWKS 端点设置为模板。
为此,Centrifugo 提供了两个额外的选项:
token_issuer_regex
- 将 JWT 颁发者(iss
声明)与此正则表达式匹配,将命名组提取到变量,然后变量可用于 JWKS 端点构建。token_audience_regex
— 将JWT受众(aud
声明)与此正则表达式匹配,将命名组提取到变量,然后变量可用于JWKS端点构建。
让我们看一下这个例子:
{
"token_issuer_regex": "https://example.com/auth/realms/(?P<realm>[A-z]+)",
"token_jwks_public_endpoint": "https://keycloak:443/{{realm}}/protocol/openid-connect/certs",
}
要在 token_jwks_public_endpoint
中使用 variable,它必须用 {{
}}
包裹。
使用 token_issuer_regex
和 token_audience_regex
时,请确保配置中没有使用 token_issuer
和 token_audience
- 否则,Centrifugo 启动时将返回错误。
设置 token_issuer_regex
和 token_audience_regex
也会影响订阅 Token(用于频道 Token 授权)。如果您需要将连接令牌配置和订阅令牌配置分开,请查看单独的订阅令牌配置功能。
自定义令牌用户 ID 声明
Centrifugo v5.4.6 中的新功能。
可以在 token 中使用替代声明来传递用户 ID:带有token_user_id_claim
选项(字符串,默认为 “”
– 即未使用)。
{
...
"token_user_id_claim": "user_id"
}
默认情况下,Centrifugo 使用 JWT 的 sub
声明来提取用户 ID - 这是在 JWT 规范中定义的,是传递用户 ID 的推荐方法。
此时,token_user_id_claim
设置的自定义声明必须遵循以下正则表达式:^[a-zA-Z_]+$
。
设置备用用户 ID 声明也会影响订阅令牌,就像任何其他令牌选项一样。要对订阅令牌使用不同的配置,Centrifugo 提供了separate_subscription_token_config选项。