Actix web で HttpOnly な Cookie を設定する

公開:2021/01/23
更新:2021/01/23
8 min読了の目安(約7600字TECH技術記事

はじめに

最近 Rust を勉強するため、Actix webBloggimg という Web アプリケーションを作りました。その際、セッション管理のために Cookie を利用したのですが、その際の手順及び設定方法についてまとめておきます。

本記事では Rust や Actix web のインストール方法については説明しません。Mac であれば brew install rustup して rustup-init した後、PATH$HOME/.cargo/bin を追加するだけで大丈夫なはずです。詳細なインストール手順については 公式サイト をご参照ください。

開発環境については VSCode の Rust Plugin がオススメです。Rustup で Rust をインストールしている場合、設定から Rustup の PATH を $HOME/.cargo/bin/rustup にするだけで利用可能です。設定手順の詳細はこちらをご参照ください。

動作環境

  • Mac mini (M1, 2020)
    • Rust 1.49
    • Actix web 3
    • Serde 1.0
Cargo.toml
[package]
name = "cookie_test"
version = "0.1.0"
authors = ["nikaera"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "3"
serde = { version = "1.0", features = ["derive"] }

Actix web で Cookie をセットする

サーバー側で Cookie を設定するため、HTTP レスポンスヘッダーに Set-Cookie を含める形でセッション情報をクライアントへ渡します。その際、最低でも Cookie の属性に HttpOnlySecureSameSite=Strict は設定します。実際の Cookie を設定するための Actix web でのサンプルコードは下記になります。

use std::env;
use actix_web::{App, HttpServer};
use actix_web::cookie::{Cookie, SameSite};
use actix_web::{get, web, Error, HttpRequest, HttpResponse};

use serde::{Deserialize};

/// Cookie に設定するキー
/// 今回は cookie_test をキーとして使用する
/// 
const KEY: &str = "cookie_test";

/// 存在していれば、HTTP Request ヘッダーから Cookie 文字列を取得する関数
/// 
/// # Arguments
/// * `req` - actix_web::HttpRequest
/// 
/// # Return value
/// * Option<String> - key=value; key1=value1;~ のような Cookie の文字列
/// 
fn get_cookie_string_from_header(req: HttpRequest) -> Option<String> {
    let cookie_header = req.headers().get("cookie");
    if let Some(v) = cookie_header {
        let cookie_string = v.to_str().unwrap();
        return Some(String::from(cookie_string));
    }
    return None;
}

/// 存在していれば、特定のキーで Cookie に設定された値を取得するための関数
/// 
/// # Arguments
/// * `key` - Cookie から取り出したい値のキー
/// * `cookie_string` - get_cookie_string_from_header 関数で取得した Cookie の文字列
/// 
/// # Return value
/// * Option<String> - Cookie に設定されている値を取得する
/// 
fn get_cookie_value(key: &str, cookie_string: String) -> Option<String> {
    // 取得した Cookie 文字列を ; で分割してループで回す
    let kv: Vec<&str> = cookie_string.split(';').collect();
    for c in kv {
        // Cookie 文字列をパースして key で指定した値とマッチしたキーが存在するかチェックする
        match Cookie::parse(c) {
            Ok(kv) => {
                if key == kv.name() {
                    // key で指定した値とマッチしたキーが存在していたら、その値を取得する
                    return Some(String::from(kv.value()));
                }
            }
            Err(e) => {
                println!("cookie parse error. -> {}", e);
            }
        }
    }
    return None;
}

/// 特定のキーで環境変数から値を取得するための関数
/// 
/// # Arguments
/// * `key` - 環境変数から取り出したい値のキー
/// 
/// # Return value
/// * String - 環境変数の値を文字列として取得する
/// 
fn get_env(key: &str) -> String {
    match env::var(key) {
        Ok(value) => return value,
        Err(e) => println!("ENV: ERR {:?}", e),
    }
    return String::new();
}

/// 環境変数に設定された HTTPS の値が 1 か判定する
/// Cookie の属性に Secure を付与するか判定するのに使用する
/// 
/// # Return value
/// * bool - Secure 属性を付与するか判定するための真偽値
/// 
fn is_https() -> bool {
    return get_env("HTTPS") == "1";
}

/// Cookie に設定する値を扱う HTTP Query の定義
#[derive(Deserialize)]
pub struct CookieQuery {
    pub value: String,
}

/// Cookie を設定するために用意したルート
/// 
/// # Example
/// 
/// 例えば GET /cookie?value=test にアクセスした場合、
/// Cookie に cookie_test=test が設定されるようになる
/// 
#[get("/cookie")]
async fn set_cookie(query: web::Query<CookieQuery>) -> Result<HttpResponse, Error> {
    // 設定したい Cookie を作成する
    // その際に Secure, HttpOnly, SameSite=Strict 属性を付与する
    let cookie = Cookie::build(KEY, &query.value)
            .secure(is_https())
            .http_only(true)
            .same_site(SameSite::Strict)
            .finish();

    // 作成した Cookie を HTTP Response の Set-Cookie ヘッダーに含めることで、
    // HTTP Response を受け取ったクライアントに Cookie をセットさせる
    return Ok(HttpResponse::Ok()
        .header("Set-Cookie", cookie.to_string())
        .body(""));
}

/// KEY で指定した Cookie が存在すれば、その値を返却する
/// KEY で指定した Cookie が存在しなければ、空の文字列を返却する
#[get("/")]
async fn index(req: HttpRequest) -> Result<HttpResponse, Error> {
    let cookie_string = get_cookie_string_from_header(req);
    if let Some(s) = cookie_string {
        if let Some(v) = get_cookie_value(KEY, s) {
            return Ok(HttpResponse::Ok().body(v));
        }
    }
    return Ok(HttpResponse::Ok().body(""));
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(set_cookie)
            .service(index)
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

ザッとインラインコメントで説明していますが、
最も重要な set_cookie 関数について簡単に説明します。

Actix web には Cookie クラスが存在します。この Cookie クラスは Cookie 文字列を生成したり、パースしたりするのに役立ちます。set_cookie 関数では、Cookie を生成するための関数 Cookie::build を利用しています。

Cookie::build 関数を利用することで、メソッドチェインで Cookie の値や属性を設定できます。作成した Cookie は to_string 関数を使用することで文字列として出力できます。出力した Cookie 文字列を HTTP レスポンスヘッダーに Set-Cookie として設定すれば Cookie を設定できます。

動作検証

今回用意した Actix web のサンプルコードには 2つのエンドポイントを用意しました。

URI 説明
GET /cookie value クエリで HttpOnly な Cookie を設定する
GET / GET /cookie で設定した Cookie を確認する

cargo run で Actix web のサンプルを起動した後に、ブラウザで http://localhost:8080/cookie?value=sample にアクセスしてみます。またその際に HTTP レスポンスヘッダーを確認したいため、開発者ツールを開いておきます。

スクリーンショット 2021-01-23 13.12.27.png
HTTP レスポンスヘッダーに Set-Cookie が含まれていることを確認する

Set-Cookie が含まれていることが確認できたら正常に Cookie が設定されているか確認します。

スクリーンショット 2021-01-23 13.26.52.png
HTTP リクエストヘッダーの Cookie に cookie_test=sample が存在していることを確認する

スクリーンショット 2021-01-23 13.32.00.png
実際にブラウザーにも Cookie が正しく設定されているか、開発者ツールで確認する

正常に Cookie がセットされていることが確認できれば作業完了です。Cookie の属性に Secure を設定した場合の動作検証は、環境変数に HTTPS=1 をセットして cargo run で可能です。

おわりに

Actix web で割と汎用的に使えそうな知識として Cookie の設定方法について、メモ的な記事を書いてみました。引き続き、Rust への理解を深めるために Bloggimg の開発を進めながら学習を進めていきます 🧑‍🎓

本記事の内容がセキュリティの観点から適切でない場合等はコメントでご指摘いただけますと幸いです。

参考リンク