如何通过Spring Boot从主机公开静态文件并规避Spring Security认证
部署后端服务的主机上存储了一些PDF文件,需要公开提供,同时支持客户端通过开发的模块动态替换旧文件。为避免接口服务的性能损耗,选择配置静态资源路径,在application.properties中添加了:
spring.web.resources.static-locations=file:/home/spidey/sopon3/rda-aof/
现在可通过my-devdomain.com/public-data.pdf访问文件,但受Spring Security限制,访问需要认证。已在安全配置中尝试放行/rda-aof/**路径,但未生效,且不能直接放行所有端点,寻求解决方案及其他可行实现方法。
安全配置代码如下:
package ca.p6son.spring.config.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.security.authentication.AuthenticationManager; 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.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import java.util.Arrays; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final Logger logger = LoggerFactory.getLogger(getClass()); @Autowired Environment env; @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private UserDetailsService jwtUserDetailsService; @Autowired private JwtRequestFilter jwtRequestFilter; @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { logger.info("AuthManager init: " + super.authenticationManagerBean().toString()); return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { boolean isProd = (Arrays.stream(env.getActiveProfiles()).findFirst().orElse(null)).equals("prod"); http .authorizeRequests() .antMatchers("/api/v1/geojson/atm", isProd ? "/api/v1/invalid/path1" : "/swagger-ui/**", isProd ? "/api/v1/invalid/path2" : "/v3/api-docs/**", "/api/v1/account/resend_otp", "/api/v1/resource/**", "/api/v1/geojson/branches", "/api/v1/guides", "/api/v1/faq", "/actuator/**", "/api/v1/products", "/api/v1/version_validate", "/api/v1/security_guides", "/api/v1/biometric_challenge", // To enable "/api/v1/bio-override", "/api/v1/dao/bio-override", "/api/v1/push-notification", "/api/v1/deals_and_offers", "/api/v1/city_list", "/api/v1/users", "/api/v1/cleanup/*", "/api/v1/account/forgot_username/*", "/api/v1/account/forgot_password/*", "/api/v1/auth/**", "/api/v1/validate/pin", "/api/v1/test", "/api/v1/cleanup", "/inter-module/**", "/rda-aof/**", "/api/v1/verify-email-link", "/api/v1/robo/**", "/api/v1/terms_and_conditions").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .headers(headers -> { headers.addHeaderWriter((request, response) -> { String requestURI = request.getRequestURI(); if (!(requestURI.startsWith("/api/v1/resource/alerts") || requestURI.startsWith("/api/v1/verify-email-link"))) { response.addHeader("Referrer-Policy", "strict-origin-when-cross-origin"); response.addHeader("Content-Security-Policy", "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self'"); response.addHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); response.addHeader("Permissions-Policy", "geolocation=(self), microphone=(), camera=()"); } }); }) .cors().and().csrf().disable(); http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } }
问题根源
你配置的静态资源路径file:/home/spidey/sopon3/rda-aof/被Spring默认映射到根路径(即/),所以文件访问路径是/public-data.pdf,但Security中放行的是/rda-aof/**,两者路径不匹配,导致放行规则无效。
方案1:修正静态资源映射前缀,匹配Security规则
修改application.properties,添加静态资源访问前缀配置:
spring.web.resources.static-locations=file:/home/spidey/sopon3/rda-aof/ spring.mvc.static-path-pattern=/rda-aof/**
修改后文件访问路径变为my-devdomain.com/rda-aof/public-data.pdf,与Security中/rda-aof/**的放行规则完全匹配,无需修改Security代码即可生效。
方案2:直接放行当前文件访问路径
如果不想修改访问路径,直接在Security的antMatchers中添加PDF文件的放行规则:
在permitAll()的路径列表中加入"/*.pdf"(放行所有根路径下的PDF文件),或精确到单个文件"/public-data.pdf"。修改后的片段示例:
.antMatchers( // 原有路径... "/rda-aof/**", "/*.pdf", // 新增:放行根路径下所有PDF "/api/v1/verify-email-link", // 其他路径... ).permitAll()
方案3:自定义静态资源映射(更灵活)
通过WebMvcConfigurer手动配置静态资源映射,同时确保Security放行对应路径,这种方式便于后续扩展:
- 在配置类中添加
WebMvcConfigurerBean:
@Bean public WebMvcConfigurer staticResourceConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 配置访问前缀为/rda-aof/**,映射到本地文件路径 registry.addResourceHandler("/rda-aof/**") .addResourceLocations("file:/home/spidey/sopon3/rda-aof/"); } }; }
- 确保Security配置中
/rda-aof/**已在permitAll()列表中(当前配置已包含),之后即可通过my-devdomain.com/rda-aof/public-data.pdf访问文件,且支持动态替换文件。
额外检查
确认JwtRequestFilter没有绕过Security授权规则,只要antMatchers配置了permitAll,Spring Security会在过滤器执行前放行这些请求,无需修改过滤器逻辑。
内容的提问来源于stack exchange,提问作者Hussain Ahmed Siddiqui

