一、背景与痛点
1.1 原始部署方式(不安全)
我有三个 Flask 项目,分别运行在同一台阿里云服务器的不同端口上:
-
项目 A:
http://your-server-ip:5001/ -
项目 B:
http://your-server-ip:5003/ -
项目 C:
http://your-server-ip:5005/(含/admin后台)
用户直接通过 HTTP + IP + 端口 访问,存在以下问题:
-
明文传输,密码、卡密等敏感信息易被窃听
-
无域名,IP 地址变化后需要通知所有用户
-
端口暴露,容易被扫描攻击
-
浏览器不安全提示,影响用户体验
1.2 目标
-
申请一个单域名 SSL 证书 (如
example.com),仅支持该域名,不支持泛域名 -
让所有 Flask 项目通过 HTTPS + 域名 + 子路径 访问:
-
https://example.com/5001/→ 原 5001 端口项目 -
https://example.com/5003/→ 原 5003 端口项目 -
https://example.com/5005/→ 原 5005 端口项目(含/admin)
-
-
保留原有 HTTP 访问方式不变(供旧客户端过渡,但最终要废弃)
1.3 技术挑战
-
Flask 默认生成的 URL(重定向、静态文件、表单 action)不包含子路径前缀
-
AJAX 请求路径硬编码,导致登录后跳转 404
-
静态文件 404
-
会话失效后 AJAX 请求不跳转登录页
-
Nginx 代理时前缀处理混乱
二、最终架构图
客户端 HTTPS 请求
↓
Nginx (443端口,SSL终结)
├─ /5001/ → 代理到 127.0.0.1:5001 (保留前缀)
├─ /5003/ → 代理到 127.0.0.1:5003
└─ /5005/ → 代理到 127.0.0.1:5005
↓
每个 Flask 应用使用蓝图设置 url_prefix='/5005'
↓
Flask 内部路由正常处理 /admin/login 等
↓
模板中使用 url_for 自动生成带前缀的链接
三、Nginx 配置(核心:proxy_pass 末尾无斜杠)
以 5005 端口项目为例,其他项目类似。
nginx
server {
listen 443 ssl;
server_name example.com; # 替换为你的域名
ssl_certificate /path/to/your/cert.pem;
ssl_certificate_key /path/to/your/cert.key;
# ... 其他 SSL 参数(可参考常规配置)...
# 静态文件直接由 Nginx 托管(可选)
location /5005/static/ {
alias /home/your_user/your_project/static/; # 替换为实际路径
}
# 反向代理到 Flask
location /5005/ {
proxy_pass http://127.0.0.1:5005; # 注意:末尾没有斜杠!
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
关键点 :proxy_pass 末尾不加 /,这样 Nginx 会将完整的 /5005/admin/login 原样传给 Flask。如果加了 /,会去掉前缀导致路由 404。
四、Flask 代码改造(蓝图 + 统一前缀)
4.1 安装依赖(无额外库,Flask 自带蓝图)
4.2 修改主文件 main.py
python
from flask import Flask, Blueprint, render_template, redirect, url_for, session, jsonify, request
from functools import wraps
app = Flask(__name__)
app.secret_key = 'your-secret-key-here' # 替换为随机字符串
# 创建蓝图,设置 URL 前缀
bp = Blueprint('main', __name__, url_prefix='/5005')
# ------------------ 登录验证装饰器(支持 AJAX 401)------------------
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('logged_in'):
# 判断是否为 AJAX 请求
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'redirect': url_for('main.login_page')}), 401
return redirect(url_for('main.login_page'))
return f(*args, **kwargs)
return decorated_function
# ------------------ 路由(原有 @app.route 全部换成 @bp.route)------------------
@bp.route('/admin/login', methods=['GET'])
def login_page():
return render_template('login.html')
@bp.route('/admin/login', methods=['POST'])
def login():
# 验证逻辑(示例)
username = request.form.get('username')
password = request.form.get('password')
# ... 数据库验证 ...
session['logged_in'] = True
return redirect(url_for('main.admin_page')) # 注意:必须加 main. 前缀
@bp.route('/admin')
@login_required
def admin_page():
return render_template('admin.html')
# ... 其他路由(/admin/keys、/api/verify 等)
# 注册蓝图
app.register_blueprint(bp)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5005) # 监听本地,仅允许 Nginx 代理
改动说明:
-
将
@app.route全部替换为@bp.route -
所有
redirect(url_for('xxx'))改为redirect(url_for('main.xxx')) -
添加 AJAX 友好的
login_required装饰器
4.3 模板修改(admin.html)
全局 AJAX 处理 401
javascript
$.ajaxSetup({
statusCode: {
401: function(xhr) {
var response = xhr.responseJSON;
if (response && response.redirect) {
window.location.href = response.redirect;
} else {
window.location.href = '/5005/admin/login';
}
}
}
});
所有 AJAX 请求使用绝对路径(带 /5005)
javascript
// 删除卡密
function deleteKey(id) {
$.post('/5005/admin/keys/' + id + '/delete', function(res) {
if (res.success) loadKeys();
});
}
链接和表单使用 url_for
html
<a href="{{ url_for('main.change_password') }}">修改密码</a>
<form action="{{ url_for('main.login') }}" method="post">...</form>
静态文件使用 /5005/static/ 绝对路径或 CDN
html
<link href="/5005/static/css/bootstrap.min.css" rel="stylesheet">
4.4 静态文件权限
bash
sudo chmod -R 755 /home/your_user/your_project/static
sudo chown -R www-data:www-data /home/your_user/your_project/static
五、迁移步骤总结
-
申请 SSL 证书 (单域名,如
example.com) -
修改 Nginx 配置 :添加
location /5005/代理(无斜杠)和静态文件 location -
修改 Flask 代码 :引入蓝图,设置
url_prefix,修改装饰器支持 AJAX 401 -
修改模板 :全局 AJAX 设置,将硬编码路径改为
/5005/xxx或url_for -
重启服务 :
sudo systemctl reload nginx,重启 Flask(使用 gunicorn) -
测试 :访问
https://example.com/5005/admin/login,登录,操作增删改查
六、常见错误与解决
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
访问 /5005/admin 返回 404 |
Nginx proxy_pass 末尾加了 / |
去掉 / |
登录后跳转到 /admin(缺少前缀) |
redirect(url_for('admin_page')) 未加蓝图名 |
改为 url_for('main.admin_page') |
| 静态文件 404 | alias 路径错误或权限不足 |
检查路径,执行 chmod |
| AJAX 请求返回 HTML 登录页 | 后端对 AJAX 返回 302,前端无法处理 | 后端返回 401 + JSON,前端全局处理 |
| 删除/编辑提示"记录不存在" | 前端 URL 拼接错误(双斜杠或缺少 id) | 使用硬编码 /5005/admin/keys/${id}/delete |
七、性能优化建议
-
静态文件由 Nginx 直接托管,不要经过 Flask
-
使用
gunicorn替代flask run,设置worker数量为 CPU 核心数×2+1 -
为 Nginx 开启
gzip压缩(配置文件已包含) -
配置
proxy_buffering和proxy_cache提升响应速度
八、结语
通过本方案,您的 Flask 应用从 不安全的 HTTP + IP 端口 平滑迁移到了 HTTPS + 域名子路径,既保证了通信安全,又保留了多项目共存的灵活性。整个改造过程仅需修改 Nginx 配置和少量 Flask 代码,无需重构业务逻辑。
下一步:可以进一步使用 Docker 容器化部署,或者接入 CI/CD 自动更新 SSL 证书。
如果本文对您有帮助,欢迎点赞、收藏、转发。如有问题,请在评论区留言。