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

Spring Boot中仅对/rest/**接口应用JWT认证的登录问题排查

问题分析与解决方案

你的问题核心在于全局添加的JWT过滤器和无状态会话配置干扰了原有formLogin的认证流程:

  1. 你设置了sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS),而formLogin默认依赖Session保存认证状态,无状态配置会导致登录成功后无法维持用户会话。
  2. 虽然过滤器里通过shouldNotFilter尝试跳过非/rest/**的请求,但全局添加过滤器的方式还是会让登录请求被过滤器处理,再加上无状态会话的限制,最终导致登录时直接返回401。

最优解决方案:拆分Security配置,隔离REST与Web认证流程

Spring Security支持多套HttpSecurity配置,通过@Order指定优先级,让REST接口和普通Web页面使用完全独立的认证规则,互不干扰。

1. 创建REST接口专用的Security配置

@Configuration
@Order(1) // 优先级更高,优先匹配/rest/**路径
public class RestSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/rest/**") // 仅对/rest/**路径生效
                .csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .anyRequest().authenticated();

        // 仅在REST路径下添加JWT过滤器
        JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService, jwtTokenUtil, tokenHeader);
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

2. 保留原有Web页面的Security配置

@Configuration
@Order(2) // 优先级更低,处理非/rest/**的所有请求
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private Environment env;

    private String[] publicMatchers() {
        // 这里填写你的公共资源路径,比如静态文件、登录页等
        return new String[]{
                "/css/**",
                "/js/**",
                "/images/**",
                "/login"
        };
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        final List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
        if (activeProfiles.contains("dev")) {
            http.csrf().disable();
            http.headers().frameOptions().disable();
        }

        http
                .authorizeRequests()
                .antMatchers(publicMatchers()).permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/bonanza/list")
                .failureUrl("/login?error")
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }
}

3. 简化JWT过滤器代码

现在过滤器只会作用于/rest/**路径,无需再做路径判断,删除相关逻辑:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    logger.info("processing authentication for '{}'", request.getRequestURL());

    final String requestHeader = request.getHeader(this.tokenHeader);
    String username = null;
    String authToken = null;

    if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
        authToken = requestHeader.substring(7);
        try {
            username = jwtTokenUtil.getUsernameFromToken(authToken);
        } catch (IllegalArgumentException e) {
            logger.info("an error occurred during getting username from token", e);
        } catch (ExpiredJwtException e) {
            logger.info("the token is expired and not valid anymore", e);
        }
    } else {
        logger.info("couldn't find bearer string, will ignore the header");
    }

    logger.info("checking authentication for user '{}'", username);
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        logger.info("security context was null, so authorizing user");

        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

        if (jwtTokenUtil.validateToken(authToken, userDetails)) {
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            logger.info("authorized user '{}', setting security context", username);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
    }

    chain.doFilter(request, response);
}

// 删除shouldNotFilter方法

为什么这样能解决问题?

  • 拆分配置后,/rest/**的请求会走无状态的JWT认证流程,普通Web请求走基于Session的formLogin流程,两者完全隔离,不会互相干扰。
  • 登录请求不再经过JWT过滤器,formLogin可以正常创建Session并维持用户认证状态,解决了登录时的401问题。

内容的提问来源于stack exchange,提问作者Nunyet Calçada

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:22:13