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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 06:30:11