使用React-OAuth进行Google/GitHub登录的教程和案例

1. 概述

react-oauth是一个基于React生态的OAuth 2.0认证库,它封装了OAuth认证流程中的复杂细节,提供了声明式的组件API和简洁的钩子函数(Hooks),支持多种OAuth授权流程(如授权码流程、隐式流程等),并兼容主流的身份提供商(Identity Provider,简称IdP),如Google、Microsoft、GitHub、Auth0等。

该插件的核心优势在于:

  • 开箱即用:无需手动处理OAuth流程中的跳转、令牌存储、刷新等细节
  • 类型安全:完全支持TypeScript,提供完整的类型定义
  • 灵活扩展:支持自定义认证流程和令牌管理策略
  • React生态友好:与React Router、Redux等主流库无缝集成
  • 轻量级:核心包体积小,无过多依赖,性能优秀

1.1 核心概念解析

在使用react-oauth前,需要先理解几个OAuth 2.0的核心概念:

  • 授权服务器(Authorization Server):负责验证用户身份并颁发令牌(如Google账号服务器)
  • 资源服务器(Resource Server):存储用户资源的服务器(如Google Drive)
  • 客户端(Client):即我们开发的React应用,需要在授权服务器注册并获取Client ID
  • 令牌(Token)
    • 访问令牌(Access Token):用于访问资源服务器的短期令牌
    • 刷新令牌(Refresh Token):用于获取新访问令牌的长期令牌(部分授权流程支持)
  • 授权流程react-oauth主要支持两种常用流程:
    • 授权码流程(Authorization Code Flow):安全性高,适用于服务端存在的场景
    • 隐式流程(Implicit Flow):仅客户端参与,适用于纯前端应用(安全性较低,建议优先使用授权码流程)

2. 环境准备与安装

前置条件:

  • 已创建React项目(建议使用React 16.8+,支持Hooks)
  • 已在目标身份提供商(如Google)平台注册应用,获取Client ID
    • 以Google为例,注册地址:Google Cloud Console
    • 注册时需配置授权回调地址(Redirect URI) ,如http://localhost:3000/auth/callback

2.1. 安装依赖

react-oauth的核心包为@react-oauth/google(针对Google认证),若需支持其他身份提供商,可安装对应的扩展包。以Google认证为例,安装命令如下:

bash 复制代码
# 使用npm
npm install @react-oauth/google axios  # axios用于后续API请求

# 使用yarn
yarn add @react-oauth/google axios
  • @react-oauth/google:Google OAuth认证的核心包
  • axios:用于携带访问令牌请求资源服务器API(非必需,可替换为fetch等)

3. 基础使用:实现Google第三方登录

3.1. 全局配置OAuth客户端

首先,在React应用的入口文件(如src/index.js)中,通过GoogleOAuthProvider组件配置全局Client ID,确保所有子组件都能访问到认证上下文:

jsx 复制代码
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { GoogleOAuthProvider } from '@react-oauth/google';
import App from './App';
import './index.css';

// 替换为你的Google Client ID
const clientId = 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <GoogleOAuthProvider clientId={clientId}>
      <App />
    </GoogleOAuthProvider>
  </React.StrictMode>
);

3.2. 实现登录按钮

react-oauth提供了两种实现登录的方式:预制登录按钮和自定义登录按钮,开发者可根据需求选择。

方式1:使用预制登录按钮

预制按钮已封装好Google官方样式,支持一键登录,无需自定义UI:

jsx 复制代码
// src/components/GoogleLoginButton.js
import React from 'react';
import { GoogleLogin } from '@react-oauth/google';
import axios from 'axios';

const GoogleLoginButton = () => {
  // 登录成功回调
  const handleSuccess = async (response) => {
    console.log('登录成功,响应数据:', response);
    const { credential } = response; // credential即为访问令牌(Access Token)

    // 1. 存储令牌(建议使用localStorage或专用状态管理库)
    localStorage.setItem('accessToken', credential);

    // 2. 携带令牌请求资源服务器API(示例:获取Google用户信息)
    try {
      const userResponse = await axios.get(
        'https://www.googleapis.com/oauth2/v3/userinfo',
        {
          headers: { Authorization: `Bearer ${credential}` },
        }
      );
      console.log('用户信息:', userResponse.data);
      // 可将用户信息存入全局状态(如Redux、Context)
    } catch (error) {
      console.error('获取用户信息失败:', error);
    }
  };

  // 登录失败回调
  const handleError = (error) => {
    console.error('登录失败:', error);
    alert('登录失败,请稍后重试');
  };

  return (
    <GoogleLogin
      onSuccess={handleSuccess}
      onError={handleError}
      // 可选配置
      type="standard" // 按钮类型:standard(默认)、icon
      size="large" // 按钮大小:small、medium、large
      text="signin_with" // 按钮文本:signin_with(默认)、signup_with、continue_with
      shape="rectangular" // 按钮形状:rectangular(默认)、pill、circle、square
      theme="filled_black" // 按钮主题:filled_black(默认)、filled_blue、outline、standard
    />
  );
};

