Setting and Reading Session Cookies in Rust with Actix Web
actix-web rustIn case it's not readily apparent, I've been having some trouble picking a backend stack recently, but, at the risk of jinxing it, I really think Rust is the backend for me, and Actix Web is a blindingly fast MVC framework for Rust with an elegant and functional middleware system. As with all web apps, my first challenge is persisting and managing state between requests, so let's get into user session management in Actix Web.
Starting with Hello World in Rust with Actix Web
For completeness, I'm going to start with cargo new
. Feel free to skip
ahead.
You can start with cargo new actix-kata
.
If you enter your directory, you should find a "Hello, world" program. If you
cargo run
it, you should get "Hello, world" printed to the screen.
To add Actix Web to your project, run cargo add actix-web
. If you cargo build
or cargo run
again, it should install Actix Web and all its
dependencies.
Then replace src/main.rs with the following:
use actix_web;
#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
actix_web::HttpResponse::Ok().body("Hello, world")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
actix_web::HttpServer::new( || {
actix_web::App::new()
.service(index)
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
Getting started with actix-session
actix-session
is it's own Rust crate, separate from actix-web
. It has a
feature flag for cookie sessions. To use it,
install it with cargo add actix-session --features cookie-session
.
Our goal will be to build out a SessionMiddleware<CookieSessionStore>
. We can
do that with a
SessionMiddlewareBuilder
.
To put a fine point on it, let's break it out into its own function.
use actix_session::{ SessionMiddleware };
use actix_session::storage::{ CookieSessionStore };
use actix_web;
use actix_web::cookie::{ Key };
#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
actix_web::HttpResponse::Ok().body("Hello, world")
}
fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
SessionMiddleware::builder(
CookieSessionStore::default(), Key::from(&[0; 64])
)
.build()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
actix_web::HttpServer::new( || {
actix_web::App::new()
.wrap(session_middleware()) //register session middleware
.service(index)
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
This accepts all defaults and registers the session middleware in the application.
Building out a cookie session in Actix Web
Now let's fill in some details using the provided builder methods. (Some of these are set by default, but in the interest of a good demonstration, let's make as much explicit as we can.)
Importantly, many possible configuration items are matters of security both for the application and for the user, so it is a very good idea to familiarize oneself with each item in the documentation.
use actix_session::{ SessionMiddleware, Session };
use actix_session::config::{ BrowserSession, CookieContentSecurity };
use actix_session::storage::{ CookieSessionStore };
use actix_web;
use actix_web::cookie::{ Key, SameSite };
#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
actix_web::HttpResponse::Ok().body("Hello, world")
}
fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
SessionMiddleware::builder(
CookieSessionStore::default(), Key::from(&[0; 64])
)
.cookie_name(String::from("my-kata-cookie")) // arbitrary name
.cookie_secure(true) // https only
.session_lifecycle(BrowserSession::default()) // expire at end of session
.cookie_same_site(SameSite::Strict)
.cookie_content_security(CookieContentSecurity::Private) // encrypt
.cookie_http_only(true) // disallow scripts from reading
.build()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
actix_web::HttpServer::new( || {
actix_web::App::new()
.wrap(session_middleware())
.service(index)
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
Reading from a cookie in Actix Web
Reading from and writing to cookies is very simple with the Session
extractor. Just include a parameter typed as Session
, and the framework
will take care of the rest.
use actix_session::{ SessionMiddleware, Session };
use actix_session::config::{ BrowserSession, CookieContentSecurity };
use actix_session::storage::{ CookieSessionStore };
use actix_web::{ HttpResponse };
use actix_web::cookie::{ Key, SameSite };
use actix_web::web::{ Json };
#[actix_web::get("get_session")]
async fn get_session(session: Session) -> impl actix_web::Responder {
match session.get::<String>("message") {
Ok(message_option) => {
match message_option {
Some(message) => HttpResponse::Ok().body(message),
None => HttpResponse::NotFound().body("Not set.")
}
}
Err(_) => HttpResponse::InternalServerError().body("Session error.")
}
}
#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
HttpResponse::Ok().body("Hello, world")
}
fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
SessionMiddleware::builder(
CookieSessionStore::default(), Key::from(&[0; 64])
)
.cookie_name(String::from("my-kata-cookie"))
.cookie_secure(true)
.session_lifecycle(BrowserSession::default())
.cookie_same_site(SameSite::Strict)
.cookie_content_security(CookieContentSecurity::Private)
.cookie_http_only(true)
.build()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
actix_web::HttpServer::new( || {
actix_web::App::new()
.wrap(session_middleware())
.service(index)
.service(get_session)
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
Setting the session cookie in Actix Web
We can use serde
to create an endpoint to set the session. We'll also need
the derive
feature.
cargo add serde --features derive
At this point, your Cargo.toml should look like this:
[package]
name = "actix-kata"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-session = { version = "0.7.2", features = ["cookie-session"] }
actix-web = "4.3.1"
serde = { version = "1.0.164", features = ["derive"] }
With serde
, we can make a deserializable struct
to model our input and then
extract it simply by putting it in the signature of our method. The framework
will handle the rest.
Then we can insert the message using session.insert
.
use actix_session::{ SessionMiddleware, Session };
use actix_session::config::{ BrowserSession, CookieContentSecurity };
use actix_session::storage::{ CookieSessionStore };
use actix_web::{ HttpResponse };
use actix_web::cookie::{ Key, SameSite };
use actix_web::web::{ Json };
use serde;
#[derive(serde::Deserialize)]
struct CookieModel {
message: String
}
#[actix_web::get("get_session")]
async fn get_session(session: Session) -> impl actix_web::Responder {
match session.get::<String>("message") {
Ok(message_option) => {
match message_option {
Some(message) => HttpResponse::Ok().body(message),
None => HttpResponse::NotFound().body("Not set.")
}
}
Err(_) => HttpResponse::InternalServerError().body("Error.")
}
}
#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
HttpResponse::Ok().body("Hello, world")
}
async fn set_session(session: Session, model: Json<CookieModel>)
-> impl actix_web::Responder
{
match session.insert("message", model.message.clone()) {
Ok(_) => HttpResponse::Created().body("Created."),
Err(_) => HttpResponse::InternalServerError().body("Error.")
}
}
fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
SessionMiddleware::builder(
CookieSessionStore::default(), Key::from(&[0; 64])
)
.cookie_name(String::from("my-kata-cookie"))
.cookie_secure(true)
.session_lifecycle(BrowserSession::default())
.cookie_same_site(SameSite::Strict)
.cookie_content_security(CookieContentSecurity::Private)
.cookie_http_only(true)
.build()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
actix_web::HttpServer::new( || {
actix_web::App::new()
.wrap(session_middleware())
.service(index)
.service(get_session)
.route("set_session", actix_web::web::post().to(set_session))
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
Wrapping up
And that's really it. Make sure you set your mime type for the /set_session
request to application/json
if you're testing. This has been helpful to me
as I get into Actix Web middleware for the first time, so I hope it has been
helpful to you.
I write to learn, so I welcome your constructive criticism. Report issues on GitLab.