Actix web で HttpOnly な Cookie を設定する
はじめに
最近 Rust を勉強するため、Actix web で Bloggimg という 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
[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 の属性に HttpOnly
と Secure
、SameSite=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 レスポンスヘッダーを確認したいため、開発者ツールを開いておきます。
HTTP レスポンスヘッダーに Set-Cookie が含まれていることを確認する
Set-Cookie
が含まれていることが確認できたら正常に Cookie が設定されているか確認します。
HTTP リクエストヘッダーの Cookie に cookie_test=sample
が存在していることを確認する
実際にブラウザーにも Cookie が正しく設定されているか、開発者ツールで確認する
正常に Cookie がセットされていることが確認できれば作業完了です。Cookie の属性に Secure
を設定した場合の動作検証は、環境変数に HTTPS=1
をセットして cargo run
で可能です。
おわりに
Actix web で割と汎用的に使えそうな知識として Cookie の設定方法について、メモ的な記事を書いてみました。引き続き、Rust への理解を深めるために Bloggimg の開発を進めながら学習を進めていきます 🧑🎓
参考リンク
- Install Rust - Rust Programming Language
- Rust - Visual Studio Marketplace
- VSCode で Rust インストールしたのに「Rustup not available」が出るとき (備忘録) - TAKOYAKING’s blog
- Set-Cookie - HTTP | MDN
- actix/actix-web: Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
- actix_web::http::Cookie - Rust
- ブラウザー開発者ツールとは? - ウェブ開発を学ぶ | MDN
Discussion