export default GoogleLoginButton;

方式2:自定义登录按钮

若需自定义登录按钮样式,可使用useGoogleLogin钩子函数,手动触发登录流程:

jsx 复制代码
// src/components/CustomGoogleLogin.js
import React from 'react';
import { useGoogleLogin } from '@react-oauth/google';
import axios from 'axios';
import './CustomGoogleLogin.css';

const CustomGoogleLogin = () => {
  // 初始化登录钩子
  const login = useGoogleLogin({
    onSuccess: async (response) => {
      console.log('登录成功,令牌:', response.access_token);
      localStorage.setItem('accessToken', response.access_token);

      // 获取用户信息
      const userInfo = await axios.get(
        'https://www.googleapis.com/oauth2/v3/userinfo',
        {
          headers: { Authorization: `Bearer ${response.access_token}` },
        }
      );
      console.log('用户信息:', userInfo.data);
    },
    onError: (error) => {
      console.error('登录失败:', error);
      alert('登录失败,请检查网络或账号状态');
    },
    // 可选:设置令牌刷新策略
    flow: 'auth-code', // 使用授权码流程(推荐),默认是implicit流程
    scope: 'openid email profile', // 请求的权限范围
  });

  return (
    <button 
      className="custom-login-btn" 
      onClick={() => login()} // 点击触发登录
    >
      <img 
        src="https://www.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_74x24px.svg" 
        alt="Google Logo" 
        className="google-logo"
      />
      自定义Google登录
    </button>
  );
};

export default CustomGoogleLogin;

对应的CSS样式(CustomGoogleLogin.css):

css 复制代码
.custom-login-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.2s;
}

.custom-login-btn:hover {
  background: #f5f5f5;
}

.google-logo {
  width: 24px;
  height: 24px;
}

3.3. 实现登出功能

登出功能主要涉及清除本地存储的令牌,并可选地调用身份提供商的登出API(如Google的全局登出):

jsx 复制代码
// src/components/LogoutButton.js
import React from 'react';
import { useGoogleLogout } from '@react-oauth/google';

const LogoutButton = () => {
  // 初始化登出钩子
  const logout = useGoogleLogout({
    onSuccess: () => {
      // 1. 清除本地存储的令牌
      localStorage.removeItem('accessToken');
      // 2. 清除全局用户状态(如Redux中的user信息)
      // dispatch(clearUser());
      console.log('登出成功');
      alert('已成功登出');
    },
    onError: (error) => {
      console.error('登出失败:', error);
    },
  });

  return (
    <button 
      style={{
        padding: '8px 16px',
        backgroundColor: '#dc3545',
        color: 'white',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
        marginTop: '16px',
      }}
      onClick={() => logout()}
    >
      登出
    </button>
  );
};

export default LogoutButton;

4. 进阶技巧:令牌管理与路由保护

4.1. 令牌过期与刷新

访问令牌(Access Token)通常有较短的有效期(如Google的令牌有效期为1小时)。当令牌过期后,需通过刷新令牌(Refresh Token) 获取新的访问令牌,避免用户重复登录。

react-oauthuseGoogleLogin钩子支持通过flow: 'auth-code'启用授权码流程,该流程会返回刷新令牌。以下是实现令牌刷新的示例:

jsx 复制代码
// src/hooks/useTokenRefresh.js
import { useEffect, useState } from 'react';
import axios from 'axios';

// 替换为你的Google Client ID和Client Secret(Client Secret需在服务端存储,避免暴露在前端)
const CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com';
const CLIENT_SECRET = 'YOUR_GOOGLE_CLIENT_SECRET'; // 仅服务端使用!

