Spring Boot Security部署WAR后API请求返回401认证错误(凭证合法)
嘿,我来帮你分析下这个头疼的问题——嵌入式Tomcat里好好的JWT认证,打包成WAR部署到外部容器就全失效了。先理清楚关键信息,再逐一排查可能的原因:
问题背景梳理
- 技术栈:Spring Boot + React 单页应用,JWT认证机制
- 开发环境:用嵌入式Tomcat时,所有API接口(包括需要认证的
/api/user/me)都能正常访问 - 部署异常:打包为WAR后部署到Wildfly、Glassfish 4.1.2、Tomcat 8.5.23,均出现已认证请求被拒绝的情况
- 关键现象:
- JwtFilter监控显示凭证本身没有错误,但Tomcat日志抛出
JwtAuthenticationEntryPoint : Responding with unauthorized error. Message - Full authentication is required to access this resource - 请求地址变化:开发时是
http://localhost:8080/api/user/me,部署后多了应用上下文路径myproject,变成http://localhost:8080/myproject/api/user/me
- JwtFilter监控显示凭证本身没有错误,但Tomcat日志抛出
你的核心代码参考
Spring Boot主类
@SpringBootApplication @EntityScan(basePackageClasses = { AccountingApplication.class, Jsr310JpaConverters.class }) public class AccountingApplication extends SpringBootServletInitializer{ @PostConstruct void init() { TimeZone.setDefault(TimeZone.getTimeZone("UTC")); } public static void main(String[] args) { SpringApplication.run(applicationClass, args); } protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(applicationClass); } private static Class<AccountingApplication> applicationClass = AccountingApplication.class; }
安全配置类
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomUserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder. userDetailsService(userDetailsService). passwordEncoder(passwordEncoder()); } @Bean(BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { /* removed for clarity */ } @Override protected void configure(HttpSecurity http) throws Exception { http .cors() .and() .csrf() .disable() .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/resources/**") .permitAll() .antMatchers("/", "/favicon.ico", "/**/*.png", "/**/*.gif", "/**/*.svg", "/**/*.woff", "/**/*.woff2", "/**/*.ttf", "/**/*.eot", "/**/*.jpg", "/**/*.html", "/**/*.css", "/**/*.js") .permitAll() .antMatchers("/api/auth/**") .permitAll() .anyRequest() .authenticated(); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } }
可能的原因及解决方案
1. 应用上下文路径导致JWT Filter未正确拦截请求
这是最常见的原因!部署到外部容器后,应用多了myproject上下文路径,而你的JwtAuthenticationFilter可能是基于完整请求URI判断是否处理认证,而不是Servlet路径。
比如Filter里如果写了:
if (request.getRequestURI().startsWith("/api/")) { // 处理JWT认证 }
部署后请求URI是/myproject/api/user/me,startsWith("/api/")会返回false,Filter直接跳过认证逻辑,导致请求被Security拦截。
修复方案:
在Filter中获取Servlet路径(自动去掉上下文路径)来判断:
// 在JwtAuthenticationFilter的doFilterInternal方法中 String servletPath = request.getServletPath(); if (servletPath.startsWith("/api/")) { // 提取Token、验证Token、设置Authentication等逻辑 }
或者用上下文路径截取后判断:
String contextPath = request.getContextPath(); String requestUri = request.getRequestURI().substring(contextPath.length()); if (requestUri.startsWith("/api/")) { // 处理认证 }
2. CORS配置未适配上下文路径
部署后请求路径带了上下文,CORS配置可能没有正确覆盖这个路径,导致前端请求的OPTIONS预检失败,或者Token没有被正确携带。
修复方案:
自定义全局CORS配置,确保匹配所有路径:
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(Collections.singletonList("*")); // 生产环境请替换为具体域名 config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 匹配上下文路径后的所有路径 return source; }
然后在SecurityConfig中使用这个配置:
@Override protected void configure(HttpSecurity http) throws Exception { http.cors().configurationSource(corsConfigurationSource()) // 替换原来的.cors() .and() // 其他配置... }
3. JWT Token的Audience/Issuer与部署环境不匹配
如果你的JWT生成时包含了aud(受众)或iss(签发者)字段,开发环境是http://localhost:8080,部署后变成http://localhost:8080/myproject,验证Token时会因为不匹配而失败。
修复方案:
- 生成Token时,将
aud/iss设置为包含上下文路径的地址,或者动态获取当前应用的上下文路径 - 测试环境可以临时关闭
aud/iss验证(不建议生产环境这么做)
4. 外部容器的依赖冲突或版本差异
嵌入式Tomcat和外部容器的版本差异可能导致Filter执行顺序、Servlet API版本不兼容等问题。
修复方案:
确保Maven中排除嵌入式Tomcat依赖,并引入Servlet API的provided依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency>
5. 增加日志排查细节
在JwtAuthenticationFilter中添加详细日志,明确知道Filter是否处理了请求、Token是否被正确提取:
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { logger.info("========== Processing Request =========="); logger.info("Full Request URI: {}", request.getRequestURI()); logger.info("Context Path: {}", request.getContextPath()); logger.info("Servlet Path: {}", request.getServletPath()); String jwt = getJwtFromRequest(request); logger.info("Extracted JWT Token: {}", jwt != null ? "Found Token" : "Token Missing"); // 后续认证逻辑... }
通过日志可以快速定位Filter是否生效,以及Token提取是否正常。
内容的提问来源于stack exchange,提问作者alegria

