Spring Boot OAuth2客户端认证授权端点返回404问题排查
我正在使用Spring Boot OAuth2(spring-boot-starter-oauth2-client)和Azure Active Directory(azure-spring-boot-starter-active-directory)实现认证与授权。后端运行在8080端口,前端是运行在3000端口的React应用。从前端访问http://localhost:8080/oauth2/authorization/azure时,返回404白标错误页面。我已通过application.properties正确配置相关信息,请问我的安全配置是否存在问题?能否帮忙排查配置中的错误?
AADOAuth2WebSecurityConfig.java
import com.app.security.JwtAuthenticationFilter; import com.app.security.RestAuthenticationEntryPoint; import com.app.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository; import com.app.security.oauth2.OAuth2AuthenticationFailureHandler; import com.app.security.oauth2.OAuth2AuthenticationSuccessHandler; import com.app.service.auth.OAuth2UserService; import lombok.NonNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Collections; @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) @EnableJpaRepositories(basePackages = "com.app.repository") public class AADOAuth2WebSecurityConfig { private final OAuth2UserService oAuth2UserService; private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository; @Value("${azure.activedirectory.tenant-id}") private String tenantId; @Value("${azure.activedirectory.client-id}") private String clientId; @Value("${azure.activedirectory.client-secret}") private String clientSecret; @Value("${azure.activedirectory.redirect-uri-template}") private String redirectUri; @Value("${azure.activedirectory.authorization-clients.azure.scopes}") private String scope; @Value("${oauth.login.uri}") private String loginUri; @Value("${oauth.authorization-uri}") private String authUri; @Value("${oauth.token-uri}") private String tokenUri; @Value("${oauth.jwk-set-uri}") private String jwkSetUri; @Lazy @Autowired public AADOAuth2WebSecurityConfig( OAuth2UserService oAuth2UserService, RestAuthenticationEntryPoint restAuthenticationEntryPoint, OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler, OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler, HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) { this.oAuth2UserService = oAuth2UserService; this.restAuthenticationEntryPoint = restAuthenticationEntryPoint; this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler; this.oAuth2AuthenticationFailureHandler = oAuth2AuthenticationFailureHandler; this.authorizationRequestRepository = authorizationRequestRepository; } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Bean public HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository() { return new HttpCookieOAuth2AuthorizationRequestRepository(); } @Bean(BeanIds.AUTHENTICATION_MANAGER) public AuthenticationManager authenticationManager( @NonNull AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository( ClientRegistration.withRegistrationId("azure") .clientId(clientId) .clientSecret(clientSecret) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri(redirectUri) .scope(scope) .authorizationUri(loginUri + tenantId + authUri) .tokenUri(loginUri + tenantId + tokenUri) .userNameAttributeName(IdTokenClaimNames.SUB) .jwkSetUri(loginUri + tenantId + jwkSetUri) .clientName("Azure") .build()); } @Bean public SecurityFilterChain filterChain(@NonNull HttpSecurity httpSecurity) throws Exception { // the HSTS header is only injected into HTTPS responses, works via HTTPS after first // request httpSecurity.headers( header -> header.httpStrictTransportSecurity( transportSecurity -> transportSecurity .includeSubDomains(true) .maxAgeInSeconds(31536000))); // 1 year httpSecurity.cors(corsConfig -> corsConfig.configurationSource(corsConfigurationSource())); // disable csrf httpSecurity.csrf(AbstractHttpConfigurer::disable); // exception handling starting from authentication entry point httpSecurity.exceptionHandling( exHandler -> exHandler.authenticationEntryPoint(restAuthenticationEntryPoint)); // stateless as JWT is being used httpSecurity.sessionManagement( sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // allow access to assets without permission httpSecurity.authorizeHttpRequests( authReqConfig -> authReqConfig // allow public urls .requestMatchers( new AntPathRequestMatcher("/oauth2/**", "/api/v1/auth/**")) .permitAll() // allow public url .requestMatchers(new AntPathRequestMatcher("/api/v1/user/logout")) .permitAll() .anyRequest() .authenticated()); // as oauth2 is used add its authorization endpoint, redirection endpoint and user info // endpoint httpSecurity.oauth2Login( oAuthConfig -> oAuthConfig .clientRegistrationRepository(clientRegistrationRepository()) .authorizationEndpoint( authEndPoint -> authEndPoint .baseUri("/oauth2/authorization/azure") .authorizationRequestRepository( authorizationRequestRepository)) .redirectionEndpoint( redirectionEndpointConfig -> redirectionEndpointConfig.baseUri( "/login/oauth2/code/")) .userInfoEndpoint( userInfoEndpointConfig -> userInfoEndpointConfig.oidcUserService( oAuth2UserService)) .successHandler(oAuth2AuthenticationSuccessHandler) .failureHandler(oAuth2AuthenticationFailureHandler)); // add JWT authentication filter before username and password authentication filter httpSecurity.addFilterBefore( jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); } /** * Set cors configuration * * @return {@link CorsConfigurationSource} */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Collections.singletonList("*")); configuration.setAllowedMethods(Collections.singletonList("*")); configuration.setAllowedHeaders(Collections.singletonList("*")); // configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", configuration); return source; } }
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.app</groupId> <artifactId>license-analytics-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>license-analytics-server</name> <description>license-analytics-server</description> <properties> <java.version>17</java.version> <spring-cloud-azure.version>5.3.0</spring-cloud-azure.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>24.0.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-core</artifactId> <version>1.26.0</version> </dependency> <dependency> <groupId>com.azure.spring</groupId> <artifactId>azure-spring-boot-starter-active-directory</artifactId> <version>3.14.0</version> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-security-keyvault-secrets</artifactId> <version>4.2.3</version> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-identity</artifactId> <version>1.4.6</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
1. AntPathRequestMatcher使用错误
这是导致404的核心原因:你在authorizeHttpRequests中把两个路径传递给了单个AntPathRequestMatcher的构造函数,而该构造函数的第二个参数是HTTP方法,不是路径。
// 错误写法 .requestMatchers(new AntPathRequestMatcher("/oauth2/**", "/api/v1/auth/**"))
正确写法是拆分路径,直接传递多个字符串给requestMatchers:
.requestMatchers("/oauth2/**", "/api/v1/auth/**").permitAll()
或者创建多个AntPathRequestMatcher实例:
.requestMatchers( new AntPathRequestMatcher("/oauth2/**"), new AntPathRequestMatcher("/api/v1/auth/**") ).permitAll()
这个错误导致/oauth2/**路径没有被正确设置为允许匿名访问,请求被拦截后无法匹配到OAuth2授权端点,返回404。
2. OAuth2授权端点配置冗余
你在oauth2Login的authorizationEndpoint中显式设置了baseUri("/oauth2/authorization/azure"),但Spring Security OAuth2的默认授权端点格式就是/oauth2/authorization/{registrationId},这里你的registrationId是azure,所以这个配置完全多余,建议移除,避免潜在的路径冲突。
3. CORS配置不完整
当前CORS配置只对/api/**路径生效,但OAuth2相关的/oauth2/**和/login/oauth2/code/**路径也需要支持跨域(前端3000端口发起请求)。修改corsConfigurationSource:
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000")); // 生产环境指定具体域名,不要用* configuration.setAllowedMethods(Collections.singletonList("*")); configuration.setAllowedHeaders(Collections.singletonList("*")); configuration.setAllowCredentials(true); // OAuth2流程需要凭证支持 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", configuration); source.registerCorsConfiguration("/oauth2/**", configuration); source.registerCorsConfiguration("/login/oauth2/code/**", configuration); return source; }
4. 依赖版本冲突风险
你的Spring Boot版本是3.1.1,但azure-spring-boot-starter-active-directory用了3.14.0(该版本对应Spring Boot 2.x),Spring Boot 3.x需要搭配Spring Cloud Azure 4.x+版本。建议通过bom统一管理版本:
在pom.xml中添加依赖管理:
<dependencyManagement> <dependencies> <dependency> <groupId>com.azure.spring</