const useTokenRefresh = () => {
  const [accessToken, setAccessToken] = useState(localStorage.getItem('accessToken'));
  const refreshToken = localStorage.getItem('refreshToken');

  // 刷新令牌函数
  const refreshAccessToken = async () => {
    try {
      // 注意:该请求需在服务端发起,避免Client Secret泄露
      const response = await axios.post(
        'https://oauth2.googleapis.com/token',
        {
          client_id: CLIENT_ID,
          client_secret: CLIENT_SECRET,
          refresh_token: refreshToken,
          grant_type: 'refresh_token',
        }
      );

      const { access_token: newAccessToken } = response.data;
      // 更新本地存储和状态
      localStorage.setItem('accessToken', newAccessToken);
      setAccessToken(newAccessToken);
      return newAccessToken;
    } catch (error) {
      console.error('令牌刷新失败:', error);
      // 刷新失败,引导用户重新登录
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
      window.location.href = '/login';
      return null;
    }
  };

  // 监听令牌过期(可通过定时器定期刷新)
  useEffect(() => {
    if (!refreshToken) return;

    // 每55分钟刷新一次令牌(提前5分钟避免令牌过期)
    const refreshInterval = setInterval(refreshAccessToken, 55 * 60 * 1000);
    return () => clearInterval(refreshInterval);
  }, [refreshToken]);

  return { accessToken, refreshAccessToken };
};

export default useTokenRefresh;

安全提示:Client Secret是敏感信息,绝对不能暴露在前端代码中。上述令牌刷新请求必须在服务端发起,前端需通过调用自身服务端接口间接获取新令牌。

4.2. 路由保护:未登录用户重定向

在React应用中,通常需要保护某些路由(如个人中心),仅允许已登录用户访问。可通过React Router的Navigate组件实现未登录用户的重定向:

jsx 复制代码
// src/components/PrivateRoute.js
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';

const PrivateRoute = () => {
  // 检查本地存储中是否存在有效令牌
  const isAuthenticated = !!localStorage.getItem('accessToken');

  // 已登录:渲染子路由(Outlet);未登录:重定向到登录页
  return isAuthenticated ? <Outlet /> : <Navigate to="/login" replace />;
};

export default PrivateRoute;

在路由配置中使用PrivateRoute

jsx 复制代码
// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import GoogleLoginButton from './components/GoogleLoginButton';
import LogoutButton from './components/LogoutButton';
import PrivateRoute from './components/PrivateRoute';
import ProfilePage from './pages/ProfilePage'; // 个人中心页面

function App() {
  return (
    <Router>
      <div className="App" style={{ padding: '24px' }}>
        <Routes>
          {/* 登录页:公开访问 */}
          <Route path="/login" element={<GoogleLoginButton />} />
          {/* 受保护路由:仅已登录用户访问 */}
          <Route element={<PrivateRoute />}>
            <Route 
              path="/profile" 
              element={
                <>
                  <ProfilePage />
                  <LogoutButton />
                </>
              } 
            />
          </Route>
          {/* 默认路由:重定向到登录页 */}
          <Route path="/" element={<Navigate to="/login" replace />} />
        </Routes>
      </div>
    </Router>
  );
}

export default App;

5. 常见问题与解决方案

5.1. 登录时提示"Invalid redirect URI"

问题原因 :前端配置的回调地址(Redirect URI)与在身份提供商(如Google Cloud Console)注册的地址不匹配。
解决方案

  1. 登录Google Cloud Console,进入应用的"API和服务"→"凭据"页面
  2. 找到对应的OAuth 2.0客户端ID,点击"编辑"
  3. 在"已授权的重定向URI"中添加当前前端应用的回调地址(如http://localhost:3000https://your-app-domain.com/auth/callback
  4. 保存配置后,等待5-10分钟生效(配置可能存在延迟)

5.2. 令牌存储安全问题

问题原因 :将访问令牌存储在localStorage中存在XSS(跨站脚本攻击)风险,攻击者可通过注入脚本窃取令牌。
解决方案

  1. 优先使用HttpOnly Cookie存储令牌(需服务端配合),HttpOnly属性可防止JavaScript访问Cookie,避免XSS攻击
  2. 若使用localStorage,需加强前端安全措施:
    • 开启CSP(内容安全策略),限制脚本执行源
    • 对用户输入进行严格过滤,防止XSS注入
    • 缩短令牌有效期,减少泄露风险

5.3. 刷新令牌失效

问题原因

  • 刷新令牌已过期(部分身份提供商对刷新令牌有有效期限制)
  • 用户已撤销应用的访问权限
  • 同一用户在多个设备登录,导致旧刷新令牌失效
    解决方案
  1. 捕获令牌刷新失败的错误,引导用户重新登录
  2. 在服务端记录刷新令牌的使用状态,避免重复使用
  3. 向用户提示"登录已过期,请重新登录",提升用户体验

6. 总结与扩展

6.1. 支持其他身份提供商

除了 Google,react-oauth生态还支持 Microsoft、GitHub、Auth0 等主流身份提供商,核心思路是通过对应的扩展包或自定义配置实现认证流程。以下以 Microsoft 和 GitHub 为例,介绍集成方法。

6.1.1. 集成 Microsoft 登录

  1. 前置准备
    • Microsoft Entra ID 中注册应用,获取 Client ID ,并配置回调地址(如 http://localhost:3000/auth/microsoft/callback)。
    • 安装 Microsoft 认证包:
bash 复制代码
     npm install @react-oauth/microsoft
  1. 全局配置 : 在入口文件中使用 MicrosoftOAuthProvider 包裹应用:
jsx 复制代码
   // src/index.js
   import { MicrosoftOAuthProvider } from '@react-oauth/microsoft';

   const clientId = 'YOUR_MICROSOFT_CLIENT_ID';

   root.render(
     <React.StrictMode>
       <MicrosoftOAuthProvider clientId={clientId}>
         <App />
       </MicrosoftOAuthProvider>
     </React.StrictMode>
   );
  1. 实现登录按钮 : 类似 Google 登录,使用 MicrosoftLogin 组件或 useMicrosoftLogin Hook:
jsx 复制代码
   // src/components/MicrosoftLoginButton.js
   import { MicrosoftLogin } from '@react-oauth/microsoft';

   const MicrosoftLoginButton = () => {
     const handleSuccess = (response) => {
       console.log('Microsoft 登录成功:', response);
       localStorage.setItem('msAccessToken', response.access_token);
     };

     return (
       <MicrosoftLogin
         onSuccess={handleSuccess}
         onError={(error) => console.error('登录失败:', error)}
         scope="user.read email" // 请求用户信息权限
       />
     );
   };

   export default MicrosoftLoginButton;

6.1.2. 集成GitHub登录

GitHub 认证需通过自定义 OAuth 流程实现(react-oauth 暂未提供专用包),核心步骤如下:

  1. 前置准备

    • GitHub Developer Settings 中注册应用,获取 Client IDClient Secret (Client Secret 仅服务端使用),配置回调地址(如 http://localhost:3000/auth/github/callback)。
  2. 前端实现登录跳转: 构造 GitHub 授权链接,引导用户跳转至 GitHub 授权页面:

jsx 复制代码
   // src/components/GitHubLoginButton.js
   const GitHubLoginButton = () => {
     const clientId = 'YOUR_GITHUB_CLIENT_ID';
     const redirectUri = 'http://localhost:3000/auth/github/callback';
     const scope = 'user:email';

     const handleLogin = () => {
       const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`;
       window.location.href = authUrl;
     };

     return (
       <button onClick={handleLogin} style={{ padding: '8px 16px', cursor: 'pointer' }}>
         用 GitHub 登录
       </button>
     );
   };

   export default GitHubLoginButton;
  1. 服务端获取令牌与用户信息 : 用户授权后,GitHub 会重定向至回调地址并携带 code 参数,服务端需通过该 code 换取访问令牌,再请求用户信息:
javascript 复制代码
   // 服务端示例(Node.js/Express)
   const axios = require('axios');

   app.get('/auth/github/callback', async (req, res) => {
     const { code } = req.query;
     const clientId = 'YOUR_GITHUB_CLIENT_ID';
     const clientSecret = 'YOUR_GITHUB_CLIENT_SECRET';

     // 1. 换取访问令牌
     const tokenResponse = await axios.post(
       'https://github.com/login/oauth/access_token',
       { client_id: clientId, client_secret: clientSecret, code },
       { headers: { Accept: 'application/json' } }
     );
     const { access_token } = tokenResponse.data;

     // 2. 获取用户信息
     const userResponse = await axios.get('https://api.github.com/user/emails', {
       headers: { Authorization: `Bearer ${access_token}` },
     });

     // 3. 返回令牌给前端(建议通过 Cookie 或 JWT 封装)
     res.cookie('githubAccessToken', access_token, { httpOnly: true });
     res.redirect('/profile');
   });

6.2. 性能优化

认证流程若处理不当,可能导致页面加载延迟、重复请求等问题。以下是针对性的优化方案:

6.2.1. 延迟加载认证组件

对于非首屏的登录组件(如个人中心页的登录入口),可使用 React 的 React.lazySuspense 实现延迟加载,减少首屏加载时间:

jsx 复制代码
// src/App.js
import React, { Suspense, lazy } from 'react';

// 延迟加载登录组件
const GoogleLoginButton = lazy(() => import('./components/GoogleLoginButton'));

function App() {
  return (
    <Router>
      <Routes>
        <Route
          path="/login"
          element={
            <Suspense fallback={<div>加载中...</div>}>
              <GoogleLoginButton />
            </Suspense>
          }
        />
      </Routes>
    </Router>
  );
}

6.2.2. 缓存用户信息,避免重复请求

用户登录后,可将用户信息缓存至本地存储(如 sessionStorage)或全局状态(如 Redux),避免每次页面刷新都重新请求资源服务器:

jsx 复制代码
// src/hooks/useUserInfo.js
import { useState, useEffect } from 'react';
import axios from 'axios';

const useUserInfo = () => {
  const [userInfo, setUserInfo] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUserInfo = async () => {
      const accessToken = localStorage.getItem('accessToken');
      const cachedUser = sessionStorage.getItem('userInfo');

      // 优先使用缓存
      if (cachedUser) {
        setUserInfo(JSON.parse(cachedUser));
        setLoading(false);
        return;
      }

      // 缓存不存在时请求接口
      if (accessToken) {
        try {
          const response = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', {
            headers: { Authorization: `Bearer ${accessToken}` },
          });
          setUserInfo(response.data);
          sessionStorage.setItem('userInfo', JSON.stringify(response.data)); // 缓存用户信息
        } catch (error) {
          console.error('获取用户信息失败:', error);
        }
      }
      setLoading(false);
    };

    fetchUserInfo();
  }, []);

  return { userInfo, loading };
};

export default useUserInfo;

6.2.3. 优化令牌刷新策略

前文提到的定时器刷新策略(每 55 分钟刷新一次)可能导致不必要的请求。可改为 请求拦截器动态刷新,仅在令牌即将过期或已过期时发起刷新请求:

jsx 复制代码
// src/utils/axiosInstance.js
import axios from 'axios';
import useTokenRefresh from '../hooks/useTokenRefresh';

const axiosInstance = axios.create();
const { accessToken, refreshAccessToken } = useTokenRefresh();

// 请求拦截器:添加令牌并检查有效期
axiosInstance.interceptors.request.use(
  async (config) => {
    // 检查令牌是否存在且即将过期(假设令牌有效期 1 小时,剩余时间 < 5 分钟时刷新)
    const tokenExpiry = localStorage.getItem('tokenExpiry'); // 存储令牌过期时间(登录时记录)
    const now = Date.now() / 1000;

    if (accessToken && tokenExpiry && tokenExpiry - now < 300) {
      const newToken = await refreshAccessToken();
      config.headers.Authorization = `Bearer ${newToken}`;
    } else if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }

    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器:处理令牌过期错误
axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // 若为 401 错误且未重试过,刷新令牌后重试
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      const newToken = await refreshAccessToken();
      originalRequest.headers.Authorization = `Bearer ${newToken}`;
      return axiosInstance(originalRequest);
    }

    return Promise.reject(error);
  }
);

export default axiosInstance;

7. 总结

react-oauth 插件通过封装 OAuth 2.0 复杂流程,为 React 应用提供了高效、安全的第三方认证解决方案。从基础的 Google 登录集成,到进阶的令牌管理与路由保护,再到多身份提供商扩展与性能优化,它覆盖了前端认证的核心场景。

在实际开发中,需注意以下关键点:

  1. 安全性优先:避免暴露 Client Secret,优先使用 HttpOnly Cookie 存储令牌,开启 CSP 防护;
  2. 适配多场景:根据应用类型(纯前端/前后端分离)选择合适的授权流程(授权码流程为主);
  3. 优化用户体验:通过缓存减少重复请求,令牌刷新时避免打断用户操作;
  4. 关注标准更新:紧跟 OAuth 2.1 等新标准,及时升级插件版本。

本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

往期文章

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax