React前端+Spring Boot后端OAuth2登录按钮失效排查求助
问题排查:Spring Boot + React OAuth2登录按钮无法跳转授权页面
问题描述
我有一个基于Spring Boot后端和React(Material-UI)前端的项目,计划通过OAuth2结合Spring Security实现注册/登录功能。当前端页面点击Google或GitHub登录按钮时,无法跳转到对应的授权登录页面;但未接入前端时,GitHub OAuth2功能可正常运行。已配置SecurityConfiguration类及application.yml中的客户端信息,不知如何排查解决。
相关代码
前端登录组件代码
import * as React from 'react'; import Avatar from '@mui/material/Avatar'; import Button from '@mui/material/Button'; import Link from '@mui/material/Link'; import Box from '@mui/material/Box'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import Typography from '@mui/material/Typography'; import Container from '@mui/material/Container'; import { createTheme, ThemeProvider } from '@mui/material/styles'; function Copyright(props: any) { return ( <Typography variant='body2' color='text.secondary' align='center' {...props} > {'Copyright © '} <Link color='inherit' href='https://mui.com/' > Your Website </Link>{' '} {new Date().getFullYear()} {'.'} </Typography> ); } const theme = createTheme(); export default function LogIn() { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const data = new FormData(event.currentTarget); console.log({ email: data.get('email'), password: data.get('password'), }); }; return ( <ThemeProvider theme={theme}> <Container component='main' maxWidth='xs' > <Box sx={{ marginTop: 8, display: 'flex', flexDirection: 'column', alignItems: 'center', }} > <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}> <LockOutlinedIcon /> </Avatar> <Typography component='h1' variant='h5' > Sign in </Typography> <Link sx={{width: '100%'}} href="/oauth2/authorization/github" > <Button fullWidth variant='contained' sx={{ mt: 3, mb: 1 }} > GITHUB LOGIN </Button> </Link> <Link sx={{width: '100%'}} href="/oauth2/authorization/google"> <Button fullWidth variant='contained' sx={{ mt: 1, mb: 1 }} > GOOGLE LOGIN </Button> </Link> </Box> </Container> </ThemeProvider> ); }
后端Security配置代码
package SideProject.WorkoutJournal.SideProjectWorkoutJournal.SecurityConfigurations; import SideProject.WorkoutJournal.SideProjectWorkoutJournal.Entities.AuthProvider; import SideProject.WorkoutJournal.SideProjectWorkoutJournal.Entities.UserAccount; import SideProject.WorkoutJournal.SideProjectWorkoutJournal.Repos.UserAccountRepo; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Value; 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.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import java.io.IOException; import java.util.StringJoiner; import java.util.Objects; @Configuration @EnableWebSecurity public class SecurityConfiguration { private UserAccountRepo userRepo; public SecurityConfiguration( UserAccountRepo userRepo ) { this.userRepo = userRepo; } // Security Filter Chain --> Ordering of the security filters are important... @Bean SecurityFilterChain unsecuredFilter( HttpSecurity http ) throws Exception { http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) .authorizeHttpRequests().requestMatchers("/","/log-in").permitAll() .requestMatchers("/workoutMain/**").authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1) // .sessionRegistry(sessionRegistry()) .and().and() .headers() .xssProtection() // Prevent's XSS Cross-Site Scripting .and() .contentSecurityPolicy(contentSecurityPolicy()) // Ensures that the origin types come from self, Prevents different types of Data Injections. .and().and() .oauth2Login() .loginPage("/log-in") .successHandler( new SavedRequestAwareAuthenticationSuccessHandler(){ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // Reason why I have to cast it into a DefaultOAuth2User is because what is returned is an Object. // The principle object is always returned after a successful authentication. DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal(); UserAccount newUser = getUserInfoFromAuth(request.getRequestURI(), oAuth2User); //Setting the Username and picture if it changed. if (Objects.nonNull(newUser) && Objects.nonNull(newUser.getProviderId())){ UserAccount user = userRepo.findByProviderId(newUser.getProviderId()); user.setImageURL(newUser.getImageURL()); user.setUsername(newUser.getUsername()); userRepo.save(user); } super.onAuthenticationSuccess(request, response, authentication); } } ) .and() // .formLogin().disable() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/")) .logoutSuccessUrl("/log-in") .deleteCookies("JSESSIONID", "XSRF-TOKEN") .invalidateHttpSession(true); // .and() // TODO: Need to figure out the Filter Later // .addFilterBefore() return http.build(); } private UserAccount getUserInfoFromAuth(String requestURI, DefaultOAuth2User oAuth2User) { if (requestURI.endsWith("google")) { return getGoogleUserInfo(oAuth2User); } else if (requestURI.endsWith("github")) { return getGithubUserInfo(oAuth2User); } return null; } private UserAccount getGithubUserInfo(DefaultOAuth2User oAuth2User) { String authId = oAuth2User.getAttributes().get("id").toString(); String userName = oAuth2User.getAttributes().get("login").toString(); String avatarUrl = oAuth2User.getAttributes().get("avatar_url").toString(); return new UserAccount( userName, avatarUrl, authId, AuthProvider.Github); } private UserAccount getGoogleUserInfo(DefaultOAuth2User oAuth2User) { String authId = oAuth2User.getAttributes().get("sub").toString(); String userName = oAuth2User.getAttributes().get("given_name").toString(); String avatarUrl = oAuth2User.getAttributes().get("picture").toString(); return new UserAccount(userName, avatarUrl, authId, AuthProvider.Google); } private String contentSecurityPolicy() { StringJoiner cspDirectives = new StringJoiner(";"); return cspDirectives.add("default-src 'none'") // When you say self, you're saying that you want spring to only trust .add("script-src 'self'") .add("connect-src 'self'") .add("img-src 'self'") .add("frame-ancestors 'none'") .add("form-action 'self'") .add("font-src 'self'") .add("manifest-src 'self'") .add("style-src 'self'").toString(); } }
application.yml配置
spring: security: oauth2: client: registration: google: redirect-uri: "http://localhost:8080/login/oauth2/code.google.com" clientId: ${GoogleClientID} clientSecret: ${GoogleSecretID} scope: - email - profile github: redirect-uri: "http://localhost:8080/login/oauth2/code/github.com" clientId: ${GithubClientId} clientSecret: ${GithubSecretKey} datasource: url: 'jdbc:postgresql://localhost:5430/postgres' username: ${PostgressUsername} password: ${PostgresPassword} driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: create-drop show_sql: true properties: hibernate: format_sql: true server: port: '8080' servlet: session: cookie: secure: true http-only: true tracking-modes: COOKIE shutdown: graceful info: session: timeout: 900 warningPeriod: 60
排查与解决步骤
1. 放开OAuth2相关端点的匿名访问权限
当前Security配置仅允许/和/log-in匿名访问,OAuth2的授权和回调端点被拦截,需添加到白名单:
.authorizeHttpRequests() .requestMatchers("/","/log-in", "/oauth2/authorization/**", "/login/oauth2/code/**").permitAll() .requestMatchers("/workoutMain/**").authenticated()
2. 修正重定向URI格式
application.yml中Google的重定向URI格式错误,正确格式应为:
google: redirect-uri: "http://localhost:8080/login/oauth2/code/google" github: redirect-uri: "http://localhost:8080/login/oauth2/code/github"
(后缀需与客户端注册标识一致,Google为google,GitHub为github,而非google.com/github.com)
3. 解决跨域问题(前后端端口不同时)
若前端运行在非8080端口(如3000),需配置CORS允许前端域名请求:
@Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:3000")); // 替换为你的前端地址 config.setAllowedMethods(List.of("GET", "POST", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; }
同时前端需配置代理(在package.json中添加):
"proxy": "http://localhost:8080"
4. 调整会话Cookie的Secure属性
本地开发使用HTTP协议时,secure: true会导致浏览器拒绝保存Cookie,需改为:
server: servlet: session: cookie: secure: false
5. 修正前端跳转逻辑
避免Link与Button嵌套的潜在冲突,直接通过Button的点击事件触发跳转:
const handleGithubLogin = () => { window.location.href = '/oauth2/authorization/github'; }; const handleGoogleLogin = () => { window.location.href = '/oauth2/authorization/google'; }; // 替换原Link+Button结构 <Button fullWidth variant='contained' sx={{ mt: 3, mb: 1 }} onClick={handleGithubLogin} > GITHUB LOGIN </Button> <Button fullWidth variant='contained' sx={{ mt: 1, mb: 1 }} onClick={handleGoogleLogin} > GOOGLE LOGIN </Button>
6. 查看后端DEBUG日志
开启Spring Security的DEBUG日志,排查请求拦截细节:
logging: level: org.springframework.security: DEBUG
内容的提问来源于stack exchange,提问作者RogueGingerz
相关产品推荐
相关产品推荐

