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

Spring Security环境下静态资源加载失败(401未授权)问题求助

问题描述

应用采用自定义认证机制:

  • /api/** 端点使用Token认证
  • /manager/** 和 /viewer/** 使用表单登录
  • API接口用@RestController,网页用@Controller

访问页面时出现以下问题:

  1. 静态资源无法加载,请求返回net::ERR_ABORTED 401 (Unauthorized)
  2. /viewer/home 请求返回401,但能正常渲染HTML内容并加载DTO数据
  3. 直接在浏览器访问http://localhost:8080/css/operator.css可以正常获取CSS文件

控制器代码

@GetMapping("/viewer/home")
public String operatorHome(Model model) {
    // logic...
    model.addAttribute("files", dtos);
    return "operator-home";
}

安全配置代码

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    private AuthenticationService authService;

    @Bean
    @Order(1)
    public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/manager/**").hasAuthority(Role.MANAGER.name())
                        .requestMatchers("/viewer/**").hasAuthority(Role.OPERATOR.name())
                        .requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**").permitAll()
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                        .anyRequest().permitAll()
                )
                .formLogin((form) -> form.successHandler(new CustomAuthenticationSuccessHandler()))
                .logout(LogoutConfigurer::permitAll)
                .httpBasic(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/api/**").hasAuthority(Role.USER.name())
                        .anyRequest().permitAll()
                )
                .addFilterBefore(new TokenAuthenticationFilter(authService), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .httpBasic(Customizer.withDefaults())
        ;
        return http.build();
    }

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter(authService);
    }

    @Bean
    CustomUserDetailsService customUserDetailsService() {
        return new CustomUserDetailsService();
    }

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

补充:即使注释掉第二个API过滤器链,保留anyRequest().permitAll(),问题依然存在;Postman测试REST API正常。

HTML代码

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <!-- 这个加载正常 -->
<link rel="stylesheet" type="text/css" th:href="@{/css/operator.css}"> <!-- 这个无法加载 -->

请求日志

DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Securing GET /viewer/home
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=test_operator, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[OPERATOR]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=9E3858A4DC3CD548374F4874C21539D6], Granted Authorities=[OPERATOR]]]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Secured GET /viewer/home
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : GET "/viewer/home", parameters={}
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.lavkatech.audiorecognition.controller.WebController#operatorHome(Model)
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] org.hibernate.SQL                        : select ao1_0.id,ao1_0.audio_len,ao1_0.checked_by_id,ao1_0.checked_on,ao1_0.file_loc,ao1_0.file_name,ao1_0.file_size,ao1_0.file_text,ao1_0.is_checked,ao1_0.op_end_time,ao1_0.op_start_time,ao1_0.requested_by_value from files ao1_0
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8, application/signed-exchange;v=b3;q=0.7]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : Completed 401 UNAUTHORIZED
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Securing GET /css/operator.css
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Secured GET /css/operator.css
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet        : GET "/css/operator.css", parameters={}
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet        : Completed 401 UNAUTHORIZED
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=test_operator, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[OPERATOR]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=9E3858A4DC3CD548374F4874C21539D6], Granted Authorities=[OPERATOR]]]
问题分析与解决方案

从日志可见,/viewer/home和静态资源请求已被Spring Security放行,但DispatcherServlet最终返回401,说明问题出在视图渲染或资源处理阶段的额外拦截,而非Spring Security过滤器链本身。

排查与修复步骤:

  1. 移除webFilterChain中的httpBasic配置
    表单登录场景不需要HTTP Basic认证,两个过滤器链同时配置httpBasic可能导致认证逻辑冲突。修改webFilterChain:

    // 移除该行
    .httpBasic(Customizer.withDefaults());
    
  2. 检查自定义拦截器/过滤器
    排查是否存在WebMvcConfigurer中添加的自定义拦截器,确保静态资源和页面请求不会被额外拦截。若有不必要的拦截规则,直接移除或调整匹配路径。

  3. 验证模板引擎权限配置
    检查Thymeleaf模板中是否使用了sec:authorize标签,若权限表达式与当前用户角色不匹配,会导致视图渲染后返回401。移除或修正相关标签。

  4. 显式配置静态资源映射
    确保静态资源路径映射正确,添加WebMvc配置类:

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/css/**")
                    .addResourceLocations("classpath:/static/css/");
            registry.addResourceHandler("/js/**")
                    .addResourceLocations("classpath:/static/js/");
        }
    }
    
  5. 检查认证成功处理器
    排查CustomAuthenticationSuccessHandler是否正确设置会话属性,避免跳转时修改认证状态导致后续请求失效。


内容的提问来源于stack exchange,提问作者uauauaua

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 06:13:11