1. 项目概述
最近我在搭建一个私有化的搜索引擎服务时,选择了SearXNG这个开源的元搜索引擎。SearXNG最大的特点是保护用户隐私,不会记录用户的搜索历史和个人信息。但在实际部署过程中,我发现直接将SearXNG暴露在公网上存在被滥用的风险,于是决定给它加上一层Token认证机制。
这个方案的核心思路是:使用Docker部署SearXNG服务,然后通过一个Python Flask编写的轻量级网关来提供访问控制。网关会拦截所有请求,只有携带有效Token的请求才会被转发到后端的SearXNG服务。这样既保持了SearXNG原有的功能,又增加了安全性。
2. 环境准备与配置
2.1 目录结构规划
合理的目录结构能让后续维护更加方便。我采用了以下结构:
code复制/opt/searxng/
├── searxng/ # SearXNG主配置
│ └── settings.yml # 核心配置文件
├── auth-gateway/ # 认证网关
│ ├── auth_gateway.py # 网关代码
│ └── Dockerfile # 网关构建文件
└── docker-compose.yml # 容器编排文件
创建这个结构的命令很简单:
bash复制mkdir -p /opt/searxng/searxng
mkdir -p /opt/searxng/auth-gateway
cd /opt/searxng
2.2 SearXNG核心配置
SearXNG的配置文件是YAML格式的,对缩进非常敏感。为了避免格式问题导致服务启动失败,我建议使用下面这个经过精简的配置模板:
yaml复制use_default_settings: true
general:
debug: false
instance_name: "SearXNG-Private"
privacypolicy_url: false
donation_url: false
contact_url: false
enable_metrics: true
search:
safe_search: 0
autocomplete: "baidu"
default_lang: "zh-CN"
ban_time_on_fail: 5
max_ban_time_on_fail: 120
formats:
- html
- json
server:
secret_key: "change-this-to-a-random-secret-key"
limiter: false
image_proxy: true
port: 8080
bind_address: "0.0.0.0"
public_instance: false
ui:
static_use_hash: true
default_locale: "zh-Hans-CN"
query_in_title: true
infinite_scroll: true
center_alignment: false
default_theme: "simple"
theme_args:
simple_style: "auto"
redis:
url: "redis://redis:6379/0"
outgoing:
request_timeout: 5.0
max_request_timeout: 15.0
useragent_suffix: ""
pool_connections: 100
pool_maxsize: 20
enable_http2: true
engines:
- name: google
engine: google
shortcut: g
disabled: true
- name: bing
engine: bing
shortcut: b
disabled: false
- name: baidu
engine: baidu
shortcut: bd
disabled: false
注意:secret_key一定要修改为随机字符串,可以使用
openssl rand -hex 32命令生成。
2.3 环境变量配置
我们需要为SearXNG和认证网关生成一些随机密钥:
bash复制echo "SEARXNG_SECRET=$(openssl rand -hex 32)" > .env
echo "VALID_TOKENS=$(openssl rand -hex 32)" >> .env
这样会在当前目录下生成一个.env文件,包含两个随机生成的密钥:
- SEARXNG_SECRET:用于SearXNG的会话加密
- VALID_TOKENS:用于网关认证的访问令牌
3. 认证网关实现
3.1 网关核心逻辑
认证网关的主要功能是:
- 拦截所有请求
- 检查请求中是否包含有效Token
- 将合法请求转发给后端SearXNG服务
- 处理静态资源请求
以下是网关的核心代码(/opt/searxng/auth-gateway/auth_gateway.py):
python复制#!/usr/bin/env python3
"""
SearXNG Token 认证网关
"""
import os
import logging
from flask import Flask, request, Response, jsonify
import requests
app = Flask(__name__, static_folder=None)
# 配置参数
SEARXNG_BACKEND = os.getenv('SEARXNG_BACKEND', 'http://searxng:8080').rstrip('/')
VALID_TOKENS = set(filter(None, os.getenv('VALID_TOKENS', '').split(',')))
WEB_PAGES = {'/', '/preferences', '/stats', '/config', '/search'}
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)
def forward(path):
"""请求转发逻辑"""
target_path = path.lstrip('/')
backend_url = f"{SEARXNG_BACKEND}/{target_path}"
args = request.args.copy()
args.pop('token', None) # 移除Token参数
try:
headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'x-api-token', 'content-length']}
resp = requests.request(
method=request.method,
url=backend_url,
headers=headers,
params=args,
data=request.get_data(),
cookies=request.cookies,
allow_redirects=False,
stream=True,
timeout=30
)
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
headers = [(k, v) for k, v in resp.raw.headers.items() if k.lower() not in excluded_headers]
return Response(resp.iter_content(chunk_size=1024*8), resp.status_code, headers)
except Exception as e:
logger.error(f"转发异常: {str(e)}")
return jsonify({'error': 'Gateway Error'}), 502
def render_login_page():
"""生成登录页面"""
html = """
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>SearXNG Login</title></head>
<body style="display:flex;justify-content:center;align-items:center;height:100vh;background:#f0f2f5;">
<form action="/" method="get" style="background:white;padding:2rem;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<h3>🔐 请输入访问令牌</h3>
<input type="password" name="token" required style="width:100%;padding:10px;margin:10px 0;">
<button type="submit" style="width:100%;padding:10px;background:#007bff;color:white;border:none;cursor:pointer;">进入</button>
</form>
</body>
</html>
"""
return Response(html, mimetype='text/html'), 401
@app.route('/', defaults={'path': ''}, methods=['GET', 'POST'])
@app.route('/<path:path>', methods=['GET', 'POST'])
def handle_all_requests(path):
# 静态资源直接放行
if path.startswith('static/') or path in ['favicon.ico', 'opensearch.xml', 'robots.txt']:
return forward(path)
# 提取Token
token = request.args.get('token') or request.headers.get('X-API-Token') or request.cookies.get('searxng_token')
# 验证Token
if token and token in VALID_TOKENS:
resp = forward(path)
# 登录成功种Cookie (7天有效)
if resp.status_code == 200 and 'text/html' in resp.headers.get('Content-Type', ''):
resp.set_cookie('searxng_token', token, max_age=604800, httponly=True)
return resp
# 验证失败
if path == '' or path in WEB_PAGES:
return render_login_page()
return jsonify({'error': 'Unauthorized'}), 401
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8003, threaded=True)
3.2 网关Dockerfile
为了让网关能够稳定运行,我们使用Gunicorn作为WSGI服务器:
dockerfile复制FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir Flask==3.0.0 requests==2.31.0 gunicorn==21.2.0
COPY auth_gateway.py .
CMD ["gunicorn", "--bind", "0.0.0.0:8003", "--workers", "4", "--threads", "4", "--timeout", "60", "auth_gateway:app"]
4. Docker Compose编排
使用Docker Compose可以方便地管理多个容器服务。以下是完整的docker-compose.yml配置:
yaml复制version: "3.9"
services:
# 认证网关 (对外暴露8003端口)
auth-gateway:
build: ./auth-gateway
container_name: searxng-auth-gateway
restart: unless-stopped
ports:
- "8003:8003"
environment:
- SEARXNG_BACKEND=http://searxng:8080
- VALID_TOKENS=${VALID_TOKENS}
networks:
- searxng-net
depends_on:
- searxng
# SearXNG (仅内部访问)
searxng:
image: searxng/searxng:latest
container_name: searxng
restart: unless-stopped
depends_on:
redis:
condition: service_healthy
expose:
- "8080"
volumes:
- ./searxng:/etc/searxng:rw
environment:
- SEARXNG_BASE_URL=http://localhost:8003/
- SEARXNG_SECRET=${SEARXNG_SECRET}
networks:
- searxng-net
redis:
image: redis:7-alpine
container_name: searxng-redis
command: redis-server --save 30 1 --loglevel warning
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- searxng-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
redis-data:
networks:
searxng-net:
driver: bridge
这个配置定义了三个服务:
- auth-gateway:认证网关,对外暴露8003端口
- searxng:SearXNG搜索引擎服务,仅内部访问
- redis:缓存服务,用于提高搜索性能
5. 部署与问题排查
5.1 启动服务
使用以下命令启动所有服务:
bash复制cd /opt/searxng
docker compose up -d --build
注意:首次启动或修改网关代码后,必须加上--build参数重新构建镜像。
5.2 常见问题解决
5.2.1 Internal Server Error (500)
现象:访问服务时返回500错误,日志显示YAML解析错误。
原因:settings.yml文件格式不正确,可能是缩进问题或混入了非法字符。
解决方案:
- 使用本文提供的纯净版配置
- 检查YAML文件的缩进是否正确
- 确保没有混入特殊字符(特别是从网页复制时)
5.2.2 页面样式丢失 (404)
现象:页面可以打开,但CSS和JS文件加载失败。
原因:Flask默认会拦截/static路由并在容器本地查找文件。
解决方案:
- 在Flask初始化时设置static_folder=None
- 手动处理静态资源请求的转发
- 确保网关代码中的静态资源白名单逻辑正确
5.2.3 Too Many Requests (429)
现象:频繁搜索后返回429错误。
原因:SearXNG默认开启了限流功能,所有请求经过网关后来源IP相同,容易触发限流。
解决方案:
- 在settings.yml中设置server.limiter: false
- 在网关层面实现限流逻辑(如果需要)
6. 使用与维护
部署成功后,访问http://服务器IP:8003会看到一个简单的登录页面,输入.env文件中的VALID_TOKENS值即可进入搜索界面。
为了提高安全性,建议:
- 定期轮换VALID_TOKENS
- 限制访问IP(可以在网关代码中添加IP白名单)
- 监控服务运行状态
对于团队使用,可以在网关代码中扩展支持多Token管理,或者集成现有的认证系统(如LDAP、OAuth等)。