Contents
Actix Web と JWT の基礎知識
Actix Web における認証は、トークンの構造と正しい運用方法を理解したうえで実装しなければなりません。本セクションでは JWT がどのように構成されているか と、Rust エコシステムで広く利用されているクレート群の選定基準について解説します。安全な設計を行うことで、後続の認可ロジックやトークン管理が格段に楽になります。
JWT の構造と典型的な利用シーン
JWT は Header・Payload(Claims)・Signature の 3 部構成です。以下の表は代表的なユースケースをまとめたものです。
| シナリオ | 主目的 | メリット |
|---|---|---|
| SPA の API 認証 | フロントエンドが取得したトークンを Authorization: Bearer <token> で送信 |
ステートレスで水平スケーリングが容易 |
| マイクロサービス間の認可情報伝搬 | サービス同士がユーザー属性やスコープを共有 | 中央認可サーバーが不要になるケースも |
| モバイルアプリの短期アクセストークン | 短時間有効なアクセストークンと長期間有効なリフレッシュトークンを組み合わせる | トークン漏洩時の被害範囲を限定できる |
ポイント:JWT は自己完結型であり、サーバ側にセッション情報を保持しないため、スケールアウトがシンプルになります。
必要クレートとバージョン管理の指針
以下は 2026 年 7 月時点で安定版として広く採用されているクレートです。正確なマイナーバージョンは crates.io の最新情報を確認してロックしてください。dangerous_insecure_disable_validation フラグはテスト以外で決して有効にしません。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
[dependencies] actix-web = { version = "^4", default-features = false, features = ["rustls"] } # TLS 対応 jsonwebtoken = { version = "^9", default-features = false } # HS256/RS256 どちらでも利用可 serde = { version = "^1", features = ["derive"] } serde_json = "^1" dotenvy = "^0.15" uuid = { version = "^1", features = ["v4"] } # jti 用 UUID time = "^0.3" once_cell = "^1" # 設定の遅延初期化に使用 tracing = "^0.1" tracing-subscriber = { version = "^0.3", features = ["env-filter"] } |
ポイント:
actix-webとjsonwebtokenの組み合わせは公式ドキュメントで相互互換性が確認されています。セキュリティ修正を含むマイナーバージョンの更新は CI で自動的に検知できるように設定しましょう。
プロジェクト設定と環境変数管理
実務では コードにシークレットを書き込まない ことが最優先です。この章では Cargo.toml の記述例と、.env を用いた安全なキー管理手順を示します。
Cargo.toml に必須依存関係を追加する手順
まずはプロジェクトのルートで cargo init --bin を実行し、上記表に従って Cargo.toml に追記してください。デフォルト機能は最小限に抑え、必要な feature のみ有効化します。
|
1 2 3 4 5 6 7 8 9 10 11 |
[dependencies] actix-web = { version = "^4", default-features = false, features = ["rustls"] } jsonwebtoken = { version = "^9", default-features = false } # 本番環境では検証を無効化しないこと serde = { version = "^1", features = ["derive"] } dotenvy = "^0.15" uuid = { version = "^1", features = ["v4"] } time = "^0.3" once_cell = "^1" tracing = "^0.1" tracing-subscriber = { version = "^0.3", features = ["env-filter"] } |
rustlsを有効にすると、開発環境でも HTTPS が組み込まれた状態でテストできます。dangerous_insecure_disable_validationは テストコード以外では絶対に使用しない ことがベストプラクティスです。
dotenvy によるシークレットロード例
.env ファイルはリポジトリにコミットしないよう必ず .gitignore に追加してください。以下は HS256 用シークレットと RSA キーのパス、トークン有効期限を管理するサンプルです。
|
1 2 3 4 5 6 7 |
# .env(プロジェクトルート) JWT_SECRET=super_secret_key_32bytes_long! RSA_PRIVATE_KEY_PATH=./keys/private.pem RSA_PUBLIC_KEY_PATH=./keys/public.pem TOKEN_EXP_SECONDS=900 # 15 分 REFRESH_TOKEN_EXP_DAYS=30 # 30 日 |
Rust 側で環境変数を取得し、once_cell::sync::Lazy にキャッシュする実装例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
use dotenvy::dotenv; use once_cell::sync::Lazy; use std::{env, fs}; pub struct JwtConfig { pub secret: String, pub private_key: Vec<u8>, pub public_key: Vec<u8>, pub access_exp: i64, pub refresh_exp_days: i64, } // アプリ起動時に一度だけロード pub static CONFIG: Lazy<JwtConfig> = Lazy::new(|| { dotenv().ok(); let secret = env::var("JWT_SECRET").expect("JWT_SECRET not set"); let private_key_path = env::var("RSA_PRIVATE_KEY_PATH") .expect("RSA_PRIVATE_KEY_PATH not set"); let public_key_path = env::var("RSA_PUBLIC_KEY_PATH") .expect("RSA_PUBLIC_KEY_PATH not set"); JwtConfig { secret, private_key: fs::read(private_key_path).expect("Failed to read private key"), public_key : fs::read(public_key_path ).expect("Failed to read public key"), access_exp: env::var("TOKEN_EXP_SECONDS") .unwrap_or_else(|_| "900".into()) .parse() .expect("Invalid TOKEN_EXP_SECONDS"), refresh_exp_days: env::var("REFRESH_TOKEN_EXP_DAYS") .unwrap_or_else(|_| "30".into()) .parse() .expect("Invalid REFRESH_TOKEN_EXP_DAYS"), } }); |
ポイント:
Lazyにより環境変数の取得は一度だけ行われ、後続のハンドラから&CONFIGで高速に参照できます。
トークン発行ロジックとクレーム設計
この章では アクセストークン/リフレッシュトークン を分離し、最小限のクレームだけを保持する実装例と、リプレイ防止に必要な jti の扱い方を示します。
Claims 構造体とトークン生成関数
まずは JWT に埋め込むクレーム構造体です。jti は UUID で生成し、後述のブラックリストで管理できるようにしています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
use jsonwebtoken::{EncodingKey, Header}; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; use uuid::Uuid; /// JWT に格納するクレーム。最小限の情報だけを保持します。 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Claims { // 標準クレーム pub sub: String, // ユーザー ID(必須) pub exp: i64, // 有効期限 (Unix epoch) pub iat: i64, // 発行時刻 pub jti: String, // JWT ID(リプレイ防止用) // カスタムクレームは必要最小限に留める #[serde(skip_serializing_if = "Option::is_none")] pub role: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub scopes: Option<Vec<String>>, } /// アクセストークン(HS256)を生成する関数 pub fn create_access_token(user_id: &str) -> String { let now = OffsetDateTime::now_utc(); let exp = now + Duration::seconds(CONFIG.access_exp); let claims = Claims { sub: user_id.to_owned(), iat: now.unix_timestamp(), exp: exp.unix_timestamp(), jti: Uuid::new_v4().to_string(), role: Some("user".into()), scopes: Some(vec!["read".into(), "write".into()]), }; jsonwebtoken::encode( &Header::default(), &claims, &EncodingKey::from_secret(CONFIG.secret.as_bytes()), ) .expect("Failed to encode access token") } /// リフレッシュトークン(RS256)を生成する関数 pub fn create_refresh_token(user_id: &str) -> String { let now = OffsetDateTime::now_utc(); let exp = now + Duration::days(CONFIG.refresh_exp_days); let claims = Claims { sub: user_id.to_owned(), iat: now.unix_timestamp(), exp: exp.unix_timestamp(), jti: Uuid::new_v4().to_string(), role: None, scopes: None, }; jsonwebtoken::encode( &Header::new(jsonwebtoken::Algorithm::RS256), &claims, &EncodingKey::from_rsa_pem(&CONFIG.private_key) .expect("Invalid RSA private key"), ) .expect("Failed to encode refresh token") } |
ポイント:
jtiを UUID にすることで、サーバ側で一意に管理でき、リプレイ攻撃の検知が容易になります。
トークン検証とブラックリスト実装例
トークン検証時に jti がブラックリストに存在すれば即座に失効させます。以下は Redis を利用したシンプルな実装です。Redis の接続はアプリ起動時に一度だけ作成し、once_cell::sync::Lazy で保持します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
use jsonwebtoken::{DecodingKey, Validation}; use redis::{Commands, Client as RedisClient}; use std::time::Duration as StdDuration; use once_cell::sync::Lazy; // Redis 接続プール(起動時に作成) static REDIS: Lazy<redis::Connection> = Lazy::new(|| { let client = RedisClient::open("redis://127.0.0.1/").expect("Invalid Redis URL"); client.get_connection().expect("Failed to connect to Redis") }); /// `jti` がブラックリストに登録されているか確認する fn is_blacklisted(jti: &str) -> redis::RedisResult<bool> { REDIS.sismember::<_, _, bool>("jwt:blacklist", jti) } /// `jti` をブラックリストへ追加し、TTL(例:1 時間)を設定 pub fn blacklist_jti(jti: &str, ttl_seconds: usize) -> redis::RedisResult<()> { let _: () = REDIS.sadd("jwt:blacklist", jti)?; // TTL を設定して自動削除させる REDIS.expire("jwt:blacklist", ttl_seconds as usize) } /// トークンを検証し、`jti` がブラックリストに無いことも確認する pub fn validate_token(token: &str) -> Result<Claims, actix_web::Error> { let mut validation = Validation::default(); // HS256 か RS256 かは環境変数で切り替え可能 let key = if !CONFIG.secret.is_empty() { DecodingKey::from_secret(CONFIG.secret.as_bytes()) } else { DecodingKey::from_rsa_pem(&CONFIG.public_key) .expect("Invalid RSA public key") }; let data = jsonwebtoken::decode::<Claims>(token, &key, &validation) .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?; // リプレイ防止チェック if is_blacklisted(&data.claims.jti).unwrap_or(false) { return Err(actix_web::error::ErrorUnauthorized( "Token has been revoked", )); } Ok(data.claims) } |
ログアウト時のブラックリスト登録例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
use actix_web::{post, HttpResponse}; #[post("/logout")] async fn logout(user: AuthenticatedUser) -> HttpResponse { // jti を 24 時間分だけ保持する(トークン有効期限に合わせる) let ttl = 60 * 60 * 24; if blacklist_jti(&user.0.jti, ttl).is_ok() { HttpResponse::Ok().body("Logged out") } else { HttpResponse::InternalServerError().finish() } } |
ポイント:Redis の集合型
SADDとEXPIREを組み合わせるだけで、スケーラブルかつ低遅延なブラックリストが実現できます。
認証ミドルウェアとリクエストフロー
Actix Web の Middleware と Extractor を活用すれば、全ての保護対象エンドポイントで統一的にトークン検証を行えます。この章では実装テンプレートとエラーハンドリング例を示します。
カスタム Middleware の実装
以下は Transform と Service を組み合わせたミドルウェアです。リクエスト受信時に Authorization ヘッダーを取得し、先ほど実装した validate_token で検証します。失敗した場合は 401 を即返却します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
use actix_service::{Service, Transform}; use actix_web::{ dev::{ServiceRequest, ServiceResponse}, Error, HttpMessage, }; use futures_util::future::{ok, Ready, LocalBoxFuture}; /// ミドルウェア本体。設定は `JwtConfig` をクローンして保持 #[derive(Clone)] pub struct JwtAuthMiddleware; impl<S, B> Transform<S, ServiceRequest> for JwtAuthMiddleware where S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type InitError = (); type Transform = JwtAuth<S>; type Future = Ready<Result<Self::Transform, Self::InitError>>; fn new_transform(&self, service: S) -> Self::Future { ok(JwtAuth { service }) } } pub struct JwtAuth<S> { service: S, } impl<S, B> Service<ServiceRequest> for JwtAuth<S> where S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready( &self, ctx: &mut std::task::Context<'_>, ) -> std::task::Poll<Result<(), Self::Error>> { self.service.poll_ready(ctx) } fn call(&self, mut req: ServiceRequest) -> Self::Future { // 1️⃣ Authorization ヘッダー取得 let token_opt = req .headers() .get("Authorization") .and_then(|hv| hv.to_str().ok()) .filter(|s| s.starts_with("Bearer ")) .map(|s| &s[7..]); // 2️⃣ トークン検証ロジック let result = match token_opt { Some(t) => validate_token(t).map(|c| req.extensions_mut().insert(c)), None => Err(actix_web::error::ErrorUnauthorized( "Missing or malformed Authorization header", )), }; // 3️⃣ 成功ならハンドラへ、失敗ならエラー返却 let fut = match result { Ok(_) => self.service.call(req), Err(e) => Box::pin(async move { Err(e) }), }; Box::pin(fut) } } |
ポイント:
req.extensions_mut().insert(claims)により、後続ハンドラはHttpRequest::extensions()から安全にクレームを取得できます。
Extractor と組み合わせた型安全な認可
Extractor を実装するとエンドポイント側で明示的に Claims が存在することを保証でき、コードがすっきりします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use actix_web::{dev, Error, FromRequest, HttpRequest}; use futures_util::future::{ok, Ready}; /// 認証済みユーザー情報をラップした型 pub struct AuthenticatedUser(pub Claims); impl FromRequest for AuthenticatedUser { type Error = Error; type Future = Ready<Result<Self, Self::Error>>; fn from_request(req: &HttpRequest, _: &mut dev::Payload) -> Self::Future { match req.extensions().get::<Claims>() { Some(claims) => ok(AuthenticatedUser(claims.clone())), None => Err(actix_web::error::ErrorUnauthorized("Unauthenticated")), } } } |
エンドポイント例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use actix_web::{get, HttpResponse}; #[get("/protected")] async fn protected(user: AuthenticatedUser) -> HttpResponse { // RBAC 判定は Claims.role で実施 if let Some(role) = &user.0.role { if role != "admin" { return HttpResponse::Forbidden().body("Insufficient permissions"); } } HttpResponse::Ok() .json(serde_json::json!({ "msg": "ようこそ!", "user_id": user.0.sub })) } |
ポイント:Extractor が失敗した場合は自動的に 401 が返され、ハンドラ側のロジックがシンプルになります。
認可層とセキュリティ強化策
認証だけでなく、どのユーザーが何を実行できるか を正しく制御することが重要です。ここでは RBAC/Scope の具体的なチェック例と、トークンリプレイ防止・クレーム最小化のベストプラクティスを示します。
ロール/スコープによる共通認可関数
以下は Claims を受け取り、必要ロールまたはスコープが存在するかを判定するユーティリティです。ハンドラ内で呼び出すだけで一貫したエラーレスポンスが得られます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use actix_web::{Error, HttpResponse}; /// 必要ロールが付与されているか確認 pub fn require_role(claims: &Claims, required: &str) -> Result<(), Error> { match &claims.role { Some(r) if r == required => Ok(()), _ => Err(actix_web::error::ErrorForbidden("Role mismatch")), } } /// 必要スコープが付与されているか確認 pub fn require_scope(claims: &Claims, needed: &str) -> Result<(), Error> { match &claims.scopes { Some(sc) if sc.iter().any(|s| s == needed) => Ok(()), _ => Err(actix_web::error::ErrorForbidden("Scope insufficient")), } } |
エンドポイントでの利用例
|
1 2 3 4 5 6 7 8 9 |
#[get("/admin/data")] async fn admin_data(user: AuthenticatedUser) -> HttpResponse { if let Err(e) = require_role(&user.0, "admin") { return e.error_response(); } HttpResponse::Ok() .json(serde_json::json!({ "secret": "管理者専用データ" })) } |
ポイント:ロールやスコープは 最小権限の原則 に従い、トークンに含める情報は本当に必要なものだけに絞ります。
トークンリプレイ防止とクレーム最小化
| 手法 | 実装例・概要 | 防御効果 |
|---|---|---|
jti + ブラックリスト (Redis) |
発行時に UUID を付与し、ログアウトやパスワード変更時に blacklist_jti で無効化 |
特定トークンの即時失効が可能 |
| Nonce / One‑Time Token | 認可コードフローで nonce クレームを付与し、サーバ側で一度だけ使用できるよう管理 |
リプレイ攻撃の根本的防止 |
| クレーム最小化 | sub, exp, iat, jti のみ保持し、ロールは DB 参照に切り替える |
攻撃者が取得できる情報量を削減 |
ポイント:リプレイ防止策は HTTPS 前提だけでなく、サーバ側でも状態管理(Redis 等)を併用してトークンの即時失効を可能にします。
テスト・デバッグ・デプロイ戦略
安全な認証基盤は テスト自動化 と CI/CD パイプラインへの組み込み が不可欠です。この章ではユニットテスト、統合テストの書き方と Docker/Kubernetes 環境でのシークレット注入例を示します。
ユニットテストとトレースロギング
tracing クレートを導入し、開発・ステージング環境ではデバッグ情報を出力、本番環境では機密情報を除外する設定にします。
|
1 2 3 4 |
# Cargo.toml 追加分 tracing = "^0.1" tracing-subscriber = { version = "^0.3", features = ["env-filter"] } |
テストコード例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#[cfg(test)] mod tests { use super::*; use once_cell::sync::Lazy; // テスト用に環境変数だけで構築した設定(実際のキーはハードコードしない) static TEST_CONFIG: Lazy<JwtConfig> = Lazy::new(|| JwtConfig { secret: "test_secret_32bytes_long!".into(), private_key: vec![], public_key: vec![], access_exp: 900, refresh_exp_days: 30, }); #[test] fn access_token_roundtrip() { let token = create_access_token("user123"); let claims = validate_token(&token).expect("validation failed"); assert_eq!(claims.sub, "user123"); assert_eq!(claims.role.as_deref(), Some("user")); } #[test] fn refresh_token_expiry_check() { let token = create_refresh_token("user456"); let claims = validate_token(&token).expect("validation failed"); let now = OffsetDateTime::now_utc().unix_timestamp(); assert!(claims.exp - now > 20 * 24 * 60 * 60); // 少なくとも20日以上残っているはず } #[test] fn blacklist_prevents_use() { let token = create_access_token("user999"); let claims = validate_token(&token).expect("validation failed"); // ブラックリストへ登録 blacklist_jti(&claims.jti, 3600).unwrap(); // 再度検証 → エラーになるはず assert!(validate_token(&token).is_err()); } } |
ログ設定例
|
1 2 3 4 5 6 7 8 9 10 |
use tracing_subscriber::EnvFilter; pub fn init_tracing() { let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("actix_web=info,my_app=debug")); tracing_subscriber::fmt() .with_env_filter(filter) .init(); } |
RUST_LOG=trace cargo run とすれば詳細ログが出力されますが、本番環境では INFO 以上に制限し、トークン本文は出力しないように注意してください。
Docker / Kubernetes におけるシークレット注入
Dockerfile(マルチステージ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# ---- ビルドフェーズ ---- FROM rust:1.78 AS builder WORKDIR /app COPY Cargo.toml Cargo.lock ./ RUN cargo fetch COPY src ./src RUN cargo build --release # ---- 実行フェーズ ---- FROM gcr.io/distroless/cc-debian12 COPY --from=builder /app/target/release/my_service /usr/local/bin/ # コンテナ起動時に環境変数またはファイルでシークレットを注入 CMD ["my_service"] |
Kubernetes Secret と Deployment
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
apiVersion: v1 kind: Secret metadata: name: jwt-secret type: Opaque data: JWT_SECRET: c3VwZXJfc2VjcmV0X2tleV9iYXNlNjQ= # base64 エンコード済み --- apiVersion: apps/v1 kind: Deployment metadata: name: my-service spec: replicas: 3 selector: matchLabels: app: my-service template: metadata: labels: app: my-service spec: containers: - name: web image: ghcr.io/example/my_service:latest envFrom: - secretRef: name: jwt-secret ports: - containerPort: 8080 |
ポイント:シークレットは環境変数経由で注入し、コードベースに直接書かないことが基本です。
GitHub Actions による CI/CD パイプライン例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
name: CI on: push: branches: [main] jobs: build-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: Cache Cargo registry uses: actions/cache@v3 with: path: ~/.cargo/registry key: cargo-registry-${{ hashFiles('Cargo.lock') }} - name: Build (release) run: cargo build --release - name: Run tests env: JWT_SECRET: ${{ secrets.JWT_SECRET }} run: cargo test --quiet docker-publish: needs: build-test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build & push Docker image run: | docker build -t ghcr.io/${{ github.repository }}/my_service:${{ github.sha }} . docker push ghcr.io/${{ github.repository }}/my_service:${{ github.sha }} |
ポイント:GitHub Secrets に保存した
JWT_SECRETをテスト時に環境変数として注入することで、ローカルと同様の検証が可能です。
まとめ
- 構造把握:JWT の Header・Payload・Signature と典型的な利用シーンを理解し、ステートレス認証の利点を活かす。
- 安全な設定管理:
dotenvyとonce_cellを組み合わせて環境変数を遅延ロードし、.gitignoreで漏洩防止。 - トークン設計:アクセストークンは HS256、リフレッシュトークンは RS256 で分離。
jtiを UUID にしてブラックリスト(Redis)で即時失効できるようにする。 - ミドルウェア + Extractor:カスタム Middleware が全リクエストで検証を行い、Extractor が型安全な認可情報提供を実現。
- 認可ロジック:RBAC/Scope の共通関数化と最小権限の原則に従ったクレーム設計でコード重複を防止。
- リプレイ防止:
jtiブラックリスト、nonce、クレーム最小化の3層防御を導入し、HTTPS だけに依存しない堅牢性を確保。 - テスト・CI/CD:ユニットテストでトークン生成/検証・ブラックリスト動作を網羅し、
tracingによる可観測性を付与。Docker/Kubernetes では Secret 注入と GitHub Actions の自動デプロイで運用コストを低減。
以上のベストプラクティスに従えば、Actix Web アプリケーションは 安全・拡張性・可観測性 を兼ね備えた JWT 認証基盤として本番環境に耐える実装が完成します。