1. Flask 简介
Flask 是一个轻量级的 Python Web 应用框架,基于 Werkzeug WSGI 工具库和 Jinja2 模板引擎。它被称为"微框架",因为它不需要特定的工具或库,但可以通过扩展来添加功能。
1.1 Flask 的特点
- 轻量级:核心功能简单,易于学习
- 灵活性:可以根据需求选择组件
- 可扩展:丰富的扩展生态系统
- RESTful:内置对 RESTful 请求分发的支持
- 调试友好:内置开发服务器和调试器
2. Flask 安装和环境配置
2.1 基础安装
bash
# 创建虚拟环境
python -m venv flask_env
source flask_env/bin/activate # Linux/Mac
# flask_env\Scripts\activate # Windows
# 安装 Flask
pip install Flask
# 安装常用扩展
pip install Flask-SQLAlchemy Flask-Migrate Flask-Login Flask-WTF Flask-Mail
2.2 项目结构
arduino
my_flask_app/
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ ├── forms.py
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ └── static/
│ ├── css/
│ ├── js/
│ └── images/
├── migrations/
├── config.py
├── requirements.txt
└── run.py
3. Flask 基础概念
3.1 最简单的 Flask 应用
python
# app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
@app.route('/user/<name>')
def user_profile(name):
return f'Hello, {name}!'
if __name__ == '__main__':
app.run(debug=True)
3.2 路由和视图函数
python
from flask import Flask, request, jsonify, render_template
app = Flask(__name__)
# 基本路由
@app.route('/')
def index():
return render_template('index.html')
# 带参数的路由
@app.route('/user/<int:user_id>')
def user_detail(user_id):
return f'User ID: {user_id}'
# 多种 HTTP 方法
@app.route('/api/users', methods=['GET', 'POST'])
def users_api():
if request.method == 'GET':
return jsonify({'users': []})
elif request.method == 'POST':
data = request.get_json()
return jsonify({'message': 'User created', 'data': data}), 201
# URL 参数类型
@app.route('/post/<string:title>')
def post_detail(title):
return f'Post: {title}'
@app.route('/category/<path:category_path>')
def category_detail(category_path):
return f'Category: {category_path}'
3.3 请求处理
python
from flask import request, session, redirect, url_for
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# 验证用户凭据
if validate_user(username, password):
session['user_id'] = get_user_id(username)
return redirect(url_for('dashboard'))
else:
return render_template('login.html', error='Invalid credentials')
return render_template('login.html')
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return 'No file uploaded', 400
file = request.files['file']
if file.filename == '':
return 'No file selected', 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return 'File uploaded successfully'
4. 模板系统 (Jinja2)
4.1 基础模板语法
html
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Default Title{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<ul>
<li><a href="{{ url_for('index') }}">Home</a></li>
<li><a href="{{ url_for('about') }}">About</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('profile') }}">Profile</a></li>
<li><a href="{{ url_for('logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('login') }}">Login</a></li>
{% endif %}
</ul>
</nav>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash-messages">
{% for message in messages %}
<div class="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>
html
<!-- templates/index.html -->
{% extends "base.html" %}
{% block title %}Home Page{% endblock %}
{% block content %}
<h1>Welcome to My App</h1>
{% if users %}
<h2>Users List</h2>
<ul>
{% for user in users %}
<li>
<a href="{{ url_for('user_detail', user_id=user.id) }}">
{{ user.username }}
</a>
- {{ user.email }}
</li>
{% endfor %}
</ul>
{% else %}
<p>No users found.</p>
{% endif %}
<!-- 条件判断 -->
{% if current_user.is_admin %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-admin">Admin Panel</a>
{% endif %}
<!-- 循环和过滤器 -->
{% for post in posts %}
<article>
<h3>{{ post.title|title }}</h3>
<p>{{ post.content|truncate(100) }}</p>
<small>Published on {{ post.created_at|strftime('%Y-%m-%d') }}</small>
</article>
{% endfor %}
{% endblock %}
4.2 自定义过滤器和函数
python
from datetime import datetime
import markdown
@app.template_filter('strftime')
def strftime_filter(date, format='%Y-%m-%d'):
"""自定义日期格式化过滤器"""
return date.strftime(format)
@app.template_filter('markdown')
def markdown_filter(text):
"""Markdown 转 HTML 过滤器"""
return markdown.markdown(text)
@app.context_processor
def utility_processor():
"""全局模板函数"""
def format_price(amount):
return f"${amount:.2f}"
def is_weekend():
return datetime.now().weekday() >= 5
return dict(format_price=format_price, is_weekend=is_weekend)
5. 数据库集成 (SQLAlchemy)
5.1 配置数据库
python
# config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
5.2 定义模型
python
# app/models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
db = SQLAlchemy()
# 多对多关系表
user_roles = db.Table('user_roles',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True)
)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
# 关系
posts = db.relationship('Post', backref='author', lazy='dynamic')
roles = db.relationship('Role', secondary=user_roles, backref='users')
profile = db.relationship('UserProfile', uselist=False, backref='user')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def has_role(self, role_name):
return any(role.name == role_name for role in self.roles)
def __repr__(self):
return f'<User {self.username}>'
class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
description = db.Column(db.String(200))
class UserProfile(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
first_name = db.Column(db.String(50))
last_name = db.Column(db.String(50))
bio = db.Column(db.Text)
avatar_url = db.Column(db.String(200))
birth_date = db.Column(db.Date)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
published = db.Column(db.Boolean, default=False)
# 外键
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
# 关系
comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')
tags = db.relationship('Tag', secondary='post_tags', backref='posts')
def __repr__(self):
return f'<Post {self.title}>'
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
description = db.Column(db.Text)
posts = db.relationship('Post', backref='category', lazy='dynamic')
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
# 多对多关系表
post_tags = db.Table('post_tags',
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 外键
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# 关系
user = db.relationship('User', backref='comments')
5.3 数据库操作
python
# app/routes.py
from flask import render_template, request, redirect, url_for, flash, jsonify
from app.models import db, User, Post, Category
@app.route('/users')
def users_list():
page = request.args.get('page', 1, type=int)
users = User.query.paginate(
page=page, per_page=10, error_out=False
)
return render_template('users.html', users=users)
@app.route('/posts')
def posts_list():
# 查询所有已发布的文章,按创建时间降序排列
posts = Post.query.filter_by(published=True)\
.order_by(Post.created_at.desc())\
.all()
return render_template('posts.html', posts=posts)
@app.route('/create_post', methods=['GET', 'POST'])
def create_post():
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
category_id = request.form.get('category_id')
post = Post(
title=title,
content=content,
user_id=current_user.id,
category_id=category_id if category_id else None
)
# 处理标签
tag_names = request.form.get('tags', '').split(',')
for tag_name in tag_names:
tag_name = tag_name.strip()
if tag_name:
tag = Tag.query.filter_by(name=tag_name).first()
if not tag:
tag = Tag(name=tag_name)
db.session.add(tag)
post.tags.append(tag)
db.session.add(post)
db.session.commit()
flash('Post created successfully!', 'success')
return redirect(url_for('posts_list'))
categories = Category.query.all()
return render_template('create_post.html', categories=categories)
@app.route('/api/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])
def user_api(user_id):
user = User.query.get_or_404(user_id)
if request.method == 'GET':
return jsonify({
'id': user.id,
'username': user.username,
'email': user.email,
'created_at': user.created_at.isoformat(),
'posts_count': user.posts.count()
})
elif request.method == 'PUT':
data = request.get_json()
user.username = data.get('username', user.username)
user.email = data.get('email', user.email)
db.session.commit()
return jsonify({'message': 'User updated successfully'})
elif request.method == 'DELETE':
db.session.delete(user)
db.session.commit()
return jsonify({'message': 'User deleted successfully'})
# 复杂查询示例
@app.route('/search')
def search():
query = request.args.get('q', '')
category_id = request.args.get('category')
posts_query = Post.query.filter_by(published=True)
if query:
posts_query = posts_query.filter(
Post.title.contains(query) | Post.content.contains(query)
)
if category_id:
posts_query = posts_query.filter_by(category_id=category_id)
posts = posts_query.order_by(Post.created_at.desc()).all()
return render_template('search_results.html', posts=posts, query=query)
6. 表单处理 (Flask-WTF)
6.1 定义表单
python
# app/forms.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import StringField, TextAreaField, PasswordField, SelectField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User
class LoginForm(FlaskForm):
username = StringField('用户名', validators=[DataRequired(), Length(min=4, max=20)])
password = PasswordField('密码', validators=[DataRequired()])
remember_me = BooleanField('记住我')
submit = SubmitField('登录')
class RegistrationForm(FlaskForm):
username = StringField('用户名', validators=[
DataRequired(),
Length(min=4, max=20)
])
email = StringField('邮箱', validators=[DataRequired(), Email()])
password = PasswordField('密码', validators=[
DataRequired(),
Length(min=6)
])
password2 = PasswordField('确认密码', validators=[
DataRequired(),
EqualTo('password', message='密码不匹配')
])
submit = SubmitField('注册')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('用户名已存在,请选择其他用户名')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('邮箱已被注册,请使用其他邮箱')
class PostForm(FlaskForm):
title = StringField('标题', validators=[DataRequired(), Length(max=200)])
content = TextAreaField('内容', validators=[DataRequired()])
category = SelectField('分类', coerce=int)
tags = StringField('标签 (用逗号分隔)')
published = BooleanField('立即发布')
featured_image = FileField('特色图片', validators=[
FileAllowed(['jpg', 'jpeg', 'png', 'gif'], '只允许图片文件!')
])
submit = SubmitField('保存')
def __init__(self, *args, **kwargs):
super(PostForm, self).__init__(*args, **kwargs)
from app.models import Category
self.category.choices = [(0, '选择分类')] + [
(c.id, c.name) for c in Category.query.all()
]
class ProfileForm(FlaskForm):
first_name = StringField('名字', validators=[Length(max=50)])
last_name = StringField('姓氏', validators=[Length(max=50)])
bio = TextAreaField('个人简介', validators=[Length(max=500)])
avatar = FileField('头像', validators=[
FileAllowed(['jpg', 'jpeg', 'png'], '只允许图片文件!')
])
submit = SubmitField('更新资料')
6.2 在视图中使用表单
python
from app.forms import LoginForm, RegistrationForm, PostForm
from flask_login import login_user, logout_user, login_required, current_user
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember_me.data)
# 重定向到用户原本想访问的页面
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('index')
flash(f'欢迎回来,{user.username}!', 'success')
return redirect(next_page)
else:
flash('用户名或密码错误', 'error')
return render_template('login.html', form=form)
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('注册成功!请登录', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/create_post', methods=['GET', 'POST'])
@login_required
def create_post():
form = PostForm()
if form.validate_on_submit():
post = Post(
title=form.title.data,
content=form.content.data,
published=form.published.data,
user_id=current_user.id
)
if form.category.data:
post.category_id = form.category.data
# 处理文件上传
if form.featured_image.data:
filename = secure_filename(form.featured_image.data.filename)
form.featured_image.data.save(
os.path.join(app.config['UPLOAD_FOLDER'], filename)
)
post.featured_image = filename
# 处理标签
if form.tags.data:
tag_names = [tag.strip() for tag in form.tags.data.split(',')]
for tag_name in tag_names:
if tag_name:
tag = Tag.query.filter_by(name=tag_name).first()
if not tag:
tag = Tag(name=tag_name)
db.session.add(tag)
post.tags.append(tag)
db.session.add(post)
db.session.commit()
flash('文章创建成功!', 'success')
return redirect(url_for('post_detail', id=post.id))
return render_template('create_post.html', form=form)
7. 用户认证 (Flask-Login)
7.1 配置用户认证
python
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
# 初始化扩展
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
# 配置登录管理器
login_manager.login_view = 'login'
login_manager.login_message = '请先登录以访问此页面'
login_manager.login_message_category = 'info'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# 注册蓝图
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
return app
7.2 权限装饰器
python
# app/decorators.py
from functools import wraps
from flask import abort
from flask_login import current_user
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.has_role('admin'):
abort(403)
return f(*args, **kwargs)
return decorated_function
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.can(permission):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def same_user_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user_id = kwargs.get('user_id') or kwargs.get('id')
if not current_user.is_authenticated or \
(current_user.id != user_id and not current_user.has_role('admin')):
abort(403)
return f(*args, **kwargs)
return decorated_function
# 使用示例
@app.route('/admin')
@login_required
@admin_required
def admin_panel():
return render_template('admin/dashboard.html')
@app.route('/user/<int:user_id>/edit')
@login_required
@same_user_required
def edit_profile(user_id):
user = User.query.get_or_404(user_id)
return render_template('edit_profile.html', user=user)
8. API 开发
8.1 RESTful API 设计
python
# app/api.py
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from app.models import db, User, Post
api = Blueprint('api', __name__, url_prefix='/api/v1')
# 错误处理
@api.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Resource not found'}), 404
@api.errorhandler(400)
def bad_request(error):
return jsonify({'error': 'Bad request'}), 400
@api.errorhandler(403)
def forbidden(error):
return jsonify({'error': 'Forbidden'}), 403
# 用户 API
@api.route('/users', methods=['GET'])
def get_users():
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
users = User.query.paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
'users': [user.to_dict() for user in users.items],
'pagination': {
'page': page,
'pages': users.pages,
'per_page': per_page,
'total': users.total,
'has_next': users.has_next,
'has_prev': users.has_prev
}
})
@api.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
@api.route('/users', methods=['POST'])
def create_user():
data = request.get_json()
# 验证必需字段
if not data or not data.get('username') or not data.get('email'):
return jsonify({'error': 'Missing required fields'}), 400
# 检查用户名和邮箱唯一性
if User.query.filter_by(username=data['username']).first():
return jsonify({'error': 'Username already exists'}), 400
if User.query.filter_by(email=data['email']).first():
return jsonify({'error': 'Email already exists'}), 400
user = User(
username=data['username'],
email=data['email']
)
if data.get('password'):
user.set_password(data['password'])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
@api.route('/users/<int:user_id>', methods=['PUT'])
@login_required
def update_user(user_id):
if current_user.id != user_id and not current_user.has_role('admin'):
return jsonify({'error': 'Forbidden'}), 403
user = User.query.get_or_404(user_id)
data = request.get_json()
if 'username' in data:
if User.query.filter_by(username=data['username']).first() and \
data['username'] != user.username:
return jsonify({'error': 'Username already exists'}), 400
user.username = data['username']
if 'email' in data:
if User.query.filter_by(email=data['email']).first() and \
data['email'] != user.email:
return jsonify({'error': 'Email already exists'}), 400
user.email = data['email']
db.session.commit()
return jsonify(user.to_dict())
@api.route('/users/<int:user_id>', methods=['DELETE'])
@login_required
def delete_user(user_id):
if current_user.id != user_id and not current_user.has_role('admin'):
return jsonify({'error': 'Forbidden'}), 403
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return jsonify({'message': 'User deleted successfully'})
# 文章 API
@api.route('/posts', methods=['GET'])
def get_posts():
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
query = Post.query.filter_by(published=True)
# 筛选参数
author_id = request.args.get('author_id', type=int)
if author_id:
query = query.filter_by(user_id=author_id)
category_id = request.args.get('category_id', type=int)
if category_id:
query = query.filter_by(category_id=category_id)
search = request.args.get('search')
if search:
query = query.filter(
Post.title.contains(search) | Post.content.contains(search)
)
posts = query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
'posts': [post.to_dict() for post in posts.items],
'pagination': {
'page': page,
'pages': posts.pages,
'per_page': per_page,
'total': posts.total
}
})
@api.route('/posts', methods=['POST'])
@login_required
def create_post():
data = request.get_json()
if not data or not data.get('title') or not data.get('content'):
return jsonify({'error': 'Missing required fields'}), 400
post = Post(
title=data['title'],
content=data['content'],
published=data.get('published', False),
user_id=current_user.id
)
if data.get('category_id'):
post.category_id = data['category_id']
db.session.add(post)
db.session.commit()
return jsonify(post.to_dict()), 201
# 添加序列化方法到模型
# 在 models.py 中添加
class User(UserMixin, db.Model):
# ... 现有代码 ...
def to_dict(self, include_email=False):
data = {
'id': self.id,
'username': self.username,
'created_at': self.created_at.isoformat(),
'is_active': self.is_active,
'posts_count': self.posts.count()
}
if include_email:
data['email'] = self.email
return data
class Post(db.Model):
# ... 现有代码 ...
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'content': self.content,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'published': self.published,
'author': {
'id': self.author.id,
'username': self.author.username
},
'category': {
'id': self.category.id,
'name': self.category.name
} if self.category else None,
'tags': [tag.name for tag in self.tags],
'comments_count': self.comments.count()
}
8.2 API 认证和限流
python
# app/auth.py
from flask import Blueprint, request, jsonify
from flask_login import login_user, logout_user, current_user
import jwt
from datetime import datetime, timedelta
from functools import wraps
auth_bp = Blueprint('auth', __name__)
def token_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'Token is missing'}), 401
try:
if token.startswith('Bearer '):
token = token[7:]
data = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)
current_user_id = data['user_id']
current_user = User.query.get(current_user_id)
if not current_user:
return jsonify({'error': 'Token is invalid'}), 401
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Token is invalid'}), 401
return f(current_user, *args, **kwargs)
return decorated_function
@auth_bp.route('/api/login', methods=['POST'])
def api_login():
data = request.get_json()
if not data or not data.get('username') or not data.get('password'):
return jsonify({'error': 'Missing credentials'}), 400
user = User.query.filter_by(username=data['username']).first()
if user and user.check_password(data['password']):
# 生成 JWT token
token = jwt.encode({
'user_id': user.id,
'exp': datetime.utcnow() + timedelta(hours=24)
}, current_app.config['SECRET_KEY'], algorithm='HS256')
return jsonify({
'token': token,
'user': user.to_dict(include_email=True),
'expires_in': 24 * 3600
})
return jsonify({'error': 'Invalid credentials'}), 401
# 使用 token 认证的 API
@api.route('/profile')
@token_required
def get_profile(current_user):
return jsonify(current_user.to_dict(include_email=True))
9. 蓝图 (Blueprints) 组织代码
9.1 创建蓝图
python
# app/main/__init__.py
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes
# app/main/routes.py
from flask import render_template, request
from app.main import bp
from app.models import Post, User
@bp.route('/')
def index():
posts = Post.query.filter_by(published=True)\
.order_by(Post.created_at.desc())\
.limit(10).all()
return render_template('index.html', posts=posts)
@bp.route('/about')
def about():
return render_template('about.html')
# app/auth/__init__.py
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import routes
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User, db
@bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('main.index')
return redirect(next_page)
flash('Invalid username or password')
return render_template('auth/login.html', title='Sign In', form=form)
@bp.route('/logout')
def logout():
logout_user()
return redirect(url_for('main.index'))
9.2 注册蓝图
python
# app/__init__.py
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
# 注册蓝图
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix='/api')
return app
10. 配置管理
10.1 环境配置
python
# config.py
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 邮件配置
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
# 上传文件配置
UPLOAD_FOLDER = os.path.join(basedir, 'uploads')
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
# 分页配置
POSTS_PER_PAGE = 10
USERS_PER_PAGE = 20
# Redis 配置
REDIS_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379/0'
# 日志配置
LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite://'
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
@classmethod
def init_app(cls, app):
Config.init_app(app)
# 错误邮件通知
import logging
from logging.handlers import SMTPHandler
if app.config['MAIL_SERVER']:
auth = None
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
secure = None
if app.config['MAIL_USE_TLS']:
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr='no-reply@' + app.config['MAIL_SERVER'],
toaddrs=app.config['ADMINS'],
subject='Application Failure',
credentials=auth,
secure=secure
)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
10.2 环境变量文件
bash
# .env
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://username:password@localhost/mydatabase
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=1
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
REDIS_URL=redis://localhost:6379/0
11. 部署和生产环境
11.1 WSGI 配置
python
# wsgi.py
import os
from app import create_app
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
if __name__ == "__main__":
app.run()
11.2 Gunicorn 配置
python
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 100
preload_app = True
11.3 Nginx 配置
nginx
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static {
alias /path/to/your/app/app/static;
expires 30d;
}
location /uploads {
alias /path/to/your/app/uploads;
expires 30d;
}
}
11.4 Docker 部署
dockerfile
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_APP=wsgi.py
ENV FLASK_ENV=production
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "wsgi:app"]
yaml
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
volumes:
- ./uploads:/app/uploads
db:
image: postgres:13
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:6-alpine
volumes:
postgres_data:
12. 测试
12.1 单元测试
python
# tests/test_models.py
import unittest
from app import create_app, db
from app.models import User, Post
class UserModelCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_password_hashing(self):
u = User(username='susan')
u.set_password('cat')
self.assertFalse(u.check_password('dog'))
self.assertTrue(u.check_password('cat'))
def test_user_creation(self):
u = User(username='john', email='john@example.com')
u.set_password('test')
db.session.add(u)
db.session.commit()
self.assertEqual(User.query.count(), 1)
self.assertEqual(u.username, 'john')
self.assertTrue(u.check_password('test'))
if __name__ == '__main__':
unittest.main(verbosity=2)
12.2 集成测试
python
# tests/test_routes.py
import unittest
from app import create_app, db
from app.models import User
class RoutesTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.client = self.app.test_client()
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_home_page(self):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
def test_login_logout(self):
# 创建测试用户
user = User(username='test', email='test@example.com')
user.set_password('test')
db.session.add(user)
db.session.commit()
# 测试登录
response = self.client.post('/auth/login', data={
'username': 'test',
'password': 'test'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
# 测试登出
response = self.client.get('/auth/logout', follow_redirects=True)
self.assertEqual(response.status_code, 200)
def test_api_users(self):
response = self.client.get('/api/users')
self.assertEqual(response.status_code, 200)
data = response.get_json()
self.assertIn('users', data)
13. 最佳实践
13.1 项目结构最佳实践
bash
my_flask_app/
├── app/
│ ├── __init__.py # 应用工厂
│ ├── models.py # 数据模型
│ ├── main/ # 主要功能蓝图
│ ├── auth/ # 认证蓝图
│ ├── api/ # API 蓝图
│ ├── templates/ # 模板文件
│ ├── static/ # 静态文件
│ └── utils/ # 工具函数
├── migrations/ # 数据库迁移
├── tests/ # 测试文件
├── config.py # 配置文件
├── requirements.txt # 依赖列表
├── .env # 环境变量
├── .gitignore # Git 忽略文件
└── run.py # 应用入口
13.2 安全最佳实践
python
# 1. 使用强密钥
app.config['SECRET_KEY'] = os.urandom(24)
# 2. 防止 SQL 注入 - 使用 ORM
# Bad
query = f"SELECT * FROM users WHERE id = {user_id}"
# Good
user = User.query.get(user_id)
# 3. 防止 XSS 攻击 - 自动转义
# Jinja2 默认启用自动转义
# 4. 防止 CSRF 攻击
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
# 5. 使用 HTTPS
@app.before_request
def force_https():
if not request.is_secure and app.env != 'development':
return redirect(request.url.replace('http://', 'https://'))
# 6. 设置安全头
@app.after_request
def set_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
13.3 性能优化
python
# 1. 数据库查询优化
# Bad
posts = Post.query.all()
for post in posts:
print(post.author.username) # N+1 查询问题
# Good
posts = Post.query.options(db.joinedload(Post.author)).all()
# 2. 分页
posts = Post.query.paginate(
page=page, per_page=20, error_out=False
)
# 3. 缓存
from flask_caching import Cache
cache = Cache(app)
@cache.cached(timeout=300)
def expensive_function():
return calculate_something()
# 4. 数据库连接池
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'pool_pre_ping': True,
'pool_recycle': 300,
}
Flask 是一个功能强大且灵活的 Web 框架,适合从简单的原型到复杂的企业级应用。通过合理的项目结构、安全实践和性能优化,可以构建出高质量的 Web 应用程序。
Similar code found with 6 license types