You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

React+FastAPI无访客认证场景下的API访问控制方案咨询

React+FastAPI无访客认证场景下的API访问控制方案咨询

这个问题我之前帮朋友搭开放访客平台的时候刚好踩过坑——开放给未登录用户的API确实容易被恶意爬取或者滥用,但又不能加登录门槛影响用户体验,得在「开放易用」和「基础安全」之间找平衡。给你几个落地性强的方案,你可以根据自己的业务风险等级来选:

一、基础防护:CORS配置 + Referer头校验(适合低风险公开场景)

这是最容易上手的方案,先把最基础的“非前端域名的请求”挡在门外:

  1. 严格配置CORS
    在FastAPI里只允许你的React生产域名跨域请求,别图省事用通配符*
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 只添加你的React域名,开发环境的本地地址可以暂时保留
origins = [
    "https://your-react-app.com",
    "http://localhost:3000",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

这样浏览器层面就会拦截其他域名发起的跨域请求,能挡掉大部分随便用Postman直接测的小白或者简易自动爬虫工具。

  1. 补充Referer头校验
    虽然Referer可以被伪造,但能过滤掉很多没刻意做伪装的请求。你可以写一个FastAPI依赖项来统一校验:
from fastapi import Header, HTTPException, Depends

async def validate_referer(referer: str = Header(None)):
    allowed_domains = ["your-react-app.com", "localhost:3000"]
    if not referer:
        raise HTTPException(status_code=403, detail="无效的请求来源")
    # 检查Referer是否包含允许的前端域名
    if not any(domain in referer for domain in allowed_domains):
        raise HTTPException(status_code=403, detail="禁止的请求来源")
    return referer

# 在需要保护的路由上挂载这个依赖
@app.get("/public-list")
async def get_public_data(referer = Depends(validate_referer)):
    return {"data": "公开内容列表"}

这个方案优点是零额外开发成本,缺点是防不住刻意伪造Referer的请求,适合只是展示静态公开内容、没有资源消耗的场景。

二、进阶防护:临时访客令牌(无需用户登录)

给每个首次访问的前端分配一个临时访客令牌,用来做请求身份标识,同时配合限流,能有效降低单IP滥用的风险:

  1. 前端初始化时获取令牌
    React项目在根组件挂载时,自动向后端请求令牌并存在localStorage里:
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    const getVisitorToken = async () => {
      const storedToken = localStorage.getItem('visitor_token');
      if (!storedToken) {
        const res = await fetch('https://your-backend.com/get-visitor-token');
        const data = await res.json();
        localStorage.setItem('visitor_token', data.token);
      }
    };
    getVisitorToken();
  }, []);

  return (
    <div className="App">
      {/* 你的页面内容 */}
    </div>
  );
}

export default App;
  1. 后端生成并校验令牌
    用Redis存储令牌(设置过期时间,比如24小时),同时搭配限流工具控制请求频率:
import redis
import secrets
from fastapi import Header, HTTPException, Depends
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

# 初始化Redis和限流工具
r = redis.Redis(host='localhost', port=6379, db=0)
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# 生成访客令牌的接口
@app.get("/get-visitor-token")
@limiter.limit("10/minute")  # 限制每个IP每分钟最多拿10个令牌
async def get_visitor_token():
    token = secrets.token_hex(16)
    # 设置令牌24小时后过期
    r.setex(f"visitor:{token}", 86400, "valid")
    return {"token": token}

# 令牌校验的依赖项
async def validate_visitor_token(x_visitor_token: str = Header(None)):
    if not x_visitor_token or not r.exists(f"visitor:{x_visitor_token}"):
        raise HTTPException(status_code=403, detail="无效的访客令牌")
    return x_visitor_token

# 受保护的接口示例
@app.get("/protected-public-data")
@limiter.limit("50/hour")  # 限制每个令牌每小时最多50次请求
async def get_protected_data(token = Depends(validate_visitor_token)):
    return {"data": "带基础防护的公开内容"}

