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&family=Montserrat:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Raleway:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&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&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400&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()); } }; } }
资源加载情况截图:

我整理的排查方向和可能解决方案:
检查静态资源目录结构
先确认CSS文件的实际位置:font-awesome.min.css是否真的在src/main/resources/static/assets/stylesheets/目录下?文件名和路径的拼写要完全一致(尤其注意Linux环境下大小写敏感,比如Stylesheets和stylesheets是不同的)。验证Thymeleaf路径渲染
打开页面的HTML源码,查看CSS的最终渲染路径是不是http://localhost:8080/assets/stylesheets/font-awesome.min.css,确认Thymeleaf的@{...}语法没有解析错误。调整资源映射配置
当前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/"); }明确Spring Security的静态资源放行规则
虽然配置了anyRequest().permitAll(),但有时候过滤器可能会拦截静态资源,我们可以显式指定/assets/**路径放行,避免潜在的拦截问题:.authorizeHttpRequests(auth -> auth .requestMatchers("/assets/**").permitAll() .anyRequest().permitAll() )清理浏览器缓存
有时候浏览器缓存会导致旧的路径缓存,尝试清理缓存或者用无痕模式访问,排除缓存干扰。
希望这些思路能帮到我,也欢迎各位大佬提出其他可能的问题点!
备注:内容来源于stack exchange,提问作者Nunyet Calçada

