Spring Security中defaultSecurityFilterChain无法正常工作问题排查
JWT授权访问端点返回401问题排查
后端使用Java、Spring及最新版Spring Security(未使用SpringBoot),前端采用AngularJS。项目需生成JWT授权访问部分端点,目前token生成流程正常,但携带token访问需授权的/seguridad端点时,始终返回401错误(Postman测试结果一致)。怀疑项目缺少认证管理器配置,导致无法识别发送的token。
相关配置及代码文件
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/appServlet-servlet.xml</param-value> </context-param> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>cors</filter-name> <filter-class>com.mx.xpertys.CORSFilter</filter-class> </filter> <filter-mapping> <filter-name>cors</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Spring Security --> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- --> <session-config> <session-timeout> 30 </session-timeout> </session-config> </web-app>
JwtUtils.java
package com.mx.xpertys.security.config; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.security.Key; import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtUtils { private static final Key SECRET_KEY = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256); public static String generateToken(String username, String password, long expirationSeconds) { Instant now = Instant.now(); Instant expiration = now.plusSeconds(expirationSeconds); Map<String, Object> claims = new HashMap<>(); claims.put("username", username); claims.put("password", password); return Jwts.builder() .setClaims(claims) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(expiration)) .signWith(SECRET_KEY) .compact(); } }
SecurityConfig.java
package com.mx.xpertys.security.config; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.UUID; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; 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.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; /** * * @author Diego */ @Configuration @EnableWebSecurity public class SecurityConfig{ public SecurityConfig() { System.out.println("Entró al SecurityConfig"); } @Bean @Order(2) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { System.out.println("Entro al segundo"); OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); http .exceptionHandling((exceptions) -> exceptions .defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) ) ) // Accept access tokens for User Info and/or Client Registration .oauth2ResourceServer((resourceServer) -> resourceServer .jwt(Customizer.withDefaults())); return http.build(); } @Bean @Order(1) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorize) -> { authorize.requestMatchers(HttpMethod.GET, "/hola", "/adios", "/generarToken").permitAll() .requestMatchers(HttpMethod.POST, "/consulta/**").permitAll() .requestMatchers(HttpMethod.GET, "/seguridad").authenticated() .anyRequest().authenticated(); }).csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults())); System.out.println("Pasó el filtro de SecurityConfig"); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.builder() .username("diego") .password("hola") .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("oidc-client") .clientSecret("{noop}secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("http://127.0.0.1:8080/login/oauth2/code/oidc-client") .postLogoutRedirectUri("http://127.0.0.1:8080/") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) .build(); return new InMemoryRegisteredClientRepository(oidcClient); } @Bean public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } }
Prueba.java
package com.mx.xpertys; import com.mx.xpertys.security.config.*; import java.util.HashMap; import java.util.Map; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class Prueba { @GetMapping(value = "/hola") public String hola() { return "Hola"; } @GetMapping(value = "/adios") public String adios() { int min = 20, max = 80; return String.valueOf(Math.clamp(100, min, max)); //clamp es un método que se incorporó en la verisón 21 de Java } @GetMapping(value = "/seguridad") public String seguridad(@RequestHeader("x-xsrf-token") String token) { return "Exito. Token: " + token; } @GetMapping(value = "/generarToken") public ResponseEntity<Map<String, String>> generarToken(@RequestParam("username") String username, @RequestParam("password") String password) { String token = JwtUtils.generateToken(username, password, 3600); // Se devuelve el token en una respuesta JSON Map<String, String> response = new HashMap<>(); response.put("token", token); return ResponseEntity.ok(response); } }
问题补充
生成token的逻辑在Prueba.java的generarToken()方法中,希望访问/seguridad端点,但defaultSecurityFilterChain未按预期工作,疑似拦截了请求。是否需要额外实现功能,或修改SecurityConfig的配置?
注:已通过日志确认SecurityConfig已成功加载。
问题原因及解决方法
核心问题分析
- JWT签名算法不匹配:
JwtUtils使用HS256算法生成token,但SecurityConfig中配置的JWT解码器基于RSA密钥对,两者签名/验签逻辑完全不兼容,导致Spring Security无法验证token,直接返回401。 - Token传递方式错误:
/seguridad接口接收x-xsrf-token头的token,但Spring Security默认只识别Authorization: Bearer {token}格式的请求头,即使token有效也无法被安全过滤器识别。 - Authorization Server配置冗余:未使用OAuth2授权服务器功能(如授权码模式),但配置了对应的过滤器链,增加了不必要的逻辑干扰。
具体修改步骤
1. 统一JWT签名/验签算法(改为HS256)
修改SecurityConfig,替换基于RSA的JWT解码器为HS256版本:
// 移除原有的jwkSource、generateRsaKey方法,以及基于JWKSource的JwtDecoder Bean // 添加以下配置 @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withSecretKey(JwtUtils.SECRET_KEY).build(); } // 同时修改JwtUtils,将SECRET_KEY改为public,方便SecurityConfig调用 public class JwtUtils { public static final Key SECRET_KEY = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256); // ... 其余代码不变 }
2. 修正Token传递方式
- 请求
/seguridad时,将token放在Authorization头中,格式为Bearer {你的token}。 - 修改
Prueba.java的seguridad方法,无需手动接收token头,Spring Security会自动解析认证:
@GetMapping(value = "/seguridad") public String seguridad(Authentication authentication) { String username = authentication.getName(); return "Exito. Usuario autenticado: " + username; }
3. 移除冗余的Authorization Server配置
删除authorizationServerSecurityFilterChain、registeredClientRepository、authorizationServerSettings这几个Bean,简化SecurityConfig:
@Configuration @EnableWebSecurity public class SecurityConfig{ public SecurityConfig() { System.out.println("Entró al SecurityConfig"); } @Bean public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorize) -> { authorize.requestMatchers(HttpMethod.GET, "/hola", "/adios", "/generarToken").permitAll() .requestMatchers(HttpMethod.POST, "/consulta/**").permitAll() .requestMatchers(HttpMethod.GET, "/seguridad").authenticated() .anyRequest().authenticated(); }).csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults())); System.out.println("Pasó el filtro de SecurityConfig"); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.builder() .username("diego") .password("{noop}hola") // 添加{noop}前缀,避免密码校验失败 .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withSecretKey(JwtUtils.SECRET_KEY).build(); } }
4. 额外优化点
- 不要在JWT的claims中存放敏感信息(如密码),避免token泄露时用户信息暴露,建议只存放username、角色等非敏感内容。
内容的提问来源于stack exchange,提问作者Diego Becerril
相关产品推荐
相关产品推荐

