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.

Programmer DD
Programmer DD
Programmer DD
How to Isolate Session and JWT Users in Spring Security: Strategies and Pitfalls

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javabackend-developmentspring-securityUserDetailsServicePath InterceptionSession vs JWT
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.