Tower Sessions
認証ライブラリ
Tower Sessions
概要
Tower Sessionsは、RustのTowerおよびAxumウェブフレームワーク用のセッション管理ミドルウェアです。リクエストの拡張としてSessionを提供し、Djangoのセッションミドルウェアに触発された設計でセッションベース認証やセッション管理機能を実現します。セッションデータはクッキーではなく、サーバーサイドに安全に保存されます。
詳細
Tower Sessionsは、高性能で人間工学的なセッション管理を提供します。セッションの実装はDjangoのセッションミドルウェアの意味論を移植したもので、Rustエコシステムに適応させた設計になっています。セッションデータの永続化は、ユーザーが提供するSessionStore実装によって管理されます。
主要機能
- キー・バリューインターフェース: ネイティブRust型をサポート
- JSON シリアライゼーション: SerializeとDeserialize型の自動対応
- セッションストア抽象化: カスタマイズ可能なストレージバックエンド
- セッションライフサイクル管理: 自動的なセッション初期化と保存
- 型安全性: Rustの型システムを活用したセッション操作
ストレージオプション
- MemoryStore: インメモリセッション(開発・テスト用)
- SqliteStore: SQLiteデータベースストレージ
- PostgreSQL: PostgreSQLデータベース連携
- Redis: 高性能分散セッション管理
- カスタムストア: SessionStore trait実装でカスタマイズ可能
セキュリティ機能
- セッション固定攻撃対策: ログイン時のセッショントークンローテーション
- 安全なクッキー: HttpOnly、Secure、SameSite属性の設定
- 期限管理: 自動的な期限切れセッションの削除
- 暗号化対応: セッションデータの暗号化オプション
メリット・デメリット
メリット
- 高性能: Rustの性能特性を活用した高速なセッション処理
- 型安全: コンパイル時の型チェックによる安全性
- 柔軟性: 複数のストレージバックエンドとカスタマイズ可能
- 統合性: TowerとAxumエコシステムとの完全統合
- セキュリティ: セッション固定攻撃対策など高いセキュリティ
- モジュラー: 必要な機能のみを組み込み可能
デメリット
- Rust専用: Rust言語とTower/Axumエコシステム限定
- 学習コスト: Rustとasync/awaitの理解が必要
- エコシステム: Rustウェブ開発のエコシステムがまだ発展途上
- 設定複雑さ: 本格的な設定には一定の専門知識が必要
参考ページ
- Tower Sessions GitHub - 公式リポジトリ
- Tower Sessions Documentation - APIドキュメント
- Axum Login - 認証統合例
書き方の例
基本セットアップ
# Cargo.toml
[dependencies]
tower-sessions = "0.14.0"
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
メモリストアでの基本使用
use std::net::SocketAddr;
use axum::{response::IntoResponse, routing::get, Router};
use serde::{Deserialize, Serialize};
use time::Duration;
use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer};
const COUNTER_KEY: &str = "counter";
#[derive(Default, Deserialize, Serialize)]
struct Counter(usize);
async fn handler(session: Session) -> impl IntoResponse {
let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
session.insert(COUNTER_KEY, counter.0 + 1).await.unwrap();
format!("Current count: {}", counter.0)
}
#[tokio::main]
async fn main() {
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false)
.with_expiry(Expiry::OnInactivity(Duration::seconds(10)));
let app = Router::new().route("/", get(handler)).layer(session_layer);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service()).await.unwrap();
}
SQLiteストアでの使用
use std::net::SocketAddr;
use axum::{response::IntoResponse, routing::get, Router};
use serde::{Deserialize, Serialize};
use time::Duration;
use tower_sessions::{session_store::ExpiredDeletion, Expiry, Session, SessionManagerLayer};
use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore};
const COUNTER_KEY: &str = "counter";
#[derive(Serialize, Deserialize, Default)]
struct Counter(usize);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// SQLiteデータベース接続
let pool = SqlitePool::connect("sqlite::memory:").await?;
let session_store = SqliteStore::new(pool);
session_store.migrate().await?;
// 期限切れセッションの自動削除タスク
let deletion_task = tokio::task::spawn(
session_store
.clone()
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
);
// セッション管理レイヤー設定
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false)
.with_expiry(Expiry::OnInactivity(Duration::seconds(600)));
let app = Router::new().route("/", get(handler)).layer(session_layer);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app.into_make_service()).await?;
deletion_task.await??;
Ok(())
}
async fn handler(session: Session) -> impl IntoResponse {
let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
session.insert(COUNTER_KEY, counter.0 + 1).await.unwrap();
format!("Current count: {}", counter.0)
}
セッションベース認証
use axum::{
http::StatusCode,
response::{IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use serde::{Deserialize, Serialize};
use tower_sessions::{MemoryStore, Session, SessionManagerLayer};
const USER_ID_KEY: &str = "user_id";
#[derive(Serialize, Deserialize)]
struct User {
id: u32,
username: String,
}
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
// ログインハンドラー
async fn login_post(session: Session, Form(form): Form<LoginForm>) -> impl IntoResponse {
// 認証チェック(実際の実装では適切な認証ロジックを使用)
if authenticate_user(&form.username, &form.password).await {
let user = User {
id: 1,
username: form.username,
};
// セッションにユーザー情報を保存
session.insert(USER_ID_KEY, user.id).await.unwrap();
Redirect::to("/dashboard").into_response()
} else {
(StatusCode::UNAUTHORIZED, "Invalid credentials").into_response()
}
}
// 保護されたルート
async fn dashboard(session: Session) -> impl IntoResponse {
if let Ok(Some(user_id)) = session.get::<u32>(USER_ID_KEY).await {
format!("Welcome user {}!", user_id)
} else {
Redirect::to("/login").into_response()
}
}
// ログアウトハンドラー
async fn logout(session: Session) -> impl IntoResponse {
session.delete().await.unwrap();
Redirect::to("/login")
}
async fn authenticate_user(username: &str, password: &str) -> bool {
// 実際の認証ロジックをここに実装
username == "admin" && password == "password"
}
#[tokio::main]
async fn main() {
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store);
let app = Router::new()
.route("/login", post(login_post))
.route("/dashboard", get(dashboard))
.route("/logout", post(logout))
.layer(session_layer);
// サーバー起動コード...
}
カスタムセッションストア
use async_trait::async_trait;
use tower_sessions::session_store::{Error, Result, SessionStore};
use tower_sessions::{session::Record, SessionId};
pub struct CustomSessionStore {
// カスタムストレージの実装
}
#[async_trait]
impl SessionStore for CustomSessionStore {
async fn save(&self, session_record: &Record) -> Result<()> {
// セッションレコードを保存
println!("Saving session: {:?}", session_record.id);
Ok(())
}
async fn load(&self, session_id: &SessionId) -> Result<Option<Record>> {
// セッションIDからレコードを読み込み
println!("Loading session: {:?}", session_id);
Ok(None)
}
async fn delete(&self, session_id: &SessionId) -> Result<()> {
// セッションを削除
println!("Deleting session: {:?}", session_id);
Ok(())
}
}
セッション設定のカスタマイズ
use time::Duration;
use tower_sessions::{Expiry, SessionManagerLayer, MemoryStore};
let session_layer = SessionManagerLayer::new(MemoryStore::default())
.with_name("my_session") // セッション名
.with_domain("example.com") // ドメイン設定
.with_http_only(true) // HttpOnlyクッキー
.with_same_site(tower_sessions::cookie::SameSite::Strict) // SameSite設定
.with_secure(true) // セキュアクッキー(HTTPS必須)
.with_path("/app") // クッキーパス
.with_expiry(Expiry::OnInactivity(Duration::minutes(30))); // 30分の非アクティブで期限切れ
エラーハンドリング
use tower_sessions::Session;
async fn safe_session_handler(session: Session) -> impl IntoResponse {
match session.get::<String>("key").await {
Ok(Some(value)) => format!("Value: {}", value),
Ok(None) => "Key not found".to_string(),
Err(e) => {
eprintln!("Session error: {:?}", e);
"Internal server error".to_string()
}
}
}