Micronaut session

HTTP Session support for Micronaut

Version:

1 Introduction

By default, Micronaut is a stateless HTTP server, however depending on your application requirements you may need the notion of HTTP sessions.

2 Http Sessions

Micronaut provides this module inspired by Spring Session that enables this which currently has two implementations:

  • In-Memory sessions - which you should combine with a sticky session proxy if you plan to run multiple instances.

  • Redis sessions - In this case Redis stores sessions, and non-blocking I/O is used to read/write sessions to Redis.

2.1 Enabling Sessions

To enable support for in-memory sessions you just need the micronaut-session dependency:

implementation("io.micronaut.session:micronaut-session")
<dependency>
    <groupId>io.micronaut.session</groupId>
    <artifactId>micronaut-session</artifactId>
</dependency>

2.2 Redis Sessions

To store Session instances in Redis, use the Micronaut Redis module which includes detailed instructions.

To quickly get up and running with Redis sessions you must also have the redis-lettuce dependency in your build:

build.gradle
compile "io.micronaut-session:micronaut-session"
compile "io.micronaut.redis:micronaut-redis-lettuce"

And enable Redis sessions via configuration in the application configuration file:

Enabling Redis Sessions
redis.uri=redis://localhost:6379
micronaut.session.http.redis.enabled=true
redis:
  uri: redis://localhost:6379
micronaut:
  session:
    http:
      redis:
        enabled: true
[redis]
  uri="redis://localhost:6379"
[micronaut]
  [micronaut.session]
    [micronaut.session.http]
      [micronaut.session.http.redis]
        enabled=true
redis {
  uri = "redis://localhost:6379"
}
micronaut {
  session {
    http {
      redis {
        enabled = true
      }
    }
  }
}
{
  redis {
    uri = "redis://localhost:6379"
  }
  micronaut {
    session {
      http {
        redis {
          enabled = true
        }
      }
    }
  }
}
{
  "redis": {
    "uri": "redis://localhost:6379"
  },
  "micronaut": {
    "session": {
      "http": {
        "redis": {
          "enabled": true
        }
      }
    }
  }
}

2.3 Configuring Session Resolution

Session resolution can be configured with HttpSessionConfiguration.

By default, sessions are resolved using an HttpSessionFilter that looks for session identifiers via either an HTTP header (using the Authorization-Info or X-Auth-Token headers) or via a Cookie named SESSION.

You can disable either header resolution or cookie resolution via configuration in the application configuration file:

Disabling Cookie Resolution
micronaut.session.http.cookie=false
micronaut.session.http.header=true
micronaut:
  session:
    http:
      cookie: false
      header: true
[micronaut]
  [micronaut.session]
    [micronaut.session.http]
      cookie=false
      header=true
micronaut {
  session {
    http {
      cookie = false
      header = true
    }
  }
}
{
  micronaut {
    session {
      http {
        cookie = false
        header = true
      }
    }
  }
}
{
  "micronaut": {
    "session": {
      "http": {
        "cookie": false,
        "header": true
      }
    }
  }
}

The above configuration enables header resolution, but disables cookie resolution. You can also configure header and cookie names.

2.4 Working with Sessions

A Session can be retrieved with a parameter of type Session in a controller method. For example consider the following controller:

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.session.Session;
import io.micronaut.session.annotation.SessionValue;
import io.micronaut.core.annotation.Nullable;

@Controller("/shopping")
public class ShoppingController {
    private static final String ATTR_CART = "cart"; // (1)

    @Post("/cart/{name}")
    Cart addItem(Session session, String name) { // (2)
        Cart cart = session.get(ATTR_CART, Cart.class).orElseGet(() -> { // (3)
            Cart newCart = new Cart();
            session.put(ATTR_CART, newCart); // (4)
            return newCart;
        });
        cart.getItems().add(name);
        return cart;
    }

}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.session.Session
import io.micronaut.session.annotation.SessionValue

import jakarta.annotation.Nullable

@Controller("/shopping")
class ShoppingController {
    private static final String ATTR_CART = "cart" // (1)

    @Post("/cart/{name}")
    Cart addItem(Session session, String name) { // (2)
        Cart cart = session.get(ATTR_CART, Cart).orElseGet({ -> // (3)
            Cart newCart = new Cart()
            session.put(ATTR_CART, newCart) // (4)
            newCart
        })
        cart.items << name
        cart
    }

}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.session.Session
import io.micronaut.session.annotation.SessionValue

