下面是一套基于浏览器环境下 React 应用的“最安全”登录验证方案,结合了 HttpOnly、Secure、SameSite、CSRF 防护、短期 Access Token + 长期 Refresh Token、Axios 拦截器、React Context + Hook 等最佳实践。
一、总体思路
-
后端
-
登录成功时,在响应头通过
Set-Cookie下发两个 Cookie:refreshToken:HttpOnly;Secure;SameSite=Strict;长期有效(如 7 天)。csrfToken:非 HttpOnly;Secure;SameSite=Strict;与 refreshToken 同期有效,用于 CSRF 验证。
-
客户端每次请求刷新 Access Token(或执行敏感操作)时,需带上
csrfToken在请求头里;后端比对。 -
Access Token 不存 Cookie,而是返回在响应体里,前端放 内存 中,短期有效(例如 5–15 分钟)。
-
-
前端
- 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),请求头携带并校验。 |
| SameSite | Strict:refreshToken、csrfToken;防止跨站用刷新接口拿新 token。 |
| Secure | 仅 HTTPS,防止中间人劫持。 |
| HttpOnly | refreshToken;防止 XSS 读取。 |
| 拦截器 | Axios 统一注入 Access Token + CSRF Token,自动刷新。 |
| Context + Hook | 全局管理登录态,解耦组件逻辑。 |
通过以上方案,已将 XSS、CSRF、中间人攻击、Token 滥用 等常见风险降至最低,并保持了良好的开发体验和用户体验。