#!/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/', 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/') @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/') @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('/') 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)