webbureaucrat
Posted on July 18, 2023
In 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 youcargo 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 aSessionMiddlewareBuilder
. 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
.
main.rs
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.
Posted on July 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.