commit 25f85f3e8cd0d6ed25ac1cbc8fafa822753934ae Author: 吳佩庭 Date: Sun Sep 21 19:56:14 2025 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f18f89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# 環境變數文件 +.env + +# Python 相關 +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 虛擬環境 +venv/ +env/ +ENV/ + +# IDE 相關 +.vscode/ +.idea/ +*.swp +*.swo + +# 作業系統相關 +.DS_Store +Thumbs.db + +# 日誌文件 +*.log + +# 測試相關 +.pytest_cache/ +.coverage +htmlcov/ + +# 資料庫相關 +*.db +*.sqlite3 + +# 敏感資料 +data/*.xlsx +data/*.csv +*.bak diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b32888 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Panjit 密碼管理系統 API + +## 概述 + +這是一個為 Panjit 公司設計的密碼管理系統 API,提供員工登入驗證和密碼修改功能。 + +## 功能特色 + +- 🔐 安全的密碼雜湊技術 +- 👤 支援多種登入方式 +- 🔄 密碼修改功能 +- 📚 完整的 API 文件 +- ⚙️ 環境變數配置 +- 🛡️ 密碼強度驗證 + +## 安裝與設定 + +### 1. 安裝依賴套件 + +```bash +pip install -r requirements.txt +``` + +### 2. 環境變數配置 + +複製 `env_example.txt` 為 `.env` 並根據實際環境修改相應的值。 + +### 3. 初始化資料庫 + +```bash +python excel_to_db.py +``` + +### 4. 啟動服務 + +```bash +python app.py +``` + +## API 文件 + +啟動服務後,可透過以下網址查看完整的 API 文件: + +``` +http://localhost:12023/api/docs +``` + +## 密碼規則 + +- 長度:6-50 個字元 +- 必須包含:至少一個字母和一個數字 +- 不能與舊密碼相同 + +## 安全特性 + +- 所有密碼使用安全雜湊技術儲存 +- 支援多種登入方式 +- 完整的錯誤處理和狀態碼 +- 環境變數配置敏感資訊 + +## 開發工具 + +- **測試腳本**:`test_api.py`、`test_change_password.py` +- **資料庫重置**:`reset_database.py` +- **資料匯入**:`excel_to_db.py` + +## 技術棧 + +- **後端框架**:Flask +- **資料庫**:MySQL +- **密碼雜湊**:JWT +- **API 文件**:Swagger UI +- **配置管理**:python-dotenv + +## 授權 + +此專案為 Panjit 公司內部使用。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..59d384b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,50 @@ +# 安全說明 + +## 重要安全注意事項 + +### 環境變數配置 + +1. **絕對不要**將 `.env` 文件提交到版本控制系統 +2. **絕對不要**在公開文件中暴露真實的資料庫連接資訊 +3. **絕對不要**在 README 或其他文件中包含真實的 API 測試範例 + +### 資料庫安全 + +- 使用強密碼保護資料庫 +- 定期更換 JWT 密鑰 +- 限制資料庫訪問權限 +- 定期備份資料庫 + +### API 安全 + +- 所有密碼都經過安全雜湊處理 +- 使用 HTTPS 進行生產環境部署 +- 實施適當的速率限制 +- 記錄所有 API 訪問日誌 + +### 部署安全 + +- 在生產環境中關閉 DEBUG 模式 +- 使用環境變數管理敏感配置 +- 定期更新依賴套件 +- 實施適當的防火牆規則 + +## 配置檢查清單 + +在部署前,請確認: + +- [ ] `.env` 文件已正確配置且未提交到版本控制 +- [ ] 資料庫連接資訊已更新為生產環境 +- [ ] JWT 密鑰已更改為強密碼 +- [ ] DEBUG 模式已關閉 +- [ ] 所有測試資料已清除 +- [ ] API 文件中的範例已移除敏感資訊 + +## 緊急處理 + +如果發現安全問題: + +1. 立即更改所有密碼和密鑰 +2. 檢查系統日誌 +3. 通知相關人員 +4. 實施必要的安全措施 diff --git a/app.py b/app.py new file mode 100644 index 0000000..8d7c6fd --- /dev/null +++ b/app.py @@ -0,0 +1,683 @@ +from flask import Flask, request, jsonify +from flask_swagger_ui import get_swaggerui_blueprint +from flask_cors import CORS +import pymysql +import jwt +from datetime import datetime +import hashlib +import hmac +import base64 +from config import Config + +app = Flask(__name__) + +# 啟用 CORS 支援 +CORS(app, resources={ + r"/api/*": { + "origins": "*", + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"] + } +}) + +# 使用配置類別 +DB_CONFIG = Config.get_db_config() +JWT_SECRET = Config.JWT_SECRET + +# Swagger UI 配置 +SWAGGER_URL = '/api/docs' # Swagger UI 的 URL +API_URL = '/api/swagger.json' # API 文件的 JSON 位置 + +# 建立 Swagger UI Blueprint +swaggerui_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, + API_URL, + config={ + 'app_name': Config.APP_NAME, + 'app_version': Config.APP_VERSION, + 'description': Config.APP_DESCRIPTION + } +) + +# 註冊 Swagger UI Blueprint +app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) + +@app.route('/api/swagger.json') +def swagger(): + """ + Swagger API 文件 JSON + """ + return jsonify({ + "swagger": "2.0", + "info": { + "title": Config.APP_NAME, + "version": Config.APP_VERSION, + "description": Config.APP_DESCRIPTION, + "contact": { + "name": "Panjit IT Department", + "email": "it@panjit.com.tw" + } + }, + "host": f"{Config.API_HOST}:{Config.API_PORT}", + "basePath": "/api", + "schemes": ["http", "https"], + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": { + "/login": { + "post": { + "tags": ["Authentication"], + "summary": "用戶登入驗證", + "description": "使用帳號和密碼進行登入驗證", + "parameters": [ + { + "name": "body", + "in": "body", + "required": True, + "schema": { + "type": "object", + "properties": { + "account": { + "type": "string", + "description": "用戶帳號(loginid 或 email)", + "example": "92460" + }, + "password": { + "type": "string", + "description": "用戶密碼", + "example": "Panjit92460" + } + }, + "required": ["account", "password"] + } + } + ], + "responses": { + "200": { + "description": "登入成功", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + "user": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "loginid": {"type": "string"}, + "username": {"type": "string"}, + "email": {"type": "string"} + } + } + } + } + }, + "401": { + "description": "登入失敗", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + "error_code": {"type": "string"} + } + } + } + } + } + }, + "/change-password": { + "post": { + "tags": ["Password Management"], + "summary": "修改用戶密碼", + "description": "修改用戶的登入密碼", + "parameters": [ + { + "name": "body", + "in": "body", + "required": True, + "schema": { + "type": "object", + "properties": { + "account": { + "type": "string", + "description": "用戶帳號(loginid 或 email)", + "example": "92460" + }, + "old_password": { + "type": "string", + "description": "舊密碼", + "example": "Panjit92460" + }, + "new_password": { + "type": "string", + "description": "新密碼(6-50字元,需包含字母和數字)", + "example": "MyNewPassword123" + } + }, + "required": ["account", "old_password", "new_password"] + } + } + ], + "responses": { + "200": { + "description": "密碼修改成功", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"} + } + } + }, + "400": { + "description": "請求錯誤", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + "error_code": {"type": "string"} + } + } + }, + "401": { + "description": "舊密碼不正確", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + "error_code": {"type": "string"} + } + } + } + } + } + }, + "/health": { + "get": { + "tags": ["System"], + "summary": "系統健康檢查", + "description": "檢查 API 服務和資料庫連線狀態", + "responses": { + "200": { + "description": "系統正常", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + "database": {"type": "string"}, + "timestamp": {"type": "string"} + } + } + } + } + } + }, + "/user/{account}": { + "get": { + "tags": ["User Management"], + "summary": "查詢用戶資訊", + "description": "根據帳號查詢用戶基本資訊", + "parameters": [ + { + "name": "account", + "in": "path", + "required": True, + "type": "string", + "description": "用戶帳號(loginid 或 email)", + "example": "92460" + } + ], + "responses": { + "200": { + "description": "查詢成功", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "user": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "loginid": {"type": "string"}, + "username": {"type": "string"}, + "email": {"type": "string"} + } + } + } + } + }, + "404": { + "description": "用戶不存在", + "schema": { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + "error_code": {"type": "string"} + } + } + } + } + } + } + }, + "definitions": { + "Error": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": False}, + "message": {"type": "string"}, + "error_code": {"type": "string"} + } + }, + "User": { + "type": "object", + "properties": { + "id": {"type": "integer", "example": 1}, + "loginid": {"type": "string", "example": "92460"}, + "username": {"type": "string", "example": "丁于軒"}, + "email": {"type": "string", "example": "sharnading@panjit.com.tw"} + } + } + } + }) + +def verify_password(input_password, hashed_password): + """ + 驗證密碼是否正確 + """ + try: + # 解碼 JWT token + decoded = jwt.decode(hashed_password, JWT_SECRET, algorithms=['HS256']) + + # 檢查 token 類型 + if decoded.get('type') != 'password_hash': + return False + + # 比較密碼 + stored_password = decoded.get('password', '') + return input_password == stored_password + + except jwt.ExpiredSignatureError: + print("JWT token 已過期") + return False + except jwt.InvalidTokenError: + print("無效的 JWT token") + return False + except Exception as e: + print(f"密碼驗證錯誤: {e}") + return False + +def hash_password_with_jwt(password): + """ + 使用 JWT 將密碼進行雜湊處理 + """ + try: + # 建立 JWT payload,包含密碼和時間戳 + payload = { + 'password': password, + 'timestamp': datetime.now().isoformat(), + 'type': 'password_hash' + } + + # 使用 JWT 編碼密碼 + token = jwt.encode(payload, JWT_SECRET, algorithm='HS256') + return token + except Exception as e: + print(f"密碼雜湊錯誤: {e}") + return None + +def validate_password_strength(password): + """ + 驗證密碼強度 + """ + if len(password) < 6: + return False, "密碼長度至少需要 6 個字元" + + if len(password) > 50: + return False, "密碼長度不能超過 50 個字元" + + # 檢查是否包含至少一個字母和一個數字 + has_letter = any(c.isalpha() for c in password) + has_digit = any(c.isdigit() for c in password) + + if not has_letter: + return False, "密碼必須包含至少一個字母" + + if not has_digit: + return False, "密碼必須包含至少一個數字" + + return True, "密碼強度符合要求" + +def update_user_password(loginid_or_email, old_password, new_password): + """ + 更新用戶密碼 + """ + try: + connection = pymysql.connect(**DB_CONFIG) + cursor = connection.cursor(pymysql.cursors.DictCursor) + + # 先查詢用戶資料 + query = """ + SELECT id, loginid, username, email, hashed_password + FROM user_accounts + WHERE loginid = %s OR email = %s + """ + + cursor.execute(query, (loginid_or_email, loginid_or_email)) + user = cursor.fetchone() + + if not user: + return False, "用戶不存在" + + # 驗證舊密碼 + if not verify_password(old_password, user['hashed_password']): + return False, "舊密碼不正確" + + # 驗證新密碼強度 + is_valid, message = validate_password_strength(new_password) + if not is_valid: + return False, message + + # 生成新密碼的雜湊 + new_hashed_password = hash_password_with_jwt(new_password) + if not new_hashed_password: + return False, "密碼雜湊失敗" + + # 更新密碼 + update_query = """ + UPDATE user_accounts + SET hashed_password = %s, updated_at = CURRENT_TIMESTAMP + WHERE id = %s + """ + + cursor.execute(update_query, (new_hashed_password, user['id'])) + connection.commit() + + return True, "密碼修改成功" + + except Exception as e: + print(f"更新密碼錯誤: {e}") + return False, f"系統錯誤: {str(e)}" + finally: + if 'connection' in locals(): + connection.close() + +def get_user_by_loginid_or_email(loginid_or_email): + """ + 根據 loginid 或 email 查詢用戶資料 + """ + try: + connection = pymysql.connect(**DB_CONFIG) + cursor = connection.cursor(pymysql.cursors.DictCursor) + + # 查詢用戶資料 + query = """ + SELECT id, loginid, username, email, hashed_password + FROM user_accounts + WHERE loginid = %s OR email = %s + """ + + cursor.execute(query, (loginid_or_email, loginid_or_email)) + user = cursor.fetchone() + + return user + + except Exception as e: + print(f"資料庫查詢錯誤: {e}") + return None + finally: + if 'connection' in locals(): + connection.close() + +@app.route('/api/login', methods=['POST']) +def login(): + """ + 登入 API + 接收 JSON 格式: {"account": "帳號或email", "password": "密碼"} + """ + try: + # 取得請求資料 + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'message': '請提供 JSON 格式的資料', + 'error_code': 'INVALID_REQUEST' + }), 400 + + account = data.get('account', '').strip() + password = data.get('password', '').strip() + + # 檢查必要欄位 + if not account or not password: + return jsonify({ + 'success': False, + 'message': '請提供帳號和密碼', + 'error_code': 'MISSING_FIELDS' + }), 400 + + # 查詢用戶資料 + user = get_user_by_loginid_or_email(account) + + if not user: + return jsonify({ + 'success': False, + 'message': '無權限,請詢問資訊部同仁', + 'error_code': 'USER_NOT_FOUND' + }), 401 + + # 驗證密碼 + if not verify_password(password, user['hashed_password']): + return jsonify({ + 'success': False, + 'message': '無權限,請詢問資訊部同仁', + 'error_code': 'INVALID_PASSWORD' + }), 401 + + # 登入成功 + return jsonify({ + 'success': True, + 'message': '登入成功', + 'user': { + 'id': user['id'], + 'loginid': user['loginid'], + 'username': user['username'], + 'email': user['email'] + } + }), 200 + + except Exception as e: + print(f"登入 API 錯誤: {e}") + return jsonify({ + 'success': False, + 'message': '系統錯誤,請稍後再試', + 'error_code': 'SYSTEM_ERROR' + }), 500 + +@app.route('/api/health', methods=['GET']) +def health_check(): + """ + 健康檢查 API + """ + try: + # 測試資料庫連線 + connection = pymysql.connect(**DB_CONFIG) + cursor = connection.cursor() + cursor.execute("SELECT 1") + cursor.fetchone() + connection.close() + + return jsonify({ + 'success': True, + 'message': 'API 服務正常', + 'database': '連線正常', + 'timestamp': datetime.now().isoformat() + }), 200 + + except Exception as e: + return jsonify({ + 'success': False, + 'message': 'API 服務異常', + 'database': '連線失敗', + 'error': str(e), + 'timestamp': datetime.now().isoformat() + }), 500 + +@app.route('/api/user/', methods=['GET']) +def get_user_info(account): + """ + 查詢用戶資訊 API(不包含密碼) + """ + try: + user = get_user_by_loginid_or_email(account) + + if not user: + return jsonify({ + 'success': False, + 'message': '用戶不存在', + 'error_code': 'USER_NOT_FOUND' + }), 404 + + # 回傳用戶資訊(不包含密碼) + return jsonify({ + 'success': True, + 'user': { + 'id': user['id'], + 'loginid': user['loginid'], + 'username': user['username'], + 'email': user['email'] + } + }), 200 + + except Exception as e: + print(f"查詢用戶資訊錯誤: {e}") + return jsonify({ + 'success': False, + 'message': '系統錯誤,請稍後再試', + 'error_code': 'SYSTEM_ERROR' + }), 500 + +@app.route('/api/change-password', methods=['POST']) +def change_password(): + """ + 修改密碼 API + 接收 JSON 格式: {"account": "帳號或email", "old_password": "舊密碼", "new_password": "新密碼"} + """ + try: + # 取得請求資料 + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'message': '請提供 JSON 格式的資料', + 'error_code': 'INVALID_REQUEST' + }), 400 + + account = data.get('account', '').strip() + old_password = data.get('old_password', '').strip() + new_password = data.get('new_password', '').strip() + + # 檢查必要欄位 + if not account or not old_password or not new_password: + return jsonify({ + 'success': False, + 'message': '請提供帳號、舊密碼和新密碼', + 'error_code': 'MISSING_FIELDS' + }), 400 + + # 檢查新密碼不能與舊密碼相同 + if old_password == new_password: + return jsonify({ + 'success': False, + 'message': '新密碼不能與舊密碼相同', + 'error_code': 'SAME_PASSWORD' + }), 400 + + # 更新密碼 + success, message = update_user_password(account, old_password, new_password) + + if success: + return jsonify({ + 'success': True, + 'message': message + }), 200 + else: + # 根據錯誤類型返回不同的狀態碼 + if "用戶不存在" in message: + return jsonify({ + 'success': False, + 'message': message, + 'error_code': 'USER_NOT_FOUND' + }), 404 + elif "舊密碼不正確" in message: + return jsonify({ + 'success': False, + 'message': message, + 'error_code': 'INVALID_OLD_PASSWORD' + }), 401 + elif "密碼長度" in message or "密碼必須" in message: + return jsonify({ + 'success': False, + 'message': message, + 'error_code': 'WEAK_PASSWORD' + }), 400 + else: + return jsonify({ + 'success': False, + 'message': message, + 'error_code': 'UPDATE_FAILED' + }), 500 + + except Exception as e: + print(f"修改密碼 API 錯誤: {e}") + return jsonify({ + 'success': False, + 'message': '系統錯誤,請稍後再試', + 'error_code': 'SYSTEM_ERROR' + }), 500 + +@app.errorhandler(404) +def not_found(error): + return jsonify({ + 'success': False, + 'message': 'API 端點不存在', + 'error_code': 'NOT_FOUND' + }), 404 + +@app.errorhandler(405) +def method_not_allowed(error): + return jsonify({ + 'success': False, + 'message': '不支援的 HTTP 方法', + 'error_code': 'METHOD_NOT_ALLOWED' + }), 405 + +if __name__ == '__main__': + print(f"啟動 {Config.APP_NAME} v{Config.APP_VERSION}...") + print("API 端點:") + print(" POST /api/login - 登入驗證") + print(" POST /api/change-password - 修改密碼") + print(" GET /api/health - 健康檢查") + print(" GET /api/user/ - 查詢用戶資訊") + print(f" GET {SWAGGER_URL} - Swagger API 文件") + print(f"\n服務地址: http://{Config.API_HOST}:{Config.API_PORT}") + print(f"Swagger 文件: http://{Config.API_HOST}:{Config.API_PORT}{SWAGGER_URL}") + print("\n範例請求:") + print("登入驗證:") + print(f'curl -X POST http://{Config.API_HOST}:{Config.API_PORT}/api/login \\') + print(' -H "Content-Type: application/json" \\') + print(' -d \'{"account": "92460", "password": "Panjit92460"}\'') + print("\n修改密碼:") + print(f'curl -X POST http://{Config.API_HOST}:{Config.API_PORT}/api/change-password \\') + print(' -H "Content-Type: application/json" \\') + print(' -d \'{"account": "92460", "old_password": "Panjit92460", "new_password": "MyNewPassword123"}\'') + print("\n服務啟動中...") + + app.run(host=Config.API_HOST, port=Config.API_PORT, debug=Config.API_DEBUG) diff --git a/config.py b/config.py new file mode 100644 index 0000000..2612473 --- /dev/null +++ b/config.py @@ -0,0 +1,40 @@ +import os +from dotenv import load_dotenv + +# 載入環境變數 +load_dotenv() + +class Config: + """應用程式配置類別""" + + # 資料庫配置 + DB_HOST = os.getenv('DB_HOST', 'mysql.theaken.com') + DB_PORT = int(os.getenv('DB_PORT', 33306)) + DB_NAME = os.getenv('DB_NAME', 'db_panjit_password') + DB_USER = os.getenv('DB_USER', 'root') + DB_PASSWORD = os.getenv('DB_PASSWORD', 'zh6161168') + + # JWT 配置 + JWT_SECRET = os.getenv('JWT_SECRET', 'panjit666') + + # API 配置 + API_HOST = os.getenv('API_HOST', '0.0.0.0') + API_PORT = int(os.getenv('API_PORT', 5000)) + API_DEBUG = os.getenv('API_DEBUG', 'True').lower() == 'true' + + # 應用程式配置 + APP_NAME = os.getenv('APP_NAME', 'Panjit Password Management API') + APP_VERSION = os.getenv('APP_VERSION', '1.0.0') + APP_DESCRIPTION = os.getenv('APP_DESCRIPTION', 'Panjit 密碼管理系統 API') + + @classmethod + def get_db_config(cls): + """取得資料庫配置字典""" + return { + 'host': cls.DB_HOST, + 'port': cls.DB_PORT, + 'user': cls.DB_USER, + 'password': cls.DB_PASSWORD, + 'database': cls.DB_NAME, + 'charset': 'utf8mb4' + } diff --git a/env_example.txt b/env_example.txt new file mode 100644 index 0000000..07f2181 --- /dev/null +++ b/env_example.txt @@ -0,0 +1,22 @@ +# 環境變數配置範例 +# 請複製此文件為 .env 並修改相應的值 + +# 資料庫配置 +DB_HOST=your_database_host +DB_PORT=your_database_port +DB_NAME=your_database_name +DB_USER=your_database_user +DB_PASSWORD=your_database_password + +# JWT 配置 +JWT_SECRET=your_jwt_secret_key + +# API 配置 +API_HOST=0.0.0.0 +API_PORT=12023 +API_DEBUG=True + +# 應用程式配置 +APP_NAME=Panjit Password Management API +APP_VERSION=1.0.0 +APP_DESCRIPTION=Panjit 密碼管理系統 API diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..77aac38 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +pandas==2.0.3 +PyJWT==2.8.0 +PyMySQL==1.1.0 +openpyxl==3.1.2 +Flask==2.3.3 +flask-swagger-ui==4.11.1 +python-dotenv==1.0.0 +flask-cors==4.0.0