feat: 添加用户认证功能 (JWT)
This commit is contained in:
parent
77bdfbc5da
commit
bade5e7b26
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
218
backend/main.py
218
backend/main.py
@ -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
11
frontend/dev.log
Normal 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/
|
||||||
Loading…
Reference in New Issue
Block a user