Spring Boot 3.4.0集成Github OAuth2认证后回调URI 404问题
问题描述
在基于Spring Security的Spring Boot项目中集成Github OAuth2认证,回调URI已与Github端配置一致,但认证完成后返回404未找到错误。已尝试将该URI从Jwt过滤器、静态资源映射中排除,问题仍未解决。
Security配置代码
package eu.tasgroup.applicativo.conf; import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 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.UrlBasedCorsConfigurationSource; import eu.tasgroup.applicativo.filter.JwtAuthenticationFilter; import eu.tasgroup.applicativo.repository.AmministratoriRepository; import eu.tasgroup.applicativo.repository.ClientiRepository; import eu.tasgroup.applicativo.repository.PermessiAmministratoriRepository; import eu.tasgroup.applicativo.security.JwtAuthenticationEntryPoint; import static org.springframework.security.config.Customizer.withDefaults; @Configuration @EnableWebSecurity public class SecurityConfig { private final ClientiRepository cr; private final AmministratoriRepository ar; private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final PermessiAmministratoriRepository pr; private CustomerUserDetailsService cs; public SecurityConfig(ClientiRepository cr, AmministratoriRepository ar, PermessiAmministratoriRepository pr, JwtAuthenticationFilter jwtAuthenticationFilter, CustomerUserDetailsService cs, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) { this.cr = cr; this.ar = ar; this.pr = pr; this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; this.cs = cs; } @Bean @Order(2) SecurityFilterChain adminFilterChain(HttpSecurity http, CustomAuthenticationFailureHandler failureHandler, CustomAuthenticationSuccessHandler successHandler) throws Exception { http.securityMatcher("/admin/**") .authorizeHttpRequests( (authorize) -> authorize.requestMatchers(PathRequest.toStaticResources().atCommonLocations()) .permitAll().requestMatchers("/admin/admin-registrazione").permitAll() .requestMatchers("/admin/admin-form-registrazione").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN")) .formLogin((form) -> form.loginPage("/admin/admin-login").failureHandler(failureHandler) .successHandler(successHandler).usernameParameter("email").permitAll()) .logout((logout) -> logout.logoutRequestMatcher(new AntPathRequestMatcher("/admin/admin-logout")) .logoutSuccessUrl("/").invalidateHttpSession(true).clearAuthentication(true).permitAll()); return http.build(); } @Bean @Order(3) SecurityFilterChain userFilterChain(HttpSecurity http, CustomAuthenticationFailureHandler failureHandler, CustomAuthenticationSuccessHandler successHandler) throws Exception { http.securityMatcher("/user/**") .authorizeHttpRequests( authorize -> authorize.requestMatchers(PathRequest.toStaticResources().atCommonLocations()) .permitAll().requestMatchers("/user/user-registrazione").permitAll() .requestMatchers("/user/user-form-registrazione").permitAll() .requestMatchers("/user/**").hasRole("USER")) .formLogin(form -> form.loginPage("/user/user-login").usernameParameter("email") .failureHandler(failureHandler).successHandler(successHandler).permitAll()) .logout(logout -> logout.logoutRequestMatcher(new AntPathRequestMatcher("/user/user-logout")) .logoutSuccessUrl("/").permitAll()); return http.build(); } @Bean @Order(1) SecurityFilterChain githubOAuth2FilterChain(HttpSecurity http, CustomAuthenticationFailureHandler failureHandler, CustomAuthenticationSuccessHandler successHandler) throws Exception { http.securityMatcher("/oauth2/**").authorizeHttpRequests( (authorizeRequests) -> authorizeRequests .requestMatchers("/").permitAll() .anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .failureUrl("/user/user-login") .userInfoEndpoint(infoEndpoint -> infoEndpoint.userService(cs))) .logout(logout -> logout.logoutRequestMatcher(new AntPathRequestMatcher("/user/user-logout")) .logoutSuccessUrl("/").permitAll()); return http.build(); } @Bean @Order(4) SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/api/**") // 启用CORS并禁用CSRF .cors(cors -> cors.configurationSource(corsConfigurationSource())).csrf(csrf -> csrf.disable()) // 设置无状态会话 .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 处理未授权请求的异常 .exceptionHandling(handling -> handling.authenticationEntryPoint(jwtAuthenticationEntryPoint)) // 设置端点权限 .authorizeHttpRequests(auth -> auth.requestMatchers("/api/login").permitAll().anyRequest().authenticated()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(new CustomerUserDetailsService(ar, cr, pr)); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean UrlBasedCorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOriginPattern("http://localhost:4200"); // 使用匹配模式 config.addAllowedHeader("*"); config.addAllowedMethod("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }
控制台日志
- Securing GET /oauth2/ Set SecurityContextHolder to anonymous SecurityContext
- Saved request http://localhost:8080/oauth2/?continue to session Redirecting to http://localhost:8080/oauth2/authorization/todo
- Securing GET /oauth2/authorization/todo
- Redirecting to https://github.com/login/oauth/authorize?response_type=code&client_id=Ov23li6ngo5KgTDwUnFn&scope=user&state=xqHtfEOaN6Y4103mTO64Tv-1H9ZIITPivoliIHjixEM%3D&redirect_uri=http://localhost:8080/login/oauth2/code/github
- GET "/login/oauth2/code/github?code=7dbd4161ce384dca7df2&state=xqHtfEOaN6Y4103mTO64Tv-1H9ZIITPivoliIHjixEM%3D", parameters={masked}
应用配置
spring.security.oauth2.client.registration.todo.client-id=XXXXXXXXXXXXXXXXXXXXXX spring.security.oauth2.client.registration.todo.client-name=AppName spring.security.oauth2.client.registration.todo.provider=github spring.security.oauth2.client.registration.todo.client-secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX spring.security.oauth2.client.registration.todo.scope=user spring.security.oauth2.client.registration.todo.redirect-uri=http://localhost:8080/login/oauth2/code/github
解决方案
核心原因
你的githubOAuth2FilterChain仅配置了匹配/oauth2/**路径,但Github回调的路径是/login/oauth2/code/github,不在该过滤器链的匹配范围内,导致回调请求无法被OAuth2相关逻辑处理,最终返回404。
修复步骤
修改过滤器链匹配路径
更新githubOAuth2FilterChain的securityMatcher,同时包含OAuth2授权路径和回调路径:@Bean @Order(1) SecurityFilterChain githubOAuth2FilterChain(HttpSecurity http, CustomAuthenticationFailureHandler failureHandler, CustomAuthenticationSuccessHandler successHandler) throws Exception { // 同时匹配OAuth2授权和回调路径 http.securityMatcher("/oauth2/**", "/login/oauth2/**") .authorizeHttpRequests(authorizeRequests -> authorizeRequests .requestMatchers("/").permitAll() .anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .failureUrl("/user/user-login") .userInfoEndpoint(infoEndpoint -> infoEndpoint.userService(cs))) .logout(logout -> logout.logoutRequestMatcher(new AntPathRequestMatcher("/user/user-logout")) .logoutSuccessUrl("/").permitAll()); return http.build(); }验证路径拦截规则
确认其他过滤器链(如apiFilterChain)的匹配路径不会覆盖/login/oauth2/**,当前你的apiFilterChain仅匹配/api/**,无需调整。确认回调URI一致性
再次核对Github后台配置的回调URI与application.properties中的redirect-uri完全一致,包括协议、端口和完整路径,不能有任何拼写错误。
额外建议
Spring Security OAuth2的默认回调路径格式为/login/oauth2/code/{registrationId},你的registrationId是todo,但配置中写的是github,虽然当前配置的redirect-uri能正常工作,但建议保持registrationId与provider的对应关系,避免后续维护时混淆。
内容的提问来源于stack exchange,提问作者Samuel Mastrelli

