Web
The Arconia Multitenancy Web module adds HTTP-specific multitenancy support for Spring MVC applications. It provides tenant resolution from HTTP headers or cookies, a servlet filter for automatic context management, and a controller annotation for convenient access to the current tenant.
Dependencies
Add the Arconia Multitenancy Web Spring Boot Starter dependency to your project.
-
Gradle
-
Maven
dependencies {
implementation 'io.arconia:arconia-multitenancy-web-spring-boot-starter'
}
<dependency>
<groupId>io.arconia</groupId>
<artifactId>arconia-multitenancy-web-spring-boot-starter</artifactId>
</dependency>
The web starter includes the core multitenancy module, so all context, resolution, tenant details, and observability features are available automatically.
HTTP Tenant Resolution
The web module resolves the tenant identifier from each incoming HTTP request using a configurable strategy.
Header-Based Resolution
By default, the tenant identifier is resolved from the X-TenantId HTTP header.
-
HTTPie
-
curl
http :8080/api/resource X-TenantId:acme
curl -H "X-TenantId: acme" http://localhost:8080/api/resource
You can customize the header name:
arconia:
multitenancy:
resolution:
http:
header:
header-name: X-Custom-Tenant
Cookie-Based Resolution
Alternatively, you can resolve the tenant from an HTTP cookie by switching the resolution mode.
arconia:
multitenancy:
resolution:
http:
resolution-mode: cookie
By default, the cookie name is TENANT-ID. You can customize it:
arconia:
multitenancy:
resolution:
http:
resolution-mode: cookie
cookie:
cookie-name: MY-TENANT
OAuth2-Based Resolution
When the application is secured with OAuth2, the tenant can be resolved from a claim in the token that authenticated the request.
arconia:
multitenancy:
resolution:
http:
resolution-mode: oauth2
This works both for OAuth2 client applications, where the claim is read from the ID token and the UserInfo response, and for OAuth2 resource servers, where it is read from the access token, whether it is a JWT or an opaque token resolved via introspection. Because the claim is read from the authenticated principal rather than from the raw Authorization header, it also works for client applications that carry a session instead of a bearer token.
It requires Spring Security OAuth2 on the classpath, for example via the spring-boot-starter-security-oauth2-resource-server or spring-boot-starter-security-oauth2-client dependency.
By default, the claim name is tenant_id. You can customize it:
arconia:
multitenancy:
resolution:
http:
resolution-mode: oauth2
oauth2:
claim-name: tid
|
This mode requires the request to be authenticated before the tenant is resolved, so the |
Configuration Properties
| Property | Default | Description |
|---|---|---|
|
|
Whether HTTP tenant resolution is enabled. |
|
|
Mode of HTTP resolution. Options: |
|
|
Name of the HTTP header from which to resolve the current tenant. |
|
|
Name of the HTTP cookie from which to resolve the current tenant. |
|
|
Name of the OAuth2 token claim from which to resolve the current tenant. |
Tenant Context Filter
The TenantContextFilter is a servlet filter that intercepts incoming HTTP requests, resolves the tenant, validates the identifier via a TenantIdentifierValidator, optionally verifies it via a TenantVerifier, and establishes the tenant context for the duration of the request using a ScopedValue binding. It publishes TenantContextAttachedEvent on entry and TenantContextClosedEvent on completion, triggering all registered event listeners.
If the tenant identifier cannot be resolved (e.g., the header is missing), fails validation (e.g., it contains characters outside the accepted set), or fails verification (e.g., the tenant is not found or disabled), the filter returns an HTTP 400 Bad Request response with an application/problem+json body describing the error.
If the tenant registry itself is unavailable, for example because the database backing the TenantDetailsService cannot be reached, the filter returns an HTTP 503 Service Unavailable response instead. That distinguishes a caller sending a bad tenant identifier from an outage on the server side, and keeps a registry failure from surfacing as an unhandled error on every request.
The filter also runs on error dispatches, so error handling for a failed request executes with the same tenant bound as the request that failed. On an error dispatch the filter never writes a response of its own: if the tenant cannot be resolved or validated there, the request proceeds without a tenant context rather than replacing the response the container is already producing.
|
Asynchronous request handling is a separate case. A |
Filter Order
The TenantContextFilter is auto-configured as a standard servlet filter, registered after the Spring Security filter chain. That is what the oauth2 resolution mode requires, since the tenant claim can only be read once the request has been authenticated.
You can change where the filter runs relative to the other servlet filters:
arconia:
multitenancy:
resolution:
http:
filter:
order: -101
Spring Security registers its own filter chain at order -100, so a lower value makes the tenant context available to it. Be aware that the tenant is then resolved for every request reaching the filter, including the login and OAuth2 redirect endpoints. Those are part of the ignored paths by default, so authentication keeps working, but any other endpoint that must be reachable before a tenant is known has to be added to the ignored paths as well.
Spring Security Integration
When the tenant context must be available during security processing, for example for tenant-aware authorization, register the filter inside the security filter chain instead. Place it after the AnonymousAuthenticationFilter, which runs after every authentication filter and before the AuthorizationFilter, so that both the authenticated principal and the tenant context are available where they are needed:
import io.arconia.multitenancy.web.context.filters.TenantContextFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, TenantContextFilter tenantContextFilter) throws Exception {
return http
.addFilterAfter(tenantContextFilter, AnonymousAuthenticationFilter.class)
// ... other security configuration
.build();
}
@Bean
FilterRegistrationBean<TenantContextFilter> tenantContextFilterRegistration(TenantContextFilter filter) {
var registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
The second bean disables the registration that Spring Boot would otherwise create for the filter bean, so the filter is applied only by the security filter chain. Without it, the filter would also be registered as a standard servlet filter. That does not run it twice, since TenantContextFilter is a OncePerRequestFilter, but it does mean requests not matched by any SecurityFilterChain would be handled at a different point in the request lifecycle, with no authenticated principal available. The arconia.multitenancy.resolution.http.filter.order property no longer applies once the filter is placed in the security filter chain.
If you need the tenant before authentication, for example to select which authorization server validates the token, use the header or cookie resolution mode and place the filter early in the chain. Such a tenant identifier comes from unverified request data: it is safe to use it to select a trusted issuer, because a token minted for another tenant will not validate against the selected keys, but it must never be trusted as the tenant identity on its own. Always resolve it through a TenantDetailsService so that only known tenants are accepted, and never use it to build an issuer URL directly.
Ignoring Paths
Certain paths can be excluded from tenant resolution. By default, the following paths are ignored:
-
/actuator/** -
/webjars/** -
/css/** -
/js/** -
/*/.ico -
/login -
/oauth2/authorization/** -
/login/oauth2/code/**
The login and OAuth2 endpoints are ignored so that a user can still authenticate when no tenant has been established yet. At the default filter order those endpoints are handled inside the Spring Security filter chain and never reach the tenant filter, but they do reach it when the login page is served by your own controller, or when the filter is ordered ahead of the security chain.
Patterns are matched against the path within the application, so they keep working when the application is deployed under a context path. With server.servlet.context-path=/app, the /actuator/** pattern still matches a request to /app/actuator/health.
You can add additional paths to ignore without replacing the defaults:
arconia:
multitenancy:
resolution:
http:
filter:
additional-ignore-paths:
- /health
- /public/**
You can also replace the default ignored paths entirely:
arconia:
multitenancy:
resolution:
http:
filter:
ignore-paths:
- /actuator/**
- /public/**
Disabling the Filter
You can disable the tenant context filter while keeping the rest of the multitenancy configuration active:
arconia:
multitenancy:
resolution:
http:
filter:
enabled: false
Configuration Properties
| Property | Default | Description |
|---|---|---|
|
|
Whether the HTTP filter resolving the current tenant is enabled. |
|
|
Order of the HTTP filter resolving the current tenant. By default, the filter runs after the Spring Security filter chain, which is required when resolving the tenant from an OAuth2 token. |
|
|
HTTP request paths for which tenant resolution will not be performed. |
|
|
Additional HTTP request paths for which tenant resolution will not be performed. |
@TenantIdentifier Annotation
In Spring MVC controllers, you can inject the current tenant identifier directly into handler method parameters using the @TenantIdentifier annotation.
import io.arconia.multitenancy.web.context.annotations.TenantIdentifier;
@RestController
class TenantController {
@GetMapping("/tenant")
String currentTenant(@TenantIdentifier String tenantId) {
return tenantId;
}
}
The annotation resolves the value from the TenantContext and supports String and Optional<String> parameters.
A String parameter is null when no tenant is bound, which happens on an ignored path or when the filter is disabled. This matches how Spring Security’s @AuthenticationPrincipal behaves for an unauthenticated request. Declare the parameter as Optional<String> on handler methods that can be reached both with and without a tenant, so that the absence is visible in the signature:
@GetMapping("/status")
String status(@TenantIdentifier Optional<String> tenantId) {
return tenantId.map("Serving tenant "::concat).orElse("Serving no particular tenant");
}
You can also use the TenantContext directly to access the tenant identifier from any component, not just controllers. The @TenantIdentifier annotation is a convenience for Spring MVC handler methods.
|
Custom Tenant Resolution
You can implement your own HTTP tenant resolution strategy by providing a custom HttpRequestTenantResolver bean. This replaces the default header or cookie resolver.
import io.arconia.multitenancy.web.context.resolvers.HttpRequestTenantResolver;
@Bean
HttpRequestTenantResolver customTenantResolver() {
return request -> {
// Custom logic to extract tenant from the request
return request.getParameter("tenant");
};
}
HttpRequestTenantResolver is a specialization of the core TenantResolver<T> interface for HttpServletRequest sources.