3个版本
0.12.9 | 2024年1月13日 |
---|---|
0.12.8 | 2024年1月13日 |
0.12.7 | 2024年1月13日 |
#900 in Web编程
160KB
4K SLoC
JWT-Simple
针对Rust的新JWT (JSON Web Tokens) 实现,它侧重于简单性,同时避免常见的JWT安全陷阱。
jwt-simple
是无偏见的,并支持所有常用的身份验证和签名算法
JWT算法名称 | 描述 |
---|---|
HS256 |
HMAC-SHA-256 |
HS384 |
HMAC-SHA-384 |
HS512 |
HMAC-SHA-512 |
BLAKE2B |
BLAKE2B-256 |
RS256 |
RSA with PKCS#1v1.5 padding / SHA-256 |
RS384 |
RSA with PKCS#1v1.5 padding / SHA-384 |
RS512 |
RSA with PKCS#1v1.5 padding / SHA-512 |
PS256 |
RSA with PSS padding / SHA-256 |
PS384 |
RSA with PSS padding / SHA-384 |
PS512 |
RSA with PSS padding / SHA-512 |
ES256 |
ECDSA over p256 / SHA-256 |
ES384 |
ECDSA over p384 / SHA-384 |
ES256K |
ECDSA over secp256k1 / SHA-256 |
EdDSA |
Ed25519 |
jwt-simple
可以编译成 WebAssembly/WASI。它与 Fastly Compute 服务完全兼容。
重要提示:JWT 的目的是验证数据是否由知道密钥的一方创建。它不提供任何类型的机密性:JWT 数据只是编码为 BASE64,并且未加密。
用法
cargo.toml
:
[dependencies]
jwt-simple = "0.12"
Rust
use jwt_simple::prelude::*;
错误以 jwt_simple::Error
值返回(是 thiserror
crate 中 Error
类型的别名)。
身份验证(对称,HS*
JWT算法)示例
身份验证方案使用相同的密钥来创建和验证令牌。换句话说,双方最终必须相互信任,否则验证者也可以创建任意令牌。
密钥和令牌创建
密钥创建
use jwt_simple::prelude::*;
// create a new key for the `HS256` JWT algorithm
let key = HS256Key::generate();
可以使用 key.to_bytes()
将密钥导出为字节,并使用 HS256Key::from_bytes()
恢复。
令牌创建
/// create claims valid for 2 hours
let claims = Claims::create(Duration::from_hours(2));
let token = key.authenticate(claims)?;
-> 完成!
令牌验证
let claims = key.verify_token::<NoCustomClaims>(&token, None)?;
-> 完成!不需要额外步骤。
密钥过期时间、起始时间、认证标签等将自动验证。如果给定密钥的认证标签无效,函数将失败并返回 JWTError::InvalidAuthenticationTag
。
如有必要,可以在 claims
对象中检查完整的声明集。如果应用程序不使用自定义声明,则表示应用程序仅使用标准声明集,但也可以支持应用程序定义的声明。
可以通过 ValidationOptions
结构启用额外的验证步骤。
let mut options = VerificationOptions::default();
// Accept tokens that will only be valid in the future
options.accept_future = true;
// Accept tokens even if they have expired up to 15 minutes after the deadline,
// and/or they will be valid within 15 minutes.
options.time_tolerance = Some(Duration::from_mins(15));
// Reject tokens if they were issued more than 1 hour ago
options.max_validity = Some(Duration::from_hours(1));
// Reject tokens if they don't include an issuer from that set
options.allowed_issuers = Some(HashSet::from_strings(&["example app"]));
// see the documentation for the full list of available options
let claims = key.verify_token::<NoCustomClaims>(&token, Some(options))?;
请注意,allowed_issuers
和 allowed_audiences
不是字符串,而是字符串集合(使用 Rust 标准库中的 HashSet
类型),因为应用程序可以允许多个返回值。
签名(非对称,RS*
,PS*
,ES*
和EdDSA
算法)示例
签名需要一个密钥对:用于创建令牌的私有密钥和只能验证它们的公钥。
如果双方最终不信任对方,则始终应使用签名方案,例如客户端和 API 提供者之间交换的令牌。
密钥对和令牌创建
密钥创建
ES256
use jwt_simple::prelude::*;
// create a new key pair for the `ES256` JWT algorithm
let key_pair = ES256KeyPair::generate();
// a public key can be extracted from a key pair:
let public_key = key_pair.public_key();
ES384
use jwt_simple::prelude::*;
// create a new key pair for the `ES384` JWT algorithm
let key_pair = ES384KeyPair::generate();
// a public key can be extracted from a key pair:
let public_key = key_pair.public_key();
密钥可以导出为字节以供以后重用,也可以从字节导入,对于 RSA,可以从单个参数、DER 编码数据或 PEM 编码数据导入。
使用 OpenSSL 和 PEM 导入私有密钥创建 RSA 密钥对
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
let key_pair = RS384KeyPair::from_pem(private_pem_file_content)?;
let public_key = RS384PublicKey::from_pem(public_pem_file_content)?;
与 HS*
算法类似,令牌创建使用密钥对,验证使用相应的公钥。
令牌创建
/// create claims valid for 2 hours
let claims = Claims::create(Duration::from_hours(2));
let token = key_pair.sign(claims)?;
令牌验证
let claims = public_key.verify_token::<NoCustomClaims>(&token, None)?;
可用的验证选项与对称算法中使用的选项相同。
高级用法
自定义声明
声明对象默认支持所有标准声明,并且可以直接或通过方便的辅助函数设置。
let claims = Claims::create(Duration::from_hours(2)).
with_issuer("Example issuer").with_subject("Example subject");
但也可以定义应用程序定义的声明。这些只需在可序列化类型中存在(这需要 serde
crate)。
#[derive(Serialize, Deserialize)]
struct MyAdditionalData {
user_is_admin: bool,
user_country: String,
}
let my_additional_data = MyAdditionalData {
user_is_admin: false,
user_country: "FR".to_string(),
};
使用自定义数据创建声明
let claims = Claims::with_custom_claims(my_additional_data, Duration::from_secs(30));
使用自定义数据验证声明。请注意自定义数据类型的存在
let claims = public_key.verify_token::<MyAdditionalData>(&token, None)?;
let user_is_admin = claims.custom.user_is_admin;
在验证前查看元数据
在标签或签名验证之前,某些属性(如密钥标识符)可能很有用,以便从集合中选择正确的密钥。
let metadata = Token::decode_metadata(&token)?;
let key_id = metadata.key_id();
let algorithm = metadata.algorithm();
// all other standard properties are also accessible
重要:密钥 ID 和算法都不能被信任。这是 JWT 标准的一个无法修复的设计缺陷。
因此,应仅将 algorithm
用于调试目的,并且绝对不要用它来选择密钥类型。同样,应仅将 key_id
用于在为同一算法创建的密钥集中选择密钥。
如果最初使用签名方案创建令牌,则至少应禁止使用 HS*
进行验证。
创建和附加密钥标识符
密钥标识符告诉验证者应使用哪个公钥(或共享密钥)进行验证。它们可以随时附加到现有的共享密钥、密钥对和公钥上。
let public_key_with_id = public_key.with_key_id(&"unique key identifier");
除了委托给应用程序外,jwt-simple
还可以为现有密钥创建这样的标识符。
let key_id = public_key.create_key_id();
这为密钥创建一个文本编码的标识符,将其附加并返回。
如果已经将标识符附加到共享密钥或密钥对,使用它们创建的令牌将包括它。
减轻重放攻击的缓解措施
jwt-simple
包含减轻重放攻击的机制。
- 可以使用
create_nonce()
声明函数创建并附加到新令牌的 nonce。稍后,验证过程可以拒绝任何不包括预期 nonce(《code>required_nonce 验证选项)的令牌。 - 验证过程可以拒绝创建时间过长(无论其到期日期如何)的令牌。这防止了恶意(或受损)签发者的令牌被使用太长时间。
- 验证过程可以拒绝在某个日期之前创建的令牌。对于特定用户,可以将最后一次成功的认证日期存储在数据库中,并在此选项与稍后使用一起拒绝更旧的(重放的)令牌。
CWT (CBOR) 支持
开发代码包含一个 cwt
cargo 功能,它启用对 CWT 令牌的实验性解析和验证。
请注意,CWT 不支持自定义声明。所需的标识符尚未标准化。
此外,现有的用于 JSON 和 CBOR 反序列化的 Rust crate 不安全。不可信的实体可以发送需要大量内存和 CPU 来反序列化的序列化对象。已为 JSON 添加了补丁,但根据当前的 Rust 工具,对于 CBOR 来说这会很复杂。
作为一种缓解措施,我们强烈建议拒绝在应用程序上下文中可能过大的令牌。这可以通过使用 max_token_length
验证选项来完成。
解决与 boring
crate 的编译问题
作为对某个依赖项(boring
crate)的移植性问题的一种临时解决方案,此库可以编译为仅使用 Rust 实现版本。
为了做到这一点,请在 Cargo 配置中使用 default-features=false, features=["pure-rust"]
引入 crate。
不要无条件地这样做。这只适用于非常特定的设置和目标,并且仅在 boring
crate 的问题得到解决之前。在未来的版本中,如何在 Cargo 中配置此设置也可能发生变化。
针对 musl
库的静态构建不需要这种解决方案。只需使用 cargo-zigbuild
来构建您的项目。
在Web浏览器中使用
wasm32-freestanding
目标(在 Rust 中有时还称为 wasm32-unknown-unknown
)得到支持(即“它编译”)。
然而,强烈建议使用原生的 JavaScript 实现而不是使用它。JavaScript 中有高质量的 JWT 实现,利用 WebCrypto API,它们比 WebAssembly 模块提供更好的性能和安全性保证。
为什么还需要另一个JWT crate
这个 crate 并不是对 JWT 的认可。JWT 是 一个糟糕的设计,以及“但是这是一个标准”并不一定意味着它是好的许多例子之一。
如果您控制令牌的创建和验证,强烈推荐使用 PASETO 或 Biscuit。
然而,JWT 在业界仍然被广泛使用,并且仍然是与流行的 API 通信的绝对必需品。
此 crate 的设计是为了
- 易于使用,即使是对于 Rust 新手来说也是这样
- 避免常见的 JWT API 陷阱
- 支持广泛使用的功能。我非常希望将算法选择限制在Ed25519,但需要其他方法来连接现有的API,因此仅提供这些(除明显原因外,不包括
None
签名方法)。 - 最小化代码复杂性和外部依赖
- 自动执行常见任务以防止滥用。签名验证和声明验证会自动进行,而不是依赖于应用程序。
- 仍然允许高级用户在必要时访问JWT令牌包含的所有内容
- 在WebAssembly环境中直接运行,以便可以在函数即服务平台上使用。
依赖关系
~5–12MB
~250K SLoC