How to Isolate Session and JWT Users in Spring Security: Strategies and Pitfalls
This article explains how to separate backend admin users using stateful Session authentication from front‑end app users using stateless JWT in a Spring Security‑based system, covering path‑interception strategies, session key isolation, custom UserDetailsService implementations, and complete configuration examples for an Id Server authorization server.
In many business systems you may encounter two distinct user models: backend admin users that typically use stateful Session authentication, and front‑end app users that use the popular stateless JWT . These are completely separate isolation realms, and the article shows how to implement them safely.
Path Interception Strategies
Spring Security defines dedicated filter chains based on request‑path rules. Three ways to intercept paths are presented.
Regex Filtering
You can filter URI using HttpSecurity, for example to intercept requests whose query parameters contain id:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityFilterChain systemSecurityFilterChain(HttpSecurity http) throws Exception {
// ...
}Ant Pattern Filtering
The most common approach, e.g., intercept all paths that start with /system:
http.antMatcher("/system/**")RequestMatcher Filtering
Complex combinations can be built by defining a RequestMatcher implementation. Example of a composite rule:
RequestMatcher requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(providerSettings.getTokenEndpoint(), HttpMethod.POST.name()),
new AntPathRequestMatcher(providerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name()),
new AntPathRequestMatcher(providerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name())
);
http.requestMatcher(requestMatcher);Key Points for Isolation Configuration
Session
By default a Session relies on the jsessionid cookie. When using session mode you must isolate the session storage of multiple filter chains; otherwise different login states interfere.
The default security‑context key is SPRING_SECURITY_CONTEXT, which is shared across tabs. Define a unique key per filter chain to avoid this:
final String ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY = "SOME_UNIQUE_KEY";
HttpSessionSecurityContextRepository hs = new HttpSessionSecurityContextRepository();
hs.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
http.securityContext().securityContextRepository(hs);Stateless Token
Stateless tokens are simpler: the front‑end stores the token per path, and the token should contain information about the filter‑chain to prevent mixing.
UserDetailsService
If the different endpoints have independent users, implement separate UserDetailsService instances, but do not register them directly in Spring IoC. Define a dedicated interface, for example:
@FunctionalInterface
public interface OAuth2UserDetailsService {
UserDetails loadOAuth2UserByUsername(String username) throws UsernameNotFoundException;
}Inject this service into the corresponding filter chain:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
OAuth2UserDetailsService oAuth2UserDetailsService) throws Exception {
http.userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername);
// other configurations …
}Spring IoC still requires a fallback UserDetailsService bean; provide a not‑found implementation:
@Bean
UserDetailsService notFoundUserDetailsService() {
return username -> {
throw new UsernameNotFoundException("User not found");
};
}Other Configurations
Other settings follow their own requirements. The following example shows how an Id Server authorization server isolates admin, OAuth2, and front‑end users:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class IdServerSecurityConfiguration {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
private static final String SYSTEM_ANT_PATH = "/system/**";
public static final String ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY = "ID_SERVER_SYSTEM_SECURITY_CONTEXT";
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
@Bean("authorizationServerSecurityFilterChain")
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> cfg = new OAuth2AuthorizationServerConfigurer<>();
cfg.authorizationEndpoint(e -> e.consentPage(CUSTOM_CONSENT_PAGE_URI));
RequestMatcher matcher = cfg.getEndpointsMatcher();
http.requestMatcher(matcher)
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf(csrf -> csrf.ignoringRequestMatchers(matcher))
.formLogin()
.and()
.apply(cfg);
return http.build();
}
@Bean
ProviderSettings providerSettings(@Value("${server.port}") Integer port) {
return ProviderSettings.builder().issuer("http://localhost:" + port).build();
}
}
@Configuration(proxyBeanMethods = false)
static class SystemSecurityConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityFilterChain systemSecurityFilterChain(HttpSecurity http, UserInfoService userInfoService) throws Exception {
SimpleAuthenticationEntryPoint entryPoint = new SimpleAuthenticationEntryPoint();
AuthenticationEntryPointFailureHandler failureHandler = new AuthenticationEntryPointFailureHandler(entryPoint);
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
http.antMatcher(SYSTEM_ANT_PATH).csrf().disable()
.headers().frameOptions().sameOrigin()
.and()
.securityContext().securityContextRepository(repo)
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.userDetailsService(userInfoService::findByUsername)
.formLogin().loginPage("/system/login").loginProcessingUrl("/system/login")
.successHandler(new RedirectLoginAuthenticationSuccessHandler("/system"))
.failureHandler(failureHandler).permitAll();
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
static class OAuth2SecurityConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
OAuth2UserDetailsService oAuth2UserDetailsService,
@Qualifier("authorizationServerSecurityFilterChain") SecurityFilterChain authServerChain) throws Exception {
DefaultSecurityFilterChain authServerFilter = (DefaultSecurityFilterChain) authServerChain;
SimpleAuthenticationEntryPoint entryPoint = new SimpleAuthenticationEntryPoint();
AuthenticationEntryPointFailureHandler failureHandler = new AuthenticationEntryPointFailureHandler(entryPoint);
http.requestMatcher(new AndRequestMatcher(
new NegatedRequestMatcher(new AntPathRequestMatcher(SYSTEM_ANT_PATH)),
new NegatedRequestMatcher(authServerFilter.getRequestMatcher())))
.authorizeRequests(a -> a.anyRequest().authenticated())
.csrf().disable()
.userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername)
.formLogin().loginPage("/login")
.successHandler(new RedirectLoginAuthenticationSuccessHandler())
.failureHandler(failureHandler).permitAll()
.and()
.oauth2ResourceServer().jwt();
return http.build();
}
}
}The source code is available at https://github.com/NotFound403/id-server for further study.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
