You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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</
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 05:42:07