返回

Spring Security Google 登录重定向 404?原因与解决方法

java

搞定 Spring Security OAuth2 Google 登录重定向 404 报错

哥们儿,配 Spring Security 和 OAuth2 Google 登录的时候,是不是美滋滋地以为点一下“用 Google 继续”就能跳到 defaultSuccessUrl 了?结果呢,啪叽一下,浏览器地址栏显示 localhost:端口号//login/oauth2/code/google,然后给你一个大大的 404 页面,是不是有点懵?别急,这问题挺常见的,咱们来捋一捋。

遇到这个问题,你用的 Spring Boot 版本大概是 5.3.x,Spring Security OAuth2 Client 版本可能是 5.7.x。你说 Google Cloud Console 里的 URI 和 OAuth2ClientConfig 都配对了,那问题可能出在哪儿呢?

啥情况会导致 404?

这个 /login/oauth2/code/google URL 不是随便写的,它是 Spring Security OAuth2 Client 默认处理 Google 登录回调的端点 (Endpoint)。它的格式通常是 {baseUrl}/login/oauth2/code/{registrationId}

当你从 Google 登录页面成功授权,Google 会把你重定向回你的应用程序,带着一个授权码 (authorization code) ,目标就是这个回调 URL。Spring Security 有个专门的过滤器 (Filter) 应该在这里等着,拿到授权码,再去跟 Google 换访问令牌 (access token),然后完成登录流程,最后才跳转到你指定的 defaultSuccessUrl

现在它 404 了,意思就是:请求是发过来了,但是 Spring Security 没有成功拦截并处理这个 /login/oauth2/code/google 请求。 为啥没处理呢?原因可能五花八门,但主要就那么几个方向:

  1. Security 配置没对: SecurityFilterChain 配置里,可能没正确启用 OAuth2 登录,或者授权规则不小心把这个回调地址给挡了。
  2. application.properties/yml 配错了: 虽然你说配置对了,但魔鬼在细节。Client ID、Client Secret 或者特别是重定向 URI (redirect-uri) 没跟 Google Cloud Console 完全对上。
  3. registrationId 不匹配: URL 里的 google 必须跟你配置文件里的 registration ID 一致。
  4. 根路径 (Base URL) 或 上下文路径 (Context Path) 有坑: Spring Security 拼 redirect_uri 或者处理回调请求时,对应用的基础 URL 或 context path 理解错了,导致路径匹配不上。特别注意那个 // 双斜杠,这往往是路径拼接出问题的信号。
  5. 依赖冲突或版本问题: 项目里依赖复杂,可能存在版本冲突。
  6. 其他过滤器捣乱: 自定义的 Filter 或者其他 Web 配置,可能抢在 Spring Security 前面处理了请求,或者干扰了路径匹配。

怎么一步步解决?

别慌,咱们一个个来排查。

1. 检查 SecurityFilterChain 配置

这是最直接的地方。看看你的 SecurityConfig.java

原理:

