背景
前段时间在折腾小程序,给小程序加了一些新的功能。
(不得不说,cursor是真的好用,给我改的页面是真的很符合我的审美~)
因为加了一些功能,其中有部分接口我想给限制一下访问ratelimit,比如“限制单个用户IP每分钟访问特定接口10次”。这对于防止滥用、保障服务稳定性至关重要。
环境介绍
因为我用的是fastapi做的后端服务,所以我的需求就是找一个fastapi可以用的ratelimit包。
我作为资深调包侠,在万能的github上找到了一个包 https://github.com/laurentS/slowapi
用起来很简单,一气呵成。
from slowapi import Limiter
from slowapi.util import get_ipaddr
limiter = Limiter(key_func=get_ipaddr, headers_enabled=True)
@router.get("/test/", response_model=BaseResponse)
@limiter.limit("10/minute")
async def test(request: Request, response: Response, poetry_id: str):
"""
测试
:return:
"""
return {"test": 123}
slowapi.util
提供了两个主要的方法来获取请求的 IP 地址:
get_ipaddr(request: Request) -> str
:依赖代理头(如
X-Forwarded-For
)。当你的应用部署在反向代理(如 Nginx)之后时,这是获取真实用户 IP 的推荐方式。
注意: 需要正确配置反向代理以传递正确的头部信息,否则可能获取到代理服务器的 IP 或伪造的 IP。
get_remote_address(request: Request) -> str
:不直接依赖代理头,而是获取直接连接到应用服务器的客户端 IP。
在 Gunicorn + Uvicorn 正确配置(例如使用了
ProxyHeadersMiddleware
或 Gunicorn 的forwarded_allow_ips
)的情况下,它也可以返回真实用户 IP。如果应用直接暴露给公网,或者代理服务器配置不当,此方法可能返回的是代理服务器的 IP。
from starlette.requests import Request
def get_ipaddr(request: Request) -> str:
"""
Returns the ip address for the current request (or 127.0.0.1 if none found)
based on the X-Forwarded-For headers.
Note that a more robust method for determining IP address of the client is
provided by uvicorn's ProxyHeadersMiddleware.
"""
if "X_FORWARDED_FOR" in request.headers:
return request.headers["X_FORWARDED_FOR"]
else:
if not request.client or not request.client.host:
return "127.0.0.1"
return request.client.host
def get_remote_address(request: Request) -> str:
"""
Returns the ip address for the current request (or 127.0.0.1 if none found)
"""
if not request.client or not request.client.host:
return "127.0.0.1"
return request.client.host
这里我直接用了get_ipaddr,下面是一个对比表格:
我选择了 get_ipaddr
,因为我的部署架构中包含了 Nginx 反向代理。
因为我启用了 headers_enabled=True
,HTTP 响应头中会包含 X-RateLimit-Limit
, X-RateLimit-Remaining
, X-RateLimit-Reset
等信息,非常直观。
部署
在服务器上部署最新的代码之后,不出意外肯定会出意外~
这里说说我的后端服务的架构。很简单,很常规的用了nginx搞了一个反向代理。
部署新代码后,问题出现了:FastAPI 通过 get_ipaddr
获取到的 IP 地址总是 Nginx Proxy Manager 所在 Docker 容器的内部 IP,而不是真实用户的 IP! 这导致限流策略对所有用户都使用了同一个 IP,失去了应有的效果。
这个问题在使用了反向代理的架构中非常常见。原因是,对于 FastAPI 应用来说,直接连接它的是 Nginx,所以 request.client.host
默认是 Nginx 的 IP。而 X-Forwarded-For
头部虽然被 Nginx 设置了,但 Gunicorn/Uvicorn 需要被告知信任这个头部。
这里需要修改一下gunicorn的启动配置,增加forwarded_allow_ips配置,这里本来最好是设置一个指定的本地ip,不过因为我这个fastapi服务没有开公网端口,所以我这边是直接用的*。
forwarded_allow_ips = "*"
worker_class = "uvicorn.workers.UvicornWorker"
gunicorn forwarded_allow_ips 配置
, gunicorn uvicorn X-Forwarded-For
, fastapi gunicorn 获取真实ip
正常来说加上这个配置之后,fastapi服务还是不可以获取到用户的真实IP。
我们还需要在nginx的配置文件增加下面的配置:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
(不过我用的是nginx proxy manager,里面是自带了这些配置~)
nginx proxy manager默认的配置:
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
# Proxy!
include conf.d/include/proxy.conf;
}
查看conf.d/include/proxy.conf
关键点: 即使 Nginx 设置了 X-Forwarded-For
,如果 Gunicorn/Uvicorn (FastAPI 的运行环境) 不信任这个头部(即没有配置 forwarded_allow_ips
或类似机制),get_ipaddr
(或其他依赖此头部的方法) 仍然可能无法获取到真实的用户 IP。两端都需要正确配置。
nginx proxy manager X-Forwarded-For 设置
, nginx proxy_set_header 获取真实ip
, fastapi nginx proxy manager ip 地址问题
总结和最佳实践
为 FastAPI 应用(尤其是在小程序后端这样的场景)实现基于用户 IP 的接口限流,需要:
选择合适的限流库:
slowapi
是一个优秀的选择。正确获取用户 IP: 使用
get_ipaddr
并确保反向代理和应用服务器配置正确。配置 Gunicorn: 设置
forwarded_allow_ips
以信任来自 Nginx 的X-Forwarded-For
头部。为了安全,应指定具体的代理服务器 IP 而不是*
。配置 Nginx/Nginx Proxy Manager: 确保
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
和proxy_set_header X-Real-IP $remote_addr;
被正确设置。
通过以上步骤,你的 FastAPI 应用就能准确地获取到真实用户 IP,并有效地实施接口限流策略了。这不仅提升了应用的安全性,也保证了服务的公平性和稳定性。
FastAPI接口限流教程
, 小程序后端API防刷
, 解决FastAPI获取不到真实IP
, Python FastAPI生产部署IP问题
, SlowAPI与Nginx集成指南
, UvicornWorker真实IP获取方法