You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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已成功加载。


问题原因及解决方法

核心问题分析

  1. JWT签名算法不匹配:JwtUtils使用HS256算法生成token,但SecurityConfig中配置的JWT解码器基于RSA密钥对,两者签名/验签逻辑完全不兼容,导致Spring Security无法验证token,直接返回401。
  2. Token传递方式错误:/seguridad接口接收x-xsrf-token头的token,但Spring Security默认只识别Authorization: Bearer {token}格式的请求头,即使token有效也无法被安全过滤器识别。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 18:55:55