这个方案能有效防止单IP的批量滥用,令牌过期机制也能避免长期占用资源,缺点是如果令牌被泄露,别人还是能模拟请求,但已经能挡掉大部分自动爬虫了。

三、中高风险场景:请求签名机制

如果你的API涉及到资源消耗(比如生成动态内容、复杂数据库查询),可以加请求签名,让只有知道约定规则的前端才能生成有效请求:

  1. 前后端约定签名规则
    比如用时间戳 + 请求参数 + 共享密钥生成HMAC-SHA256签名,前端请求时在Header里带上X-TimestampX-Signature
// 前端生成签名的工具函数(注意:密钥要存在环境变量里,绝对不能硬编码在代码中!)
import crypto from 'crypto-js';

const generateSignature = (params, timestamp) => {
  const secret = process.env.REACT_APP_API_SECRET;
  // 固定拼接规则,后端要和前端完全一致
  const signStr = `${timestamp}&${JSON.stringify(params)}`;
  return crypto.HmacSHA256(signStr, secret).toString(crypto.enc.Hex);
};

// 带签名的请求示例
async function fetchComplexData(params) {
  const timestamp = Date.now().toString();
  const signature = generateSignature(params, timestamp);
  const token = localStorage.getItem('visitor_token');
  
  const res = await fetch('https://your-backend.com/complex-data', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Visitor-Token': token,
      'X-Timestamp': timestamp,
      'X-Signature': signature
    },
    body: JSON.stringify(params)
  });
  return res.json();
}
  1. 后端校验签名
import hmac
import hashlib
import json
from fastapi import Header, HTTPException, Body, Depends
from datetime import datetime

# 和前端一致的密钥,存在后端环境变量里
SECRET_KEY = b"your-shared-secret"

async def validate_signature(
    x_timestamp: str = Header(None),
    x_signature: str = Header(None),
    params: dict = Body(None)
):
    if not x_timestamp or not x_signature:
        raise HTTPException(status_code=403, detail="缺少签名参数")
    
    # 校验时间戳,防止重放攻击(只允许5分钟内的请求)
    current_ts = int(datetime.now().timestamp() * 1000)
    if abs(int(x_timestamp) - current_ts) > 300 * 1000:
        raise HTTPException(status_code=403, detail="请求已过期")
    
    # 生成对比签名,注意参数排序要和前端一致
    sign_str = f"{x_timestamp}&{json.dumps(params, sort_keys=True)}".encode('utf-8')
    expected_signature = hmac.new(SECRET_KEY, sign_str, hashlib.sha256).hexdigest()
    
    # 用安全的对比方法,避免时序攻击
    if not hmac.compare_digest(x_signature, expected_signature):
        raise HTTPException(status_code=403, detail="无效的签名")

这个方案能防大部分伪造请求,因为不知道共享密钥就生成不了正确的签名。注意前端的密钥要通过构建工具注入环境变量,不要直接写在代码里,同时可以配合代码混淆降低泄露风险。

四、兜底方案:速率限制(必须加!)

不管用上面哪种方案,都要给API加速率限制,就算被突破了校验,也不会把后端打崩。FastAPI用slowapi库很容易实现,前面的例子里已经用到了,核心是根据接口的资源消耗情况,设置合理的请求频率,比如普通接口每分钟100次,复杂接口每小时50次。

最后总结一下

其实没有100%安全的开放API,核心是根据你的业务风险来组合方案:

  • 如果只是展示静态公开内容:CORS配置 + Referer校验 + 基础限流就够了
  • 如果有轻度资源消耗的接口:临时访客令牌 + 限流
  • 如果是中高风险的核心接口:请求签名 + 临时令牌 + 严格限流
  • 如果怕机器人批量请求:可以加轻量人机验证(比如滑块验证码),访客第一次触发核心操作时验证,通过后再发有效令牌,这样能挡掉大部分自动化工具。

另外一定要记住:前端的任何校验都只能做“友好提示”,后端必须对所有请求参数做严格校验,不要信任前端传过来的任何数据!

火山引擎 最新活动