@Controller("/shopping")
class ShoppingController {

    companion object {
        private const val ATTR_CART = "cart" // (1)
    }

    @Post("/cart/{name}")
    internal fun addItem(session: Session, name: String): Cart { // (2)
        require(name.isNotBlank()) { "Name cannot be blank" }
        val cart = session.get(ATTR_CART, Cart::class.java).orElseGet { // (3)
            val newCart = Cart()
            session.put(ATTR_CART, newCart) // (4)
            newCart
        }
        cart.items.add(name)
        return cart
    }

}
1 ShoppingController declares a Session attribute named cart
2 The Session is declared as a method parameter
3 The cart attribute is retrieved
4 Otherwise a new Cart instance is created and stored in the session

Note that because the Session is declared as a required parameter, to execute the controller action a Session will be created and saved to the SessionStore.

If you don’t want to create unnecessary sessions, declare the Session as @Nullable in which case a session will not be created and saved unnecessarily. For example:

Using @Nullable with Sessions
@Post("/cart/clear")
void clearCart(@Nullable Session session) {
    if (session != null) {
        session.remove(ATTR_CART);
    }
}
Using @Nullable with Sessions
@Post("/cart/clear")
void clearCart(@Nullable Session session) {
    session?.remove(ATTR_CART)
}
Using @Nullable with Sessions
@Post("/cart/clear")
internal fun clearCart(session: Session?) {
    session?.remove(ATTR_CART)
}

The above method only injects a new Session if one already exists.

2.5 Session Clients

If the client is a web browser, sessions should work if cookies are enabled. However, for programmatic HTTP clients you need to propagate the session ID between HTTP calls.

For example, when invoking the viewCart method of the StoreController in the previous example, the HTTP client receives by default a AUTHORIZATION_INFO header. The following example demonstrates this:

Retrieving the AUTHORIZATION_INFO header
when: "The shopping cart is retrieved"
HttpResponse<Cart> response = client.exchange(HttpRequest.GET('/shopping/cart'), Cart) // (1)
                                        .blockFirst()
Cart cart = response.body()

then: "The shopping cart is present as well as a session id header"
response.header(HttpHeaders.AUTHORIZATION_INFO) != null // (2)
cart != null
cart.items.isEmpty()
1 A request is made to /shopping/cart
2 The AUTHORIZATION_INFO header is present in the response

You can then pass this AUTHORIZATION_INFO in subsequent requests to reuse the existing Session:

Sending the AUTHORIZATION_INFO header
String sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) // (1)

response = client.exchange(HttpRequest.POST('/shopping/cart/Apple', "")
                 .header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart) // (2)
                 .blockFirst()
cart = response.body()
1 The AUTHORIZATION_INFO is retrieved from the response
2 And then sent as a header in the subsequent request

2.6 Using @SessionValue

Rather than explicitly injecting the Session into a controller method, you can instead use @SessionValue. For example:

Using @SessionValue
@Get("/cart")
@SessionValue(ATTR_CART) // (1)
Cart viewCart(@SessionValue @Nullable Cart cart) { // (2)
    if (cart == null) {
        cart = new Cart();
    }
    return cart;
}
Using @SessionValue
@Get("/cart")
@SessionValue("cart") // (1)
Cart viewCart(@SessionValue @Nullable Cart cart) { // (2)
    cart ?: new Cart()
}
Using @SessionValue
@Get("/cart")
@SessionValue(ATTR_CART) // (1)
internal fun viewCart(@SessionValue cart: Cart?): Cart { // (2)
    return cart ?: Cart()
}
1 @SessionValue is declared on the method resulting in the return value being stored in the Session. Note that you must specify the attribute name when used on a return value
2 @SessionValue is used on a @Nullable parameter which results in looking up the value from the Session in a non-blocking way and supplying it if present. In the case a value is not specified to @SessionValue resulting in the parameter name being used to lookup the attribute.

2.7 Session Events

You can register ApplicationEventListener beans to listen for Session related events located in the io.micronaut.session.event package.

The following table summarizes the events:

Table 1. Session Events
Type Description

SessionCreatedEvent

Fired when a Session is created

SessionDeletedEvent

Fired when a Session is deleted

SessionExpiredEvent

Fired when a Session expires

SessionDestroyedEvent

Parent of both SessionDeletedEvent and SessionExpiredEvent

3 Release History

For this project, you can find a list of releases (with release notes) here:

4 Repository

You can find the source code of this project in this repository: