React 应用的“最安全”登录验证方案

96 阅读3分钟

下面是一套基于浏览器环境下 React 应用的“最安全”登录验证方案,结合了 HttpOnlySecureSameSiteCSRF 防护短期 Access Token + 长期 Refresh TokenAxios 拦截器React Context + Hook 等最佳实践。


一、总体思路

  1. 后端

    • 登录成功时,在响应头通过 Set-Cookie 下发两个 Cookie:

      • refreshToken:HttpOnly;Secure;SameSite=Strict;长期有效(如 7 天)。
      • csrfToken:非 HttpOnly;Secure;SameSite=Strict;与 refreshToken 同期有效,用于 CSRF 验证。
    • 客户端每次请求刷新 Access Token(或执行敏感操作)时,需带上 csrfToken 在请求头里;后端比对。

    • Access Token 不存 Cookie,而是返回在响应体里,前端放 内存 中,短期有效(例如 5–15 分钟)。

  2. 前端

    • Access Token:只存在 React 内存中(State/Context),避免 XSS 后被长期窃取。
    • Refresh Token:由浏览器自动管理的 HttpOnly Cookie 存储,前端不可读写。
    • CSRF Token:同样以 Cookie 发下,前端通过 document.cookie 读取,并在每次请求时放到自定义请求头(如 X-CSRF-Token)里。
    • Axios 拦截器:自动给请求加上 Access Token(Authorization);遇到 401 自动触发静默刷新(调用 /auth/refresh,携带 refreshToken Cookie + csrfToken 头),拿到新 Access Token 并重试。
    • React Context + Hook:全局管理用户状态、登录登出、刷新逻辑,组件只需调用 useAuth()

二、Cookie 设置(后端示例)

HTTP/1.1 200 OK
Set-Cookie: refreshToken=<long‑uuid‑or‑jwt>; Path=/; HttpOnly; Secure; SameSite=Strict; Max‑Age=604800
Set-Cookie: csrfToken=<random‑string>; Path=/; Secure; SameSite=Strict; Max‑Age=604800
Content-Type: application/json

{ "accessToken": "<short‑lived‑jwt>" }
  • refreshToken:浏览器自动发给后端,用于静默刷新 Access Token。
  • csrfToken:前端可读,将其加入请求头抵御 CSRF。

三、Axios 实现拦截器(前端部分)

// api.js
import axios from 'axios';
import { getCsrfToken } from './authStorage'; // 从 Cookie 中读

const api = axios.create({
  baseURL: '/api',
  withCredentials: true, // 允许发送 refreshToken & csrfToken Cookie
});

// 请求拦截:带上 Access Token + CSRF Token
api.interceptors.request.use(config => {
  const accessToken = window.__ACCESS_TOKEN__;   // 存在内存中
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  const csrf = getCsrfToken();
  if (csrf) {
    config.headers['X-CSRF-Token'] = csrf;
  }
  return config;
});

// 响应拦截:Access Token 失效时静默刷新
let isRefreshing = false;
let subscribers = [];

function onRefreshed(token) {
  subscribers.forEach(cb => cb(token));
  subscribers = [];
}

function addSubscriber(cb) {
  subscribers.push(cb);
}

api.interceptors.response.use(
  res => res,
  error => {
    const { config, response } = error;
    if (response?.status === 401 && !config._retry) {
      config._retry = true;
      if (!isRefreshing) {
        isRefreshing = true;
        return axios
          .post('/api/auth/refresh', {}, { withCredentials: true, headers: { 'X-CSRF-Token': getCsrfToken() } })
          .then(res => {
            const newToken = res.data.accessToken;
            window.__ACCESS_TOKEN__ = newToken;
            onRefreshed(newToken);
            return api(config);
          })
          .finally(() => {
            isRefreshing = false;
          });
      }
      // 阻塞队列中其他请求,等待刷新完成
      return new Promise(resolve => {
        addSubscriber(token => {
          config.headers.Authorization = `Bearer ${token}`;
          resolve(api(config));
        });
      });
    }
    return Promise.reject(error);
  }
);

export default api;

四、Auth Context + Hook

// AuthContext.js
import React, { createContext, useState, useEffect, useContext } from 'react';
import api from './api';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // 应用启动,尝试用 refreshToken 换取 Access Token & 用户信息
  useEffect(() => {
    api.post('/auth/refresh')
      .then(res => {
        window.__ACCESS_TOKEN__ = res.data.accessToken;
        return api.get('/auth/me');
      })
      .then(res => setUser(res.data))
      .catch(() => setUser(null))
      .finally(() => setLoading(false));
  }, []);

  const login = async creds => {
    const res = await api.post('/auth/login', creds);
    window.__ACCESS_TOKEN__ = res.data.accessToken;
    const me = await api.get('/auth/me');
    setUser(me.data);
  };

  const logout = async () => {
    await api.post('/auth/logout'); // 服务端清除 refreshToken Cookie
    window.__ACCESS_TOKEN__ = null;
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

根组件包裹:

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { AuthProvider } from './AuthContext';

ReactDOM.render(
  <AuthProvider><App /></AuthProvider>,
  document.getElementById('root')
);

组件中使用:

function Dashboard() {
  const { user, logout } = useAuth();
  if (!user) return <p>Loading...</p>;
  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <button onClick={logout}>登出</button>
    </div>
  );
}

五、整合要点

关 键 点说明
Access Token 存储仅内存(window.__ACCESS_TOKEN__ 或 React State),避免 XSS 持久窃取。
Refresh Token 存储HttpOnly;Secure;SameSite=Strict;由浏览器自动带给后端。
CSRF 防护同期下发 csrfToken Cookie(非 HttpOnly),请求头携带并校验。
SameSiteStrict:refreshToken、csrfToken;防止跨站用刷新接口拿新 token。
Secure仅 HTTPS,防止中间人劫持。
HttpOnlyrefreshToken;防止 XSS 读取。
拦截器Axios 统一注入 Access Token + CSRF Token,自动刷新。
Context + Hook全局管理登录态,解耦组件逻辑。

通过以上方案,已将 XSSCSRF中间人攻击Token 滥用 等常见风险降至最低,并保持了良好的开发体验和用户体验。