feat: 添加用户认证功能 (JWT)

This commit is contained in:
小龙 2026-04-10 06:36:41 +00:00
parent 77bdfbc5da
commit bade5e7b26
3 changed files with 220 additions and 9 deletions

Binary file not shown.

View File

@ -1,11 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
导航网站后端 API (Flask版本) 导航网站后端 API (Flask版本)
包含用户认证功能
""" """
import os import os
import sys import sys
import jwt
import hashlib
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime, timedelta
from functools import wraps
# 添加当前目录到 path # 添加当前目录到 path
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
@ -22,6 +26,11 @@ DB_USER = os.environ.get('DB_USER', 'dbuser')
DB_PASS = os.environ.get('DB_PASS', 'Tata@1234') DB_PASS = os.environ.get('DB_PASS', 'Tata@1234')
DB_NAME = os.environ.get('DB_NAME', 'favorites_nav') DB_NAME = os.environ.get('DB_NAME', 'favorites_nav')
# JWT 配置
JWT_SECRET = os.environ.get('JWT_SECRET', 'easynavi-secret-key-change-in-production')
JWT_ALGORITHM = 'HS256'
JWT_EXPIRATION_HOURS = 24 * 30 # 30天过期
app = Flask(__name__) app = Flask(__name__)
# 简单 CORS 中间件 # 简单 CORS 中间件
@ -29,7 +38,7 @@ app = Flask(__name__)
def add_cors_headers(response): def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type' response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response return response
def get_db_connection(): def get_db_connection():
@ -44,6 +53,162 @@ def get_db_connection():
cursorclass=DictCursor cursorclass=DictCursor
) )
# ==================== 密码加密 ====================
def hash_password(password):
"""哈希密码"""
return hashlib.sha256(password.encode()).hexdigest()
def verify_password(password, password_hash):
"""验证密码"""
return hashlib.sha256(password.encode()).hexdigest() == password_hash
# ==================== JWT 工具 ====================
def create_token(user_id, username):
"""创建 JWT token"""
payload = {
'user_id': user_id,
'username': username,
'exp': datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS),
'iat': datetime.utcnow()
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def decode_token(token):
"""解码 JWT token"""
try:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def token_required(f):
"""装饰器:验证 JWT token"""
@wraps(f)
def decorated(*args, **kwargs):
token = None
# 从 header 获取 token
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
if not token:
return jsonify({'error': '未登录', 'message': '请先登录'}), 401
payload = decode_token(token)
if not payload:
return jsonify({'error': '登录已过期', 'message': '请重新登录'}), 401
# 将用户信息传递给路由
request.user_id = payload.get('user_id')
request.username = payload.get('username')
return f(*args, **kwargs)
return decorated
# ==================== 用户认证 API ====================
@app.route('/api/auth/login', methods=['POST'])
def login():
"""用户登录"""
data = request.get_json()
username = data.get('username', '').strip()
password = data.get('password', '')
if not username or not password:
return jsonify({'error': '参数错误', 'message': '请输入用户名和密码'}), 400
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 查找用户
cursor.execute(
"SELECT id, username, password_hash FROM users WHERE username = %s",
(username,)
)
user = cursor.fetchone()
if not user:
return jsonify({'error': '登录失败', 'message': '用户名或密码错误'}), 401
# 验证密码
if not verify_password(password, user['password_hash']):
return jsonify({'error': '登录失败', 'message': '用户名或密码错误'}), 401
# 创建 token
token = create_token(user['id'], user['username'])
return jsonify({
'token': token,
'user_id': user['id'],
'username': user['username']
})
finally:
conn.close()
@app.route('/api/auth/register', methods=['POST'])
def register():
"""用户注册"""
data = request.get_json()
username = data.get('username', '').strip()
password = data.get('password', '')
email = data.get('email', '').strip()
if not username or not password:
return jsonify({'error': '参数错误', 'message': '请输入用户名和密码'}), 400
if len(username) < 3:
return jsonify({'error': '用户名太短', 'message': '用户名至少3个字符'}), 400
if len(password) < 6:
return jsonify({'error': '密码太短', 'message': '密码至少6个字符'}), 400
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 检查用户名是否已存在
cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
if cursor.fetchone():
return jsonify({'error': '注册失败', 'message': '用户名已存在'}), 409
# 创建用户
password_hash = hash_password(password)
cursor.execute(
"INSERT INTO users (username, password_hash, email) VALUES (%s, %s, %s)",
(username, password_hash, email)
)
conn.commit()
user_id = cursor.lastrowid
# 创建 token
token = create_token(user_id, username)
return jsonify({
'token': token,
'user_id': user_id,
'username': username
}), 201
finally:
conn.close()
@app.route('/api/auth/profile', methods=['GET'])
@token_required
def get_profile():
"""获取当前用户信息"""
return jsonify({
'user_id': request.user_id,
'username': request.username
})
@app.route('/api/auth/logout', methods=['POST'])
@token_required
def logout():
"""退出登录(前端删除 token 即可)"""
return jsonify({'message': '已退出登录'})
# ==================== 分类 API ==================== # ==================== 分类 API ====================
@app.route('/api/categories', methods=['GET']) @app.route('/api/categories', methods=['GET'])
@ -63,8 +228,9 @@ def get_categories():
conn.close() conn.close()
@app.route('/api/categories', methods=['POST']) @app.route('/api/categories', methods=['POST'])
@token_required
def create_category(): def create_category():
"""创建分类""" """创建分类(需要登录)"""
data = request.get_json() data = request.get_json()
conn = get_db_connection() conn = get_db_connection()
@ -84,8 +250,9 @@ def create_category():
conn.close() conn.close()
@app.route('/api/categories/<int:category_id>', methods=['PUT']) @app.route('/api/categories/<int:category_id>', methods=['PUT'])
@token_required
def update_category(category_id): def update_category(category_id):
"""更新分类""" """更新分类(需要登录)"""
data = request.get_json() data = request.get_json()
conn = get_db_connection() conn = get_db_connection()
@ -104,8 +271,9 @@ def update_category(category_id):
conn.close() conn.close()
@app.route('/api/categories/<int:category_id>', methods=['DELETE']) @app.route('/api/categories/<int:category_id>', methods=['DELETE'])
@token_required
def delete_category(category_id): def delete_category(category_id):
"""删除分类""" """删除分类(需要登录)"""
conn = get_db_connection() conn = get_db_connection()
try: try:
with conn.cursor() as cursor: with conn.cursor() as cursor:
@ -159,8 +327,9 @@ def get_websites():
conn.close() conn.close()
@app.route('/api/websites', methods=['POST']) @app.route('/api/websites', methods=['POST'])
@token_required
def create_website(): def create_website():
"""创建网站""" """创建网站(需要登录)"""
data = request.get_json() data = request.get_json()
conn = get_db_connection() conn = get_db_connection()
@ -193,8 +362,9 @@ def create_website():
conn.close() conn.close()
@app.route('/api/websites/<int:website_id>', methods=['PUT']) @app.route('/api/websites/<int:website_id>', methods=['PUT'])
@token_required
def update_website(website_id): def update_website(website_id):
"""更新网站""" """更新网站(需要登录)"""
data = request.get_json() data = request.get_json()
conn = get_db_connection() conn = get_db_connection()
@ -251,8 +421,9 @@ def update_website(website_id):
conn.close() conn.close()
@app.route('/api/websites/<int:website_id>', methods=['DELETE']) @app.route('/api/websites/<int:website_id>', methods=['DELETE'])
@token_required
def delete_website(website_id): def delete_website(website_id):
"""删除网站""" """删除网站(需要登录)"""
conn = get_db_connection() conn = get_db_connection()
try: try:
with conn.cursor() as cursor: with conn.cursor() as cursor:
@ -264,7 +435,7 @@ def delete_website(website_id):
@app.route('/api/websites/<int:website_id>/click', methods=['POST']) @app.route('/api/websites/<int:website_id>/click', methods=['POST'])
def click_website(website_id): def click_website(website_id):
"""点击网站""" """点击网站(无需登录)"""
conn = get_db_connection() conn = get_db_connection()
try: try:
with conn.cursor() as cursor: with conn.cursor() as cursor:
@ -287,5 +458,34 @@ def health_check():
def root(): def root():
return jsonify({'message': '导航网站 API', 'docs': '/api/health'}) return jsonify({'message': '导航网站 API', 'docs': '/api/health'})
# ==================== 启动时创建 users 表 ====================
def init_db():
"""初始化数据库,创建 users 表(如果不存在)"""
try:
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(64) NOT NULL,
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.commit()
print("Users table initialized successfully")
finally:
conn.close()
except Exception as e:
print(f"Failed to initialize users table: {e}")
# 应用启动时初始化
with app.app_context():
init_db()
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5003, debug=False) app.run(host='0.0.0.0', port=5003, debug=False)

11
frontend/dev.log Normal file
View File

@ -0,0 +1,11 @@
> favorites-nav-frontend@1.0.0 dev
> vite --host 0.0.0.0 --port 5173
VITE v5.4.21 ready in 131 ms
➜ Local: http://localhost:5173/
➜ Network: http://192.168.8.207:5173/
➜ Network: http://192.168.8.170:5173/
➜ Network: http://172.17.0.1:5173/