Spring Boot + Auth0:在Azure App Service上,健康检查端点返回401,但在本地可以正常工作,尽管该路径已从SecurityFilterChain中排除
我在一个使用OAuth2资源服务器的Spring Boot应用中实现Auth0的认证。
一切在本地工作正常,但在部署到Azure App Service之后,无法在没有Bearer令牌的情况下访问我的健康检查端点。无论如何都会收到 401 Unauthorized,尽管该端点在我的 SecurityFilterChain 中已经被显式排除在认证之外。
预期行为
/health 与 /actuator/health 应该无需认证即可访问。
实际行为
在部署到 Azure App Service 之后,访问:
GET /health
GET /actuator/health
返回:
401 Unauthorized
然而,同样的端点在本地无需Bearer令牌即可工作。
进一步排查
我还尝试将健康端点移入一个 单独的 SecurityFilterChain,该端点对 /health 和 /actuator/health 允许所有请求,但部署到Azure之后,行为仍然相同。
application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: <issuer-uri>
audiences: <audience>
auth0:
audience: <audience>
issuerURI: <issuer-uri>
AudienceValidator
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private final String audience;
public AudienceValidator(String audience) {
this.audience = audience;
}
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains(audience)) {
return OAuth2TokenValidatorResult.success();
}
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
return OAuth2TokenValidatorResult.failure(error);
}
}
安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${auth0.issuerURI}")
private String issuerUri;
@Value("${auth0.audience}")
private String audience;
@Value("${allowed-cors-origins}")
private String allowedOrigins;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health", "/actuator/health").permitAll()
.requestMatchers("/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("https://Placeholder.com/roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return converter;
}
}
环境
- Spring Boot 4.1.0-M2
- Spring Security 4.1.0-M2
- Auth0
- Azure App Service
- Java 21
其他观察
- 同一个构建在本地可以工作。
issuer-uri、audience、和placeholder.com配置正确。- 健康端点应通过
.requestMatchers("/health", "/actuator/health").permitAll()排除。
问题
为什么这些端点在仅部署到Azure App Service之后仍然返回 401 Unauthorized,尽管它们已在 SecurityFilterChain 中被显式排除?
是否存在某些Azure专用的因素(例如反向代理头、路径重写、actuator配置,或安全过滤器的执行顺序)会导致这种行为?
服务器日志
2026-03-11T21:45:52.630Z DEBUG 93 --- [ main]
o.s.s.web.DefaultSecurityFilterChain : Will secure any request with filters:
DisableEncodeUrlFilter,
WebAsyncManagerIntegrationFilter,
SecurityContextHolderFilter,
HeaderWriterFilter,
CsrfFilter,
LogoutFilter,
OAuth2ProtectedResourceMetadataFilter,
BearerTokenAuthenticationFilter,
AuthenticationFilter,
RequestCacheAwareFilter,
SecurityContextHolderAwareRequestFilter,
AnonymousAuthenticationFilter,
ExceptionTranslationFilter,
AuthorizationFilter
2026-03-11T21:46:11.586Z DEBUG 93 --- [p-nio-80-exec-3]
o.s.security.web.FilterChainProxy : Securing GET /actuator/health
2026-03-11T21:46:11.614Z DEBUG 93 --- [p-nio-80-exec-3]
o.s.s.w.a.AnonymousAuthenticationFilter :
Set SecurityContextHolder to anonymous SecurityContext
2026-03-11T21:46:11.655Z DEBUG 93 --- [p-nio-80-exec-3]
o.s.s.w.s.HttpSessionRequestCache :
Saved request https://example.azurewebsites.net/actuator/health?continue to session
2026-03-11T21:46:59.637Z DEBUG 93 --- [p-nio-80-exec-4]
o.s.security.web.FilterChainProxy : Securing GET /actuator/health
2026-03-11T21:46:59.649Z DEBUG 93 --- [p-nio-80-exec-4]
o.s.s.w.a.AnonymousAuthenticationFilter :
Set SecurityContextHolder to anonymous SecurityContext
2026-03-11T21:46:59.667Z DEBUG 93 --- [p-nio-80-exec-4]
o.s.s.w.s.HttpSessionRequestCache :
Saved request https://example.azurewebsites.net/actuator/health?continue to session
请求/响应
GET /actuator/health HTTP/1.1
User-Agent: PostmanRuntime/7.51.1
Accept: */*
Postman-Token: f3002c0e-d920-4130-afe2-1df8e872d649
Host: example.westeurope-01.azurewebsites.net
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: JSESSIONID=95A12962B2439D240BBDF0E17775F57B
HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Thu, 12 Mar 2026 08:14:32 GMT
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Expires: 0
Pragma: no-cache
WWW-Authenticate: Bearer resource_metadata="https://example.westeurope-01.azurewebsites.net/.well-known/oauth-protected-resource"
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
X-Frame-Options: DENY
解决方案
当从curl调用的响应中查看时,我们可以看到以下内容:
WWW-Authenticate: Bearer resource_metadata="https://example.westeurope-01.azurewebsites.net/.well-known/oauth-protected-resource"
这里的这一行告诉我们,是Azure本身在阻止请求,而不是Spring Boot应用。.well-known/oauth-protected-resource 遵循 RFC9728 的规定,基本上通过元数据告知我们该API需要Bearer令牌。
当Spring拒绝令牌时,响应头通常是:
WWW-Authenticate: Bearer error="invalid_token", error_description="..."
在这种情况下,由于发现日志中甚至没有打印任何请求,我相当确定开启了Azure Easy Auth之类的东西。
我并不能确切地说如何禁用它,因为我无法访问相关的Azure,而且Azure的 API改动频繁。但据了解,它被称为Easy Auth,可以在 Azure App Service Authentication and Authorization 中了解。