引言
\n现代网络开发离不开安全的用户身份验证和授权。两个最流行的标准——OAuth 2.0 和 JWT(JSON Web 令牌)——已成为事实上的行业解决方案。如果你使用 Rust 编程,那么你很幸运:这种语言不仅提供最高的性能,还提供用于实现这些协议的强大库。
\n在本文中,我们将介绍什么是 OAuth 2.0 和 JWT,它们如何协同工作,并使用 actix-web 框架和 jsonwebtoken crate 在 Rust 中编写一个简单但可用的授权示例。本文面向初学者,因此我们将逐步进行——从理论到代码。
什么是 OAuth 2.0 和 JWT?
\n在编写代码之前,理解这些概念很重要。OAuth 2.0 是一种授权协议,它允许应用程序在不传递用户密码的情况下获取对用户资源的有限访问权限。当你通过 Google、GitHub 或 VK 登录网站时,就会用到它。
\nJWT 是一种令牌格式(JSON 对象签名和加密),它包含关于用户的声明(claims)。令牌由服务器签名,因此客户端无法伪造它。在 OAuth 2.0 的上下文中,JWT 通常用作访问令牌(access token)。
\n典型场景:用户输入登录名和密码,服务器验证它们,生成 JWT 令牌并将其返回给客户端。客户端在每个请求中将此令牌发送到 Authorization: Bearer <token> 标头中。服务器验证签名并提取用户数据。
在 Rust 中设置项目
\n创建一个新项目:
\ncargo new rust_oauth_jwt\ncd rust_oauth_jwt\n在 Cargo.toml 中添加必要的依赖项:
[dependencies]\nactix-web = "4"\nactix-rt = "2"\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\njsonwebtoken = "9"\nchrono = { version = "0.4", features = ["serde"] }\ndotenv = "0.15"\nenv_logger = "0.11"\nlog = "0.4"\n在项目根目录中创建一个 .env 文件来存储密钥:
JWT_SECRET=my_super_secret_key_change_in_production\nJWT_EXPIRATION_HOURS=24\n在 main.rs 中加载环境变量:
use dotenv::dotenv;\nuse std::env;\n\nfn main() {\n dotenv().ok();\n env_logger::init();\n // ... 其余代码\n}\n\n创建用户结构和 JWT 令牌
\n首先,定义用户模型和 JWT 的 claims 结构。创建 models.rs 文件:
use serde::{Deserialize, Serialize};\n\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct User {\n pub id: i32,\n pub username: String,\n pub password_hash: String, // 在实际项目中存储哈希值,而不是密码!\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct Claims {\n pub sub: String, // subject (用户 ID)\n pub username: String,\n pub exp: usize, // 过期时间戳\n}\n现在,在 jwt_utils.rs 中创建一个用于处理 JWT 的工具:
use chrono::{Duration, Utc};\nuse jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};\nuse crate::models::Claims;\n\npub fn create_jwt(user_id: &str, username: &str) -> Result<String, jsonwebtoken::errors::Error> {\n let secret = std::env::var("JWT_SECRET").expect("必须设置 JWT_SECRET");\n let expiration_hours: i64 = std::env::var("JWT_EXPIRATION_HOURS")\n .unwrap_or_else(|_| "24".to_string())\n .parse()\n .unwrap();\n\n let expiration = Utc::now()\n .checked_add_signed(Duration::hours(expiration_hours))\n .expect("有效的时间戳")\n .timestamp() as usize;\n\n let claims = Claims {\n sub: user_id.to_owned(),\n username: username.to_owned(),\n