Spring Boot应用能否同时使用X509证书与JWT双重认证?
Absolutely! You can build a layered security model where your Spring Boot app first uses X.509 certificates to secure the exposed endpoint (only clients with valid certificates can reach your app), then requires a valid JWT token to grant access to any API. This gives you both application-level security via certificates and user-level authorization via JWT. Let's walk through adjusting your existing setup to make this work.
Core Approach
We'll keep your existing X.509 authentication as the first security gate (ensuring only trusted clients can connect). Then we'll add a JWT validation filter as a second layer, which checks for a valid JWT in each request (typically in the Authorization header as Bearer <token>) before allowing API access.
Step 1: Update Your Security Configuration
Modify your SecurityConfig to include both X.509 authentication and JWT validation. Here's the adjusted code:
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Value("${security.enable-csrf}") private boolean csrfEnabled; private final JwtAuthenticationFilter jwtAuthenticationFilter; // Inject the JWT filter we'll define next public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Override protected void configure(HttpSecurity http) throws Exception { http // First, enforce X.509 certificate validation for all requests .authorizeRequests() .anyRequest().authenticated() .and() .x509() .subjectPrincipalRegex("CN=(.*?)(?:,|$)") .userDetailsService(userDetailsService()) .and() // Add JWT validation filter before the default username/password filter .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); if (!csrfEnabled) { http.csrf().disable(); } } @Bean public UserDetailsService userDetailsService() { return (UserDetailsService) username -> { if (username.equals("XXXX")) { return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); } else { throw new UsernameNotFoundException(String.format("User %s not found", username)); } }; } // Expose AuthenticationManager as a bean for the JWT filter to use @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
Step 2: Implement the JWT Authentication Filter
Create a filter that extracts the JWT from the request header, validates it, and sets the authenticated user in the security context. Here's a sample implementation:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.security.Key; import java.util.Date; public class JwtAuthenticationFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final String jwtSecret; private final long jwtExpirationMs; // Inject JWT properties from application.properties public JwtAuthenticationFilter(UserDetailsService userDetailsService, @Value("${jwt.secret}") String jwtSecret, @Value("${jwt.expirationMs}") long jwtExpirationMs) { this.userDetailsService = userDetailsService; this.jwtSecret = jwtSecret; this.jwtExpirationMs = jwtExpirationMs; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Extract JWT from the Authorization header String jwt = extractJwtFromRequest(request); if (jwt != null && validateJwtToken(jwt)) { // Get username from JWT claims String username = getUsernameFromJwtToken(jwt); // Load user details from your UserDetailsService UserDetails userDetails = userDetailsService.loadUserByUsername(username); // Set authenticated user in the security context UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String extractJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } private boolean validateJwtToken(String authToken) { try { Key key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(authToken) .getBody(); // Check if the token is expired return !claims.getExpiration().before(new Date()); } catch (Exception e) { logger.error("Invalid JWT token: {}", e.getMessage()); } return false; } private String getUsernameFromJwtToken(String token) { Key key = Keys.hmacShaKeyFor(jwtSecret.getBytes()); return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody() .getSubject(); } }
Step 3: Add JWT Properties to application.properties
Add these configuration values to your application.properties file:
# JWT Configuration jwt.secret=your-strong-random-secret-key-here-make-it-at-least-32-characters jwt.expirationMs=86400000 # 1 day in milliseconds (adjust as needed)
Step 4: Register the JWT Filter as a Bean
Add this bean definition to your configuration class (or a separate configuration bean):
@Bean public JwtAuthenticationFilter jwtAuthenticationFilter(UserDetailsService userDetailsService) { return new JwtAuthenticationFilter(userDetailsService, jwtSecret, jwtExpirationMs); }
How It Works
- X.509 Layer: When a client sends a request, the server first validates the client's X.509 certificate. If the certificate is invalid, the request is rejected immediately.
- JWT Layer: If the certificate is valid, the request moves to the JWT filter. The filter checks for a valid JWT in the
Authorizationheader. If the token is missing or invalid, access is denied. If valid, the user's authentication is set in the security context, allowing access to the API.
Notes
- Ensure your server is properly configured with a truststore containing trusted client certificates for X.509 validation.
- For production, use a strong, randomly generated
jwt.secretand consider shorter token expiration times paired with refresh tokens if needed. - You can extend the JWT validation logic to check for custom claims (like user roles) to add fine-grained authorization.
内容的提问来源于stack exchange,提问作者Excalibur

