Flask - 密碼哈希
目標
- 安裝並配置 Flask-Bcrypt
- 將密碼存儲為哈希值
- 更新認證邏輯以驗證哈希密碼
步驟
準備環境
- 繼續使用
flask_api/
項目結構,激活虛擬環境:1 2
# Windows: flask_api_env\Scripts\activate # macOS/Linux: source flask_api_env/bin/activate
- 安裝 Flask-Bcrypt:
1
pip install flask-bcrypt
- 繼續使用
配置 Flask-Bcrypt
修改 app/init.py,初始化 Bcrypt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
from flask import Flask, jsonify, g from flask_sqlalchemy import SQLAlchemy from flask_marshmallow import Marshmallow from flask_bcrypt import Bcrypt # 新增 import jwt from functools import wraps from .routes.v1.todos import todos_bp as todos_v1_bp from .routes.v1.users import users_bp as users_v1_bp from .routes.v1.posts import posts_bp as posts_v1_bp from .routes.v2.todos import todos_bp as todos_v2_bp from .routes.v2.posts import posts_bp as posts_v2_bp from .config import config_map import os db = SQLAlchemy() ma = Marshmallow() bcrypt = Bcrypt() # 初始化 Bcrypt def create_app(): app = Flask(__name__) env = os.getenv('FLASK_ENV', 'development') app.config.from_object(config_map[env]) db.init_app(app) ma.init_app(app) bcrypt.init_app(app) # 初始化 Bcrypt app.register_blueprint(todos_v1_bp, url_prefix='/api/v1') app.register_blueprint(users_v1_bp, url_prefix='/api/v1') app.register_blueprint(posts_v1_bp, url_prefix='/api/v1') app.register_blueprint(todos_v2_bp, url_prefix='/api/v2') app.register_blueprint(posts_v2_bp, url_prefix='/api/v2') @app.errorhandler(404) def not_found(error): return jsonify({'error': 'Not Found', 'message': str(error)}), 404 @app.errorhandler(400) def bad_request(error): return jsonify({'error': 'Bad Request', 'message': str(error)}), 400 @app.errorhandler(500) def internal_error(error): return jsonify({'error': 'Internal Server Error', 'message': 'Something went wrong on our end'}), 500 with app.app_context(): db.create_all() return app def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): from .models import User token = request.headers.get('Authorization') if not token: abort(401, description='Missing token') try: if token.startswith('Bearer '): token = token[7:] data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) user = User.query.get(data['user_id']) if not user: abort(401, description='Invalid token') g.current_user = user except jwt.ExpiredSignatureError: abort(401, description='Token has expired') except jwt.InvalidTokenError: abort(401, description='Invalid token') return f(*args, **kwargs) return decorated_function
更新模型
修改 app/models.py,調整密碼字段為哈希值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
from . import db, bcrypt from datetime import datetime class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) # 改為 password_hash posts = db.relationship('Post', backref='user', lazy=True) def set_password(self, password): self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8') def check_password(self, password): return bcrypt.check_password_hash(self.password_hash, password) class Post(db.Model): __tablename__ = 'posts' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) content = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) category = db.Column(db.String(50), default='general')
代碼解釋:
set_password
:將明文密碼轉為哈希值。check_password
:驗證輸入密碼是否與哈希值匹配。- 字段改為
password_hash
,長度設為 128 以容納哈希。
更新用戶路由
修改 app/routes/v1/users.py,使用哈希密碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
from flask import Blueprint, jsonify, request, abort from ...models import User from ... import db from ...schemas import user_schema, users_schema import jwt import datetime users_bp = Blueprint('users_v1', __name__) @users_bp.route('/users', methods=['GET']) def get_users(): users = User.query.all() return jsonify({'users': users_schema.dump(users)}) @users_bp.route('/users', methods=['POST']) def create_user(): if not request.is_json: abort(400, description='Request must be JSON') data = request.get_json() if 'username' not in data or 'password' not in data: abort(400, description='Missing username or password') if User.query.filter_by(username=data['username']).first(): abort(400, description='Username already exists') user = User(username=data['username']) user.set_password(data['password']) # 設置哈希密碼 db.session.add(user) db.session.commit() return jsonify(user_schema.dump(user)), 201 @users_bp.route('/login', methods=['POST']) def login(): if not request.is_json: abort(400, description='Request must be JSON') data = request.get_json() if 'username' not in data or 'password' not in data: abort(400, description='Missing username or password') user = User.query.filter_by(username=data['username']).first() if not user or not user.check_password(data['password']): abort(401, description='Invalid credentials') token = jwt.encode({ 'user_id': user.id, 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) }, users_bp.app.config['SECRET_KEY'], algorithm='HS256') return jsonify({'token': token})
運行應用
- 刪除舊的
blog.db
(因表結構改變),運行:1
python run.py
- 刪除舊的
測試 API
- 使用 Postman 測試:
- POST /api/v1/users:
- Body:
{"username": "alice", "password": "1234"}
- 預期響應:
{"id": 1, "username": "alice", "posts": []}
- 檢查數據庫:
password_hash
應為哈希值而非 “1234”。
- Body:
- POST /api/v1/login:
- Body:
{"username": "alice", "password": "1234"}
- 預期響應:
{"token": "..."}
- Body:
- POST /api/v1/login(錯誤密碼):
- Body:
{"username": "alice", "password": "wrong"}
- 預期響應:401
- Body:
- POST /api/v1/posts:
- Headers:
Authorization: Bearer <token>
- Body:
{"title": "My Post", "content": "Hello"}
- 預期響應:201
- Headers:
- POST /api/v1/users:
- 使用 Postman 測試:
作業
- 在 v2 的 posts 路由中應用相同的密碼哈希和 JWT 認證。
- 添加一個端點
PUT /api/v1/users/<int:user_id>/password
,允許認證用戶更改自己的密碼。
注意事項
- 表結構改變後需重建數據庫。
bcrypt
生成的哈希值較長,確保數據庫字段長度足夠(這裡設為 128)。- 生產環境應使用更強的
SECRET_KEY
。
本文章以 CC BY 4.0 授權