Implements a hybrid authentication model in Spring Security 6+, using stateful session-based security for a web UI and stateless JWT security for a REST API. Use when: securing both a traditional web application (like ZK, Thymeleaf) and a stateless REST API within a single Spring Boot project.
This skill provides a guide for implementing a hybrid authentication model in a single Spring Boot application using Spring Security 6+. This is a common enterprise pattern where you have:
/api/) secured with stateless JSON Web Tokens (JWT) for consumption by clients like SPAs, mobile apps, or other microservices.The key is to define multiple, ordered SecurityFilterChain beans, each responsible for a different set of URL patterns.
In modern Spring Security, you can define multiple SecurityFilterChain beans. Each bean can have its own securityMatcher to target specific URL paths, its own session management policy, and its own set of filters.
@Order(1)/api/**@Order(2)). Matches everything else (/**). Configured to be stateful and uses standard form login.Ensure you have the web, security, and JWT-related dependencies in your pom.xml.
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT Library (e.g., JJWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>...</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>...</version>
<scope>runtime</scope>
</dependency>
This custom filter is the heart of your API security. Its job is to inspect incoming requests for a JWT, validate it, and establish the user's identity in the security context.
JwtAuthenticationFilter.java
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
// ... other imports
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService; // Your service to validate tokens
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
// If no header or doesn't start with "Bearer ", pass to the next filter
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7);
final String username = jwtService.validateTokenAndGetUsername(jwt);
// If token is valid, set authentication in the context
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// NOTE: In a real app, load UserDetails from a service here
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
username,
null,
List.of() // Or load real authorities
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);
}
}
This class defines the two SecurityFilterChain beans.
SecurityConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter;
public SecurityConfiguration(JwtAuthenticationFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
/**
* Filter chain for stateless REST APIs secured with JWT.
*/
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // Apply this chain to API paths only
.csrf(csrf -> csrf.disable()) // Stateless APIs don't need CSRF
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login").permitAll() // Public login endpoint
.anyRequest().authenticated() // All other API endpoints require auth
)
// Add our custom JWT filter before the standard auth filter
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* Filter chain for stateful web UI secured with form login and sessions.
*/
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/**") // Apply to all other paths
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login.zul", "/css/**", "/zkau/**").permitAll() // Permit public resources
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login.zul")
.defaultSuccessUrl("/index.zul", true)
.permitAll()
);
return http.build();
}
}
Finally, you need a public controller endpoint that clients can use to authenticate and receive a JWT.
AuthenticationController.java
@RestController
@RequestMapping("/api")
public class AuthenticationController {
private final AuthenticationManager authenticationManager; // Inject this
private final JwtService jwtService;
// ... constructor ...
@PostMapping("/login")
public ResponseEntity<AuthenticationResponse> login(@RequestBody AuthenticationRequest request) {
// This will authenticate the user (e.g., against a DB or LDAP)
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
String jwt = jwtService.generateToken(request.getUsername());
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
}
By setting up these separate, ordered filter chains, you can effectively and securely manage both stateful and stateless authentication within a single Spring Boot application.
zk-min-samples repository, module 10-java17-zk10-jwt-mvvm-springboot.