Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

232 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)