背景
Actix-webでBasic認証を実装しようとしたけど情報があまり出てこなかったのでメモ
Basic認証とは?
Mozillaの説明 が非常に分かりやすいのでそちらを参照するのが良いと思われます。。
簡単に言うと、クライアントがユーザ名とパスワードをBase64エンコードして送信し、サーバがそれをデコードしてパスワードを比較するという認証方法です。
パスワードが暗号化されないので、セキュリティ的にはあまり好ましい認証方式ではありません。
クライアント側がBasic認証しか対応していないなど特別な場合を除き、より安全な認証方式を用いるべきです。
実装
まず基本的なサーバを作る
まずはActix-webのREADME を参考に、基本的なHTTPサーバを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use actix_web::{get, App, HttpServer};#[get("/" )] async fn greet () -> String { format! ("Hello world!" ) } #[actix_web::main] async fn main () -> std::io::Result <()> { HttpServer::new (|| { App::new ().service (greet) }) .bind (("127.0.0.1" , 8080 ))? .run () .await }
このプログラムを動かして、http://127.0.0.1:8080 にアクセスしてみると、"Hello, world!"と表示されると思います。
Basic認証を行う
Actix-webでBasic認証を行うには、actix_web_httpauth を使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use actix_web::{get, App, HttpServer};use actix_web_httpauth::extractors::basic::{BasicAuth, Config};use actix_web_httpauth::extractors::AuthenticationError;#[get("/" )] async fn greet (auth: BasicAuth) -> Result <String , Error> { let user = auth.user_id ().as_ref (); let password = match auth.password () { Some (p) => p.as_ref ().trim (), None => "" }; if user != "foo" || password != "bar" { return Err (AuthenticationError::from (Config::default ()).into ()); } format! ("Hello, {user}!" ) }
アプリケーション全体、または特定のスコープ全体にBasic認証を設けたい場合は、HttpAuthentication というミドルウェアが便利です。
(参考:https://turreta.com/2020/06/07/actix-web-basic-and-bearer-authentication-examples/ )
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 use actix_web::dev::ServiceRequest;use actix_web::{get, App, Error, HttpServer};use actix_web_httpauth::extractors::basic::{BasicAuth, Config};use actix_web_httpauth::extractors::AuthenticationError;use actix_web_httpauth::middleware::HttpAuthentication;async fn validator (req: ServiceRequest, auth: BasicAuth) -> Result <ServiceRequest, Error> { let user = auth.user_id ().as_ref (); let password = match auth.password () { Some (p) => p.as_ref ().trim (), None => "" }; if user == "foo" && password == "bar" { Ok (req) } else { Err (AuthenticationError::from (Config::default ()).into ()) } } #[get("/" )] async fn greet (auth: BasicAuth) -> Result <String , Error> { let user = auth.user_id ().as_ref (); format! ("Hello, {user}" ) } #[actix_web::main] async fn main () -> std::io::Result <()> { HttpServer::new (|| { let auth = HttpAuthentication::basic (validator); App::new () .wrap (auth) .service (greet) }) .bind (("127.0.0.1" , 8080 ))? .run () .await }
realmを指定する
上記のプログラムの場合、ユーザ名とパスワードのフォームに表示されるメッセージ(realm)は指定されていません。
この場合、メッセージの内容はクライアント依存(多くの場合メッセージなし)になります。
メッセージの内容を変更したい場合は、Config::realm()メソッド を使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async fn validator (req: ServiceRequest, credentials: BasicAuth) -> Result <ServiceRequest, Error> { let user = auth.user_id ().as_ref (); let password = match auth.password () { Some (p) => p.as_ref ().trim (), None => "" }; if user == "foo" && password == "bar" { Ok (req) } else { let config = Config::default ().realm ("ユーザ名とパスワードを入力してください。" ); Err (AuthenticationError::from (config).into ()) } }
最後に
最初の方にも述べましたが、Basic認証はセキュリティ上好ましくない認証方式です。
以下のような認証方式が使用可能な場合は、そちらを実装することを推奨します。
bcrypt暗号化に基づくフォーム認証
APIトークンを使用したBearer認証(いわゆる「JWT認証」などもこれに含まれる)
事前のアクセストークン発行に基づくOAuth認証
デバイス認証
MFA (多要素認証)