This commit is contained in:
Ubuntu
2026-07-03 16:58:28 +08:00
commit 32b43f85bd
6240 changed files with 1987678 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
相册查看器后端(带登录保护)—— 双模式认证(本地 Session + SSO Cookie)
"""
from flask import Flask, jsonify, send_from_directory, request, make_response
from flask_cors import CORS
from flask_login import LoginManager, login_required, current_user
from pathlib import Path
from datetime import timedelta
import os
import sys
# 添加当前目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from models import db, User, init_default_users
from auth import auth_bp, api_login_required
app = Flask(__name__)
app.config['SECRET_KEY'] = 'album-viewer-secret-key-change-in-production'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///album_auth.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Session cookie 配置(支持跨域)
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # HTTP环境用Lax
app.config['SESSION_COOKIE_SECURE'] = False # HTTP环境
app.config['SESSION_COOKIE_DOMAIN'] = None
# 初始化扩展
db.init_app(app)
# CORS 精确白名单(不能用 *,因为需要 credentials)
CORS(app, supports_credentials=True, origins=[
"http://129.211.173.87:8892",
"http://129.211.173.87:9000",
"http://localhost:8892",
])
# Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# 注册蓝图
app.register_blueprint(auth_bp)
# 配置
ALBUMS_ROOT = Path('/home/ubuntu/albums')
THUMB_WIDTH = 320
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}
# SSO Cookie 配置(与 SSO 中心一致,用于清除 Cookie)
SSO_COOKIE_DOMAIN = "129.211.173.87"
def is_image(filename):
"""检查是否为图片文件"""
return Path(filename).suffix.lower() in IMAGE_EXTENSIONS
def get_directory_contents(path):
"""获取目录内容(相册和图片)"""
items = {'albums': [], 'images': []}
if not path.exists():
return items
for item in sorted(path.iterdir(), key=lambda x: x.name.lower()):
if item.is_dir() and item.name != 'thumbnails':
has_content = False
for child in item.iterdir():
if child.is_file() and is_image(child.name):
has_content = True
break
if child.is_dir() and child.name != 'thumbnails':
has_content = True
break
cover = None
thumb_dir = item / 'thumbnails'
if thumb_dir.exists():
for thumb in sorted(thumb_dir.iterdir()):
if is_image(thumb.name):
cover = f"/thumbs/{item.relative_to(ALBUMS_ROOT)}/thumbnails/{thumb.name}"
break
items['albums'].append({
'name': item.name,
'path': str(item.relative_to(ALBUMS_ROOT)),
'cover': cover,
'type': 'album'
})
elif item.is_file() and is_image(item.name):
rel_path = item.relative_to(ALBUMS_ROOT)
thumb_path = item.parent / 'thumbnails' / item.name
has_thumbnail = thumb_path.exists()
items['images'].append({
'name': item.name,
'path': str(rel_path),
'thumbnail': f"/thumbs/{rel_path.parent}/thumbnails/{item.name}" if has_thumbnail else None,
'size': item.stat().st_size,
'type': 'image'
})
return items
# ========== 受保护路由(需要登录)==========
@app.route('/api/albums', strict_slashes=False)
@api_login_required
def list_albums():
"""列出相册 - 需要登录"""
contents = get_directory_contents(ALBUMS_ROOT)
return jsonify({
'path': '',
'name': '相册首页',
'albums': contents['albums'],
'images': contents['images']
})
@app.route('/api/albums/<path:album_path>', strict_slashes=False)
@api_login_required
def get_album(album_path):
"""获取相册内容 - 需要登录"""
album_full_path = ALBUMS_ROOT / album_path
try:
album_full_path.relative_to(ALBUMS_ROOT)
except ValueError:
return jsonify({'error': 'Invalid path'}), 403
if not album_full_path.exists():
return jsonify({'error': 'Album not found'}), 404
contents = get_directory_contents(album_full_path)
return jsonify({
'path': album_path,
'name': album_full_path.name,
'albums': contents['albums'],
'images': contents['images']
})
@app.route('/api/image/<path:image_path>')
@api_login_required
def get_image(image_path):
"""获取原图 - 需要登录 ⭐ 关键保护 """
full_path = ALBUMS_ROOT / image_path
try:
full_path.relative_to(ALBUMS_ROOT)
except ValueError:
return jsonify({'error': 'Invalid path'}), 403
if not full_path.exists() or not is_image(full_path.name):
return jsonify({'error': 'Image not found'}), 404
return send_from_directory(full_path.parent, full_path.name)
@app.route('/thumbs/<path:thumb_path>')
@api_login_required
def serve_thumbnail(thumb_path):
"""服务缩略图 - 需要登录 ⭐ 关键保护 """
full_path = ALBUMS_ROOT / thumb_path
try:
full_path.relative_to(ALBUMS_ROOT)
except ValueError:
return jsonify({'error': 'Invalid path'}), 403
if not full_path.exists():
return jsonify({'error': 'Thumbnail not found'}), 404
return send_from_directory(full_path.parent, full_path.name)
# ========== SSO 退出路由 ==========
@app.route('/api/auth/sso-logout', methods=['POST'])
def sso_logout():
"""退出登录 —— 同时清除本地 Session 和 SSO Cookie"""
from flask_login import logout_user
if current_user.is_authenticated:
logout_user()
# 清除 SSO Cookie
resp = make_response(jsonify({'success': True, 'message': '已登出'}))
resp.delete_cookie('access_token', domain=SSO_COOKIE_DOMAIN, path='/')
return resp
# ========== 前端路由 ==========
@app.route('/')
def index():
"""首页 - 公开,但前端会检查登录状态"""
return send_from_directory('../frontend', 'index.html')
@app.route('/<path:path>')
def static_files(path):
"""静态文件 - 公开(CSS/JS/HTML)"""
# 排除 API 路径
if path.startswith('api/'):
return jsonify({'error': 'Not found'}), 404
return send_from_directory('../frontend', path)
# ========== 初始化 ==========
with app.app_context():
db.create_all()
init_default_users()
print("✅ 数据库初始化完成")
if __name__ == '__main__':
print("🖼️ 相册查看器(带登录保护)启动...")
print(f" 相册根目录: {ALBUMS_ROOT}")
print(f" 默认账号: admin / abcd1234")
print(f" 访问: http://localhost:8892")
app.run(host='0.0.0.0', port=8892, debug=False)