示例说明
令牌验证的强度取决于声明的约束完整度:required_spec_claims 把 exp 与 sub 设为强制——缺任一即拒绝;validate_exp 开启过期校验,leeway 容忍五秒的多机时钟偏差防误杀;ES256 非对称算法让验证方只持公钥。业务声明 tenant 的匹配是额外防线——签给甲租户的令牌到乙租户的接口只能吃到拒绝。
技术要点
- required_spec_claims 声明必填字段
- leeway 吸收多机时钟偏差
- 租户声明匹配防跨租户复用
代码解读
issue 构造带时间戳与过期时间的声明并用 ES256 私钥签名;verify 的 Validation 限定算法、必填声明、过期校验与容忍窗口四要素,decode 失败的错误以 Debug 形式输出完整分类。两次验证演示:同租户放行输出身份,跨租户被租户检查拦截。
使用建议
私钥进密钥管理系统而非仓库,示例的 include_bytes 仅演示加载方式。算法白名单防降级。刷新令牌与访问令牌分轨管理时效。
代码示例
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Serialize, Deserialize)]
struct SessionClaims {
sub: String,
role: String,
tenant: String,
exp: u64,
iat: u64,
}
fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
fn issue(key: &EncodingKey, subject: &str, role: &str, tenant: &str) -> String {
let claims = SessionClaims {
sub: subject.into(),
role: role.into(),
tenant: tenant.into(),
iat: now(),
exp: now() + 900, // 15 分钟访问令牌
};
encode(&Header::new(Algorithm::ES256), &claims, key).unwrap()
}
fn verify(
key: &DecodingKey,
token: &str,
required_tenant: &str,
) -> Result<SessionClaims, String> {
let mut validation = Validation::new(Algorithm::ES256);
validation.required_spec_claims.insert("exp".into());
validation.required_spec_claims.insert("sub".into());
validation.leeway = 5; // 时钟偏差容忍 5 秒
validation.validate_exp = true;
let data = decode::<SessionClaims>(token, key, &validation)
.map_err(|err| format!("token rejected: {err:?}"))?;
if data.claims.tenant != required_tenant {
return Err("tenant mismatch".into());
}
Ok(data.claims)
}
fn main() {
let encoding = EncodingKey::from_ec_pem(include_bytes!("../keys/private.pem")).unwrap();
let decoding = DecodingKey::from_ec_pem(include_bytes!("../keys/public.pem")).unwrap();
let token = issue(&encoding, "user-42", "editor", "acme");
match verify(&decoding, &token, "acme") {
Ok(claims) => println!("accepted: {} as {}", claims.sub, claims.role),
Err(err) => println!("rejected: {err}"),
}
match verify(&decoding, &token, "other-tenant") {
Ok(_) => unreachable!(),
Err(err) => println!("cross-tenant blocked: {err}"),
}
}