8 个版本 (稳定)
1.0.5 | 2024 年 1 月 27 日 |
---|---|
1.0.4 | 2024 年 1 月 25 日 |
1.0.2 | 2023 年 9 月 1 日 |
0.1.1 | 2023 年 8 月 18 日 |
#233 in HTTP 服务器
88KB
1K SLoC
为 actix 提供一个创建会话和会话验证库的全能解决方案。
它旨在通过中间件提取会话并通过使用 actix-web 提取器简单地验证端点。目前,您可以从 Header 或 Cookie 中提取令牌。您可以使用 [ServiceRequest::extract]
实现路径、查询或正文,但您必须有一个结构体,以便从中提取值,因此如果您有自己的字段,这将很容易实现。
示例
use serde::Deserialize;
#[derive(Deserialize)]
struct MyJsonBody {
jwt: Option<String>,
refresh: Option<String>,
}
要开始使用此库,您需要创建自己的 AppClaims
结构体并为它实现 actix_jwt_session::Claims
特性。
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Audience {
Web,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case")]
pub struct Claims {
#[serde(rename = "exp")]
pub expiration_time: u64,
#[serde(rename = "iat")]
pub issues_at: usize,
/// Account login
#[serde(rename = "sub")]
pub subject: String,
#[serde(rename = "aud")]
pub audience: Audience,
#[serde(rename = "jti")]
pub jwt_id: uuid::Uuid,
#[serde(rename = "aci")]
pub account_id: i32,
#[serde(rename = "nbf")]
pub not_before: u64,
}
impl actix_jwt_session::Claims for Claims {
fn jti(&self) -> uuid::Uuid {
self.jwt_id
}
fn subject(&self) -> &str {
&self.subject
}
}
impl Claims {
pub fn account_id(&self) -> i32 {
self.account_id
}
}
然后,您必须创建带有会话存储的中间件工厂。目前只有针对 redis 的适配器,所以我们将在本例中使用它。
- 首先使用
redis_async_pool
创建 redis 连接池。 - 接下来生成或加载 jwt 签名密钥。它们用于从声明中创建 JWT。
- 最后将密钥和算法传递给构建器,传递池并添加一些提取器
use std::sync::Arc;
use actix_jwt_session::*;
async fn create<AppClaims: actix_jwt_session::Claims>() {
// create redis connection
let redis = {
use redis_async_pool::{RedisConnectionManager, RedisPool};
RedisPool::new(
RedisConnectionManager::new(
redis::Client::open("redis://127.0.0.1:6379").expect("Fail to connect to redis"),
true,
None,
),
5,
)
};
// load or create new keys in `./config`
let keys = JwtSigningKeys::load_or_create();
// create new [SessionStorage] and [SessionMiddlewareFactory]
let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
Arc::new(keys.encoding_key),
Arc::new(keys.decoding_key),
Algorithm::EdDSA
)
// pass redis connection
.with_redis_pool(redis.clone())
// Check if header "Authorization" exists and contains Bearer with encoded JWT
.with_jwt_header("Authorization")
// Check if cookie "jwt" exists and contains encoded JWT
.with_jwt_cookie("acx-a")
.with_refresh_header("ACX-Refresh")
// Check if cookie "jwt" exists and contains encoded JWT
.with_refresh_cookie("acx-r")
.finish();
}
如您所见,我们有 SessionMiddlewareBuilder::with_refresh_cookie 和 SessionMiddlewareBuilder::with_refresh_header。库使用内部结构 [RefreshToken],它由库内部创建和管理,无需任何额外的用户工作。
这将用于扩展 JWT 生命周期。这个生命周期由两个描述存活时间的结构体描述。[JwtTtl] 描述访问令牌应有效多长时间,[RefreshToken] 描述刷新令牌的有效时间。[SessionStorage] 允许通过单个调用 SessionStorage::refresh 来扩展两者的生命周期,并将令牌创建时间更改为当前时间。
use actix_jwt_session::{JwtTtl, RefreshTtl, Duration};
fn example_ttl() {
let jwt_ttl = JwtTtl(Duration::days(14));
let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
}
现在您只需要将这些结构添加到 actix_web::App 中,使用 .app_data
和 .wrap
,然后您就可以开始了。下面是使用示例。
使用示例
use std::sync::Arc;
use actix_jwt_session::*;
use actix_web::{get, post};
use actix_web::web::{Data, Json};
use actix_web::{HttpResponse, App, HttpServer};
use jsonwebtoken::*;
use serde::{Serialize, Deserialize};
#[tokio::main]
async fn main() {
let redis = {
use redis_async_pool::{RedisConnectionManager, RedisPool};
RedisPool::new(
RedisConnectionManager::new(
redis::Client::open("redis://127.0.0.1:6379").expect("Fail to connect to redis"),
true,
None,
),
5,
)
};
let keys = JwtSigningKeys::load_or_create();
let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
Arc::new(keys.encoding_key),
Arc::new(keys.decoding_key),
Algorithm::EdDSA
)
.with_redis_pool(redis.clone())
// Check if header "Authorization" exists and contains Bearer with encoded JWT
.with_jwt_header(JWT_HEADER_NAME)
// Check if cookie JWT exists and contains encoded JWT
.with_jwt_cookie(JWT_COOKIE_NAME)
.with_refresh_header(REFRESH_HEADER_NAME)
// Check if cookie JWT exists and contains encoded JWT
.with_refresh_cookie(REFRESH_COOKIE_NAME)
.finish();
let jwt_ttl = JwtTtl(Duration::days(14));
let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
HttpServer::new(move || {
App::new()
.app_data(Data::new(storage.clone()))
.app_data(Data::new( jwt_ttl ))
.app_data(Data::new( refresh_ttl ))
.wrap(factory.clone())
.app_data(Data::new(redis.clone()))
.service(must_be_signed_in)
.service(may_be_signed_in)
.service(register)
.service(sign_in)
.service(sign_out)
.service(refresh_session)
.service(session_info)
.service(root)
})
.bind(("0.0.0.0", 8080)).unwrap()
.run()
.await.unwrap();
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionData {
id: uuid::Uuid,
subject: String,
}
#[get("/authorized")]
async fn must_be_signed_in(session: Authenticated<AppClaims>) -> HttpResponse {
use crate::actix_jwt_session::Claims;
let jit = session.jti();
HttpResponse::Ok().finish()
}
#[get("/maybe-authorized")]
async fn may_be_signed_in(session: MaybeAuthenticated<AppClaims>) -> HttpResponse {
if let Some(session) = session.into_option() {
}
HttpResponse::Ok().finish()
}
#[derive(Deserialize)]
struct SignUpPayload {
login: String,
password: String,
password_confirmation: String,
}
#[post("/session/sign-up")]
async fn register(payload: Json<SignUpPayload>) -> Result<HttpResponse, actix_web::Error> {
let payload = payload.into_inner();
// Validate payload
// Save model and return HttpResponse
let model = AccountModel {
id: -1,
login: payload.login,
// Encrypt password before saving to database
pass_hash: Hashing::encrypt(&payload.password).unwrap(),
};
// Save model
todo!()
}
#[derive(Deserialize)]
struct SignInPayload {
login: String,
password: String,
}
#[post("/session/sign-in")]
async fn sign_in(
store: Data<SessionStorage>,
payload: Json<SignInPayload>,
jwt_ttl: Data<JwtTtl>,
refresh_ttl: Data<RefreshTtl>,
) -> Result<HttpResponse, actix_web::Error> {
let payload = payload.into_inner();
let store = store.into_inner();
let account: AccountModel = {
/* load account using login */
todo!()
};
if let Err(e) = Hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
return Ok(HttpResponse::Unauthorized().finish());
}
let claims = AppClaims {
issues_at: OffsetDateTime::now_utc().unix_timestamp() as usize,
subject: account.login.clone(),
expiration_time: jwt_ttl.0.as_seconds_f64() as u64,
audience: Audience::Web,
jwt_id: uuid::Uuid::new_v4(),
account_id: account.id,
not_before: 0,
};
let pair = store
.clone()
.store(claims, *jwt_ttl.into_inner(), *refresh_ttl.into_inner())
.await
.unwrap();
Ok(HttpResponse::Ok()
.append_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
.append_header((REFRESH_HEADER_NAME, pair.refresh.encode().unwrap()))
.finish())
}
#[post("/session/sign-out")]
async fn sign_out(store: Data<SessionStorage>, auth: Authenticated<AppClaims>) -> HttpResponse {
let store = store.into_inner();
store.erase::<AppClaims>(auth.jwt_id).await.unwrap();
HttpResponse::Ok()
.append_header((JWT_HEADER_NAME, ""))
.append_header((REFRESH_HEADER_NAME, ""))
.cookie(
actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, "")
.expires(OffsetDateTime::now_utc())
.finish(),
)
.cookie(
actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, "")
.expires(OffsetDateTime::now_utc())
.finish(),
)
.finish()
}
#[get("/session/info")]
async fn session_info(auth: Authenticated<AppClaims>) -> HttpResponse {
HttpResponse::Ok().json(&*auth)
}
#[get("/session/refresh")]
async fn refresh_session(
auth: Authenticated<RefreshToken>,
storage: Data<SessionStorage>,
) -> HttpResponse {
let storage = storage.into_inner();
storage.refresh(auth.refresh_jti).await.unwrap();
HttpResponse::Ok().json(&*auth)
}
#[get("/")]
async fn root() -> HttpResponse {
HttpResponse::Ok().finish()
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Audience {
Web,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case")]
pub struct AppClaims {
#[serde(rename = "exp")]
pub expiration_time: u64,
#[serde(rename = "iat")]
pub issues_at: usize,
/// Account login
#[serde(rename = "sub")]
pub subject: String,
#[serde(rename = "aud")]
pub audience: Audience,
#[serde(rename = "jti")]
pub jwt_id: uuid::Uuid,
#[serde(rename = "aci")]
pub account_id: i32,
#[serde(rename = "nbf")]
pub not_before: u64,
}
impl actix_jwt_session::Claims for AppClaims {
fn jti(&self) -> uuid::Uuid {
self.jwt_id
}
fn subject(&self) -> &str {
&self.subject
}
}
impl AppClaims {
pub fn account_id(&self) -> i32 {
self.account_id
}
}
struct AccountModel {
id: i32,
login: String,
pass_hash: String,
}
变更日志
1.0.0
- 工厂使用构建器模式创建
- JSON Web Token 自动创建刷新令牌
- Middleware、MiddlewareFactory 和 SessionStorage 的高级抽象层
- 内置哈希函数
- 内置 TTL 结构
- 文档
1.0.1
- 在刷新生命周期后返回新对
1.0.2
- 许可证文件
- 分类和标签
- 仓库
- 错误追踪器
- 测试构建
- 徽章
错误追踪器
依赖项
~22–38MB
~735K SLoC