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

SpringBoot + Thymeleaf + Spring Security环境下图片可加载但CSS文件报404问题求助

SpringBoot + Thymeleaf + Spring Security环境下图片可加载但CSS文件报404问题求助

大家好,我最近在SpringBoot + Thymeleaf + Spring Security的项目里碰到了一个诡异的问题:图片资源能正常访问(比如http://localhost:8080/assets/images/apple-touch-icon.png),但加载CSS文件时却一直返回404,比如http://localhost:8080/assets/stylesheets/font-awesome.min.css。以下是我的相关代码和配置,麻烦各位帮忙排查下问题所在?

我的Thymeleaf头部模板代码:

<head>
    <meta charset="utf-8"/>
    <meta content="width=device-width, initial-scale=1.0" name="viewport"/>

    <meta property="og:site_name"   th:content="#{social.media.title}" />
    <meta property="og:url"         content="https://www.taradell-planets.com"/>
    <meta property="og:type"        content="article"/>
    <meta property="og:title"       th:content="#{social.media.title}" />
    <meta property="og:description" th:content="#{social.media.description}" />
    <meta property="og:image"       content="https://www.taradell-planets.com/assets/images/app.png"/>

    <!-- Other meta tags -->
    <meta name="twitter:card"           content="summary_large_image">
    <meta name="twitter:title"          th:content="#{social.media.title}" />
    <meta name="twitter:description"    th:content="#{social.media.description}" />
    <meta name="twitter:image"          content="https://www.taradell-planets.com/assets/images/app.png">
    <!-- Other meta tags -->

    <title th:text="#{html.title}">Astrology and Spirituality...</title>
    <meta th:content="#{meta.content.description}" content="Explore the realms of astrology and spirituality.." name="description"/>
    <meta th:content="#{meta.content.keywords}"    content="astrology, spirituality..."                         name="keywords"/>
    <meta content="ie=edge" http-equiv="X-UA-Compatible"/>

    <!-- Favicons -->
    <link th:href="@{/assets/images/favicon.png}" rel="icon"/>
    <link th:href="@{/assets/images/apple-touch-icon.png}" rel="apple-touch-icon"/>


    <!-- Google Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,600;1,700&amp;family=Montserrat:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&amp;family=Raleway:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&amp;display=swap"
          rel="stylesheet"/>
    <link rel="preconnect" href="https://fonts.googleapis.com"/>
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin=""/>
    <link href="https://fonts.googleapis.com/css2?family=Oswald:wght@500&amp;display=swap" rel="stylesheet" />
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400&amp;display=swap" rel="stylesheet"/>
    <!-- Vendor CSS Files -->
    <link th:href="@{/assets/stylesheets/font-awesome.min.css}" rel="stylesheet"/>
    <link th:href="@{/assets/vendor/aos/aos.css}" rel="stylesheet"/>
    <link th:href="@{/assets/vendor/swiper/swiper-bundle.min.css}" rel="stylesheet"/>
    <link th:href="@{/assets/vendor/glightbox/css/glightbox.min.css}" rel="stylesheet"/>
    <link th:href="@{/assets/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet"/>
    <link th:href="@{/assets/vendor/bootstrap-icons/bootstrap-icons.css}" rel="stylesheet"/>

    <!-- Main CSS File -->
    <link th:href="@{/assets/stylesheets/styles.css}" rel="stylesheet"/>
    <link th:href="@{/assets/stylesheets/custom.css}" rel="stylesheet"/>
    <link th:href="@{/assets/stylesheets/responsive.css}" rel="stylesheet"/>

</head>

WebConfig配置类:

@Profile("web")
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("classpath:/static/");
    }
}

WebSecurityConfig配置类:

@Configuration
@EnableWebSecurity
//@EnableMethodSecurity
@Profile("web")
public class WebSecurityConfig {

    private final JwtUtils jwtUtils;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthEntryPointJwt unauthorizedHandler;

    public WebSecurityConfig(JwtUtils jwtUtils,
                             UserDetailsServiceImpl userDetailsService,
                             AuthEntryPointJwt unauthorizedHandler) {
        this.jwtUtils = jwtUtils;
        this.userDetailsService = userDetailsService;
        this.unauthorizedHandler = unauthorizedHandler;
    }

    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter(jwtUtils, userDetailsService);
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

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


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable)
                .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().permitAll() //  Allows all requests without authentication
                );

        http.authenticationProvider(authenticationProvider());
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }



    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.ENGLISH);
        return slr;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang"); // Allows users to change locale via ?lang=fr
        return interceptor;
    }

    @Bean
    public WebMvcConfigurer localeInterceptorConfig() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(localeChangeInterceptor());
            }
        };
    }
}

资源加载情况截图:

资源加载情况截图


我整理的排查方向和可能解决方案:

  1. 检查静态资源目录结构
    先确认CSS文件的实际位置:font-awesome.min.css是否真的在src/main/resources/static/assets/stylesheets/目录下?文件名和路径的拼写要完全一致(尤其注意Linux环境下大小写敏感,比如Stylesheets和stylesheets是不同的)。

  2. 验证Thymeleaf路径渲染
    打开页面的HTML源码,查看CSS的最终渲染路径是不是http://localhost:8080/assets/stylesheets/font-awesome.min.css,确认Thymeleaf的@{...}语法没有解析错误。

  3. 调整资源映射配置
    当前WebConfig只配置了/resources/**的映射,但我们用的是/assets/**路径。虽然SpringBoot默认会自动映射classpath:/static/下的资源,但可以显式添加/assets/**的映射规则,确保Spring能正确定位资源:

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("classpath:/static/");
        // 新增assets路径的映射
        registry.addResourceHandler("/assets/**")
                .addResourceLocations("classpath:/static/assets/");
    }
    
  4. 明确Spring Security的静态资源放行规则
    虽然配置了anyRequest().permitAll(),但有时候过滤器可能会拦截静态资源,我们可以显式指定/assets/**路径放行,避免潜在的拦截问题:

    .authorizeHttpRequests(auth -> auth
            .requestMatchers("/assets/**").permitAll()
            .anyRequest().permitAll()
    )
    
  5. 清理浏览器缓存
    有时候浏览器缓存会导致旧的路径缓存,尝试清理缓存或者用无痕模式访问,排除缓存干扰。

希望这些思路能帮到我,也欢迎各位大佬提出其他可能的问题点!

备注:内容来源于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.04.15 03:20:13