你需要确保:

  • .oauth2Login() 被调用了,表示启用 OAuth2 登录流程。
  • authorizeRequests() (或新版 authorizeHttpRequests()) 里的规则,必须允许对 /login/oauth2/code/** 路径的访问。因为用户在 Google 授权后跳回来时,是处于未完全认证状态的,需要先能访问这个回调地址,才能完成认证。

怎么做:

检查你的 securityFilterChain Bean 定义:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig { // 注意:你的例子里 @Import(OAuth2ClientConfig.class) 可能不是必须的,
                             // 如果 ClientRegistrationRepository 已经能通过依赖注入获得

    // ClientRegistrationRepository 可以通过 Spring Boot 自动配置注入
    // 无需显式 @Import 和构造函数注入,除非你有非常特殊的配置需求
    // public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
    //     this.clientRegistrationRepository = clientRegistrationRepository;
    // }

    // AuthenticationManager 通常由 Spring Boot 自动配置管理,一般不需要自己显式配置
    // @Bean
    // public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    //     return http.getSharedObject(AuthenticationManagerBuilder.class).build();
    // }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 开发环境可以禁用,生产环境建议开启并配置
            .authorizeHttpRequests(authorize -> authorize
                // 核心:确保回调路径是公开访问的
                // 注意你代码里的 .antMatchers("/login", "/login/oauth2/**", "/oauth2/** ", "/myapp/login/oauth2/code/**").permitAll()
                // 一般只需要 "/login/oauth2/code/**" 和可能的登录页面路径
                // 如果你的应用没有context path "/myapp",那么 "/myapp/login/oauth2/code/**" 就没用
                // 建议精简为必须的路径
                .mvcMatchers("/", "/login", "/login/**", "/oauth2/** ", "/error").permitAll() // 允许访问登录页, 错误页及OAuth2相关路径
                // .antMatchers("/login/oauth2/code/*").permitAll() // 更精确一点也可以
                .anyRequest().authenticated() // 其他所有请求都需要认证
            )
            // 启用 OAuth2 登录
            .oauth2Login(oauth2 -> oauth2
                // 这里可以指定登录页面,如果需要自定义的话
                // .loginPage("/login")
                // 认证成功后的默认跳转地址
                .defaultSuccessUrl("/api/hi", true) // true 表示强制跳转,即使用户之前访问了受保护页面
                                                   // 根据需求看是否需要强制
                // (可选) 配置用户信息端点等
                // .userInfoEndpoint(userInfo -> ...)
                // (可选) 配置授权请求处理
                // .authorizationEndpoint(authz -> authz
                //      .baseUri("/oauth2/authorization") // 默认的授权请求触发路径
                //      // ... 其他配置
                // )
                 // (可选) 配置重定向回调处理
                .redirectionEndpoint(redir -> redir
                     .baseUri("/login/oauth2/code/*") // 指定处理回调的基础URI,确保和Google配置及路径匹配
                )
            );

            // 如果有 form login 也需要配置
            // .formLogin(formLogin -> formLogin
            //     .loginPage("/login")
            //     .permitAll()
            // );

        return http.build();
    }
}

注意点:

  • authorizeRequests() 现在推荐用 authorizeHttpRequests() 和 lambda 表达式配置,更简洁。
  • 确认你的 permitAll() 规则确实覆盖了 /login/oauth2/code/google。最保险的是用 /login/oauth2/code/** 或者更具体的 /login/oauth2/code/google
  • 检查是不是有其他 antMatcher 规则意外地覆盖了或优先拦截了这个路径,并要求了认证。Spring Security 配置的顺序很重要。
  • 那个 /myapp/login/oauth2/code/**permitAll 有点奇怪,除非你的应用真的部署在 /myapp context path 下,并且回调 URI 也配置成了这个。一般情况下,标准回调路径不带应用名。

2. 核对 application.propertiesapplication.yml

这是 OAuth2 客户端信息的核心配置地。一点差错都可能导致流程失败。

原理:

Spring Boot 会根据这里的配置信息,自动帮你构建 ClientRegistration 对象。关键信息包括:

  • client-id: Google Cloud Console 给你的客户端 ID。
  • client-secret: Google Cloud Console 给你的客户端密钥。
  • scope: 你向 Google 请求的用户信息范围,如 openid, profile, email
  • redirect-uri: 重中之重! 这个 URI 必须和你 在 Google Cloud Console -> APIs & Services -> Credentials -> 你的 OAuth 2.0 Client ID -> Authorized redirect URIs 里添加的那个 URI 一模一样! 一点不多,一点不少,包括 http 还是 https,端口号,路径等。

怎么做:

检查你的 application.yml (或者 .properties 文件):

spring:
  security:
    oauth2:
      client:
        registration:
          google: # 这个 'google' 就是 registrationId,要和回调URL路径一致
            client-id: YOUR_GOOGLE_CLIENT_ID # 替换成你的 Client ID
            client-secret: YOUR_GOOGLE_CLIENT_SECRET # 替换成你的 Client Secret
            scope: openid, profile, email # 根据需要调整 scope
            # redirect-uri: 不需要显式配置,除非你需要覆盖默认值
            # 如果不配置,Spring Boot 会默认使用模板: {baseUrl}/{action}/oauth2/code/{registrationId}
            # 假设你的应用跑在 http://localhost:8080 且没有 context path
            # 那么默认解析出来就是 http://localhost:8080/login/oauth2/code/google
            # ---
            # 如果你的应用有 context path, 比如 /myapp (server.servlet.context-path=/myapp)
            # Spring Boot 通常能正确处理,解析为 http://localhost:8080/myapp/login/oauth2/code/google
            # ---
            # 只有在默认值不对,或者你想强制指定时才配置:
            # redirect-uri: http://localhost:8080/custom/callback/google # 极不推荐,除非完全理解影响
            # 如果配置了,那么 Google Console 里也必须是这个 URI,
            # 并且 SecurityConfig 里 .redirectionEndpoint().baseUri() 也需要对应调整。
        # 如果还有其他 provider (如 GitHub),类似地配置
        # provider:
        #   google: # 这部分通常由 Spring Boot 预设,除非需要自定义 issuer-uri 等
        #     issuer-uri: https://accounts.google.com

关键检查点:

  • client-idclient-secret :从 Google Cloud Console 复制过来,别带空格,别弄错。
  • registrationId (google) : 确保和你看到的 404 URL 路径 /login/oauth2/code/google 中的 google 部分一致。如果你的配置里用的是 my-google,那 URL 就应该是 /login/oauth2/code/my-google
  • redirect-uri 的核对 :
    • 登录 Google Cloud Console
    • 找到你的项目 -> APIs & Services -> Credentials。
    • 点击你的 "OAuth 2.0 Client ID"。
    • 查看 "Authorized redirect URIs" 列表。
    • 必须包含 你的应用回调地址。
      • 如果你的应用跑在 http://localhost:8080,没有 context path,这里就应该有 http://localhost:8080/login/oauth2/code/google
      • 如果你用了 80 端口或其他端口,相应修改。
      • 如果你部署在服务器上,用的是域名和 HTTPS,那就应该是 https://yourdomain.com/login/oauth2/code/google
      • 如果你配置了 server.servlet.context-path=/myapp,这里就应该是 http://localhost:8080/myapp/login/oauth2/code/google 这点非常非常重要!
    • 确认 Spring Boot 最终使用的 redirect-uri 和 Google Console 里配置的完全一致。可以在 Spring Boot 启动日志里找找看,或者 Debug 时检查 ClientRegistration 对象。

安全建议:

别把 client-secret 直接写在代码或配置文件里提交到 Git!用环境变量、配置文件服务器 (Config Server) 或云服务商的秘密管理服务来存。

3. 确认 registrationId 一致

这通常和上一步一起检查,但值得单独拎出来。

原理:

URL /login/oauth2/code/{registrationId} 中的 {registrationId} 部分,是由 Spring Security 动态获取的,它直接关联到你在配置文件中 spring.security.oauth2.client.registration. 下定义的那个 key。

怎么做:

  • 看你的 404 URL 是 /login/oauth2/code/google
  • 那么你的配置文件里必须是 spring.security.oauth2.client.registration.google
  • 如果配置文件是 registration.google-sso,那 URL 应该是 /login/oauth2/code/google-sso。确保两边大小写、拼写、连字符都一样。

4. 检查 Base URL 和 Context Path (处理 // 双斜杠)

那个 loalhost:myportnum//login/oauth2/code/google 中的 // 是个强烈的危险信号,通常意味着 URL 路径拼接出了问题。

原理:

Spring Security 在构建发往 Google 的认证请求中的 redirect_uri 参数时,需要知道你应用的基础 URL (schema, host, port)。它通常能自动探测,但有时会出错,尤其是在反向代理后面,或者配置了 server.servlet.context-path 时。同样地,当请求回调时,它也需要根据自身认为的基础 URL + context path 来匹配回调路径。如果这两个过程中的路径计算不一致,或者与 Google 那边配置的不符,就会出问题。双斜杠 // 往往是因为某个部分(比如 context path)被错误地包含了两次或者格式不对。

怎么做:

  • 检查 server.servlet.context-path:
    • application.propertiesyml 中找找看有没有设置 server.servlet.context-path
    • 如果有,比如 server.servlet.context-path=/myapp
    • 那么: Google Cloud Console 里的 Authorized redirect URI 必须是 http://localhost:端口号/myapp/login/oauth2/code/google (或其他对应的 schema/host)
    • 同时,确认你的应用内链接、访问都是基于 /myapp 这个前缀的。
    • 尝试暂时去掉 context-path 配置 ,把 Google Console 的 URI 也改成不带 /myapp 的标准形式 (http://localhost:端口号/login/oauth2/code/google),看看问题是否消失。如果消失了,说明就是 context path 处理有问题。
  • 检查反向代理 (如果用了 Nginx, Apache 等):
    • 如果你的 Spring Boot 应用跑在反向代理后面 (比如 Nginx 处理 HTTPS 和域名,然后转发到后端的 Spring Boot 应用),确保代理配置正确传递了原始请求的协议 (X-Forwarded-Proto)、主机 (X-Forwarded-Host) 和端口 (X-Forwarded-Port) 头信息。
    • 在 Spring Boot 的 application.properties/yml 里配置 server.forward-headers-strategy=framework (较新版本) 或 server.use-forward-headers=true (较旧版本),让 Spring Boot 能识别并使用这些头信息来判断自己的 Base URL。否则,它可能以为自己还在 http://localhost:8080 上运行,导致 redirect_uri 不匹配。
  • 浏览器开发者工具:
    • 打开浏览器开发者工具 (F12),切换到 Network (网络) 面板。
    • 重新触发登录流程。
    • 观察:
      • 点击“用 Google 继续”后,跳转到 Google 登录页的那个请求,看 URL 参数里的 redirect_uri 是什么?它是否和你期望的一致?是否包含双斜杠?
      • 从 Google 回调到你的应用,导致 404 的那个请求,看它的确切 URL 是什么?是不是真的有双斜杠 //
    • 这个双斜杠非常可疑。它可能是因为 context-path 配置最后带了个 /,或者 Spring 的路径拼接逻辑在某个环节出了 bug。确保你的 context-path (如果设置) 值是像 /myapp 这样,前后都没有多余的斜杠。

5. 检查依赖版本

虽然你的版本看着还行 (Spring 5.3.x, Security 5.7.x),但保险起见可以检查下。

原理:

不同库之间可能存在不兼容。Spring Boot 的 starter 包通常能很好地管理传递性依赖,但如果你手动指定了某些 Spring Security 或 OAuth2 相关库的版本,可能会引入冲突。

怎么做:

  • 依赖管理: 优先使用 Spring Boot 提供的 spring-boot-starter-parent 作为父 POM,并直接依赖 spring-boot-starter-oauth2-client。让 Spring Boot 统一管理版本。

    <!-- pom.xml -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <!-- 使用你项目对应的 Spring Boot 版本 -->
        <version>2.7.x</version> <!-- Spring Boot 2.7.x 对应 Spring Framework 5.3.x, Security 5.7.x -->
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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-oauth2-client</artifactId>
        </dependency>
        <!-- 其他依赖 -->
    </dependencies>
    
  • 检查冲突: 用 Maven 的 mvn dependency:tree 或 Gradle 的 gradle dependencies 命令查看项目依赖树,检查是否有多个不同版本的 Spring Security 或 OAuth2 相关库被引入。如果有,使用 <dependencyManagement> (Maven) 或 resolutionStrategy (Gradle) 解决冲突,尽量统一到 Spring Boot Parent 管理的版本。

6. 终极手段:开 Debug 日志

如果以上都检查了还没好,那就得看日志挖细节了。

原理:

打开 Spring Security 的 DEBUG 级别日志,可以看到它处理请求的详细过程,包括哪个 Filter 在工作,URL 匹配情况,发生了什么错误等。

怎么做:

application.propertiesapplication.yml 中添加:

logging:
  level:
    org.springframework.security: DEBUG
    org.springframework.web: DEBUG # 查看请求分发和处理的日志也可能有帮助
    org.springframework.security.oauth2: DEBUG # 更细致的 OAuth2 日志

然后重启应用,再次尝试 Google 登录。观察控制台输出的日志。特别关注和 /login/oauth2/code/google 请求相关的日志条目。看看是哪个 Filter 链在处理它,匹配结果如何,有没有异常抛出。

进阶调试:

可以在关键的 Spring Security Filter 上打断点进行调试,比如:

  • OAuth2LoginAuthenticationFilter: 处理 OAuth2 登录认证的核心 Filter。
  • OAuth2AuthorizationCodeGrantFilter: 处理授权码模式的回调,用 code 换 token。
  • 检查进入 Filter 时的 HttpServletRequest 对象的 getRequestURI()getContextPath() 等方法,看看实际收到的路径和你预期的是否一致。

排查这个 404 问题,通常就是要细心、耐心。从配置到代码,再到依赖和环境,一步步来。多数情况下,问题就藏在 redirect-uri 的完全匹配、SecurityFilterChainpermitAll 规则,或者是由 context-path、反向代理引起的 Base URL 解析错误上。那个 // 双斜杠尤其值得关注,很可能是 context-path 或路径拼接逻辑的问题。按着上面的步骤走一遍,应该能找到症结所在。