Tower Sessions

認証ライブラリRustセッション管理TowerAxumミドルウェアセッションクッキー認証

認証ライブラリ

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ウェブ開発のエコシステムがまだ発展途上
  • 設定複雑さ: 本格的な設定には一定の専門知識が必要

参考ページ

書き方の例

基本セットアップ

# 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()
        }
    }
}