🐍 Python Day55:用户认证 — 保护你的应用
🕐 预计用时:2-3 小时 | 🎯 目标:掌握密码哈希、Flask-Login 和完整的用户认证流程
📖 今日目录
1. 用户认证概述
🔐 两个核心概念
| | |
|---|
| 认证 Authentication | | |
| 授权 Authorization | | |
❌ 为什么不能存明文密码?
# ❌ 绝对不要这样做!
# 数据库中直接存储明文密码
username: admin
password: 123456 ← 黑客拿到数据库就全完了
# ✅ 正确做法:存储密码的哈希值
username: admin
password: pbkdf2:sha256:260000$abc...$def... ← 黑客拿到也看不懂
⚠️ 真实案例:
2011年索尼 PlayStation Network 被入侵,7700万用户数据泄露——其中包括明文密码。索尼被罚款 2.5 亿美元。
如果他们用了密码哈希,即使数据库泄露,攻击者也无法还原出原始密码。
2. 密码哈希
🔧 werkzeug.security 工具
Flask 推荐使用 werkzeug 库的密码哈希工具(它是 Flask 的依赖,无需额外安装):
from werkzeug.security import generate_password_hash, check_password_hash
# 注册时:哈希密码后存入数据库
password = 'my_secure_password'
hashed = generate_password_hash(password)
print(hashed)
# 输出: pbkdf2:sha256:260000$salt$hash_value
# 登录时:验证密码
is_correct = check_password_hash(hashed, 'my_secure_password')
print(is_correct) # True
is_wrong = check_password_hash(hashed, 'wrong_password')
print(is_wrong) # False
📋 在 User 模型中使用
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
__tablename__ = 'users'
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(256), nullable=False) # 存哈希,不存明文
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)
# 使用
user = User(username='alice', email='alice@example.com')
user.set_password('my_secure_password') # 哈希后存储
db.session.add(user)
db.session.commit()
# 登录验证
if user.check_password('my_secure_password'):
print('密码正确!')
else:
print('密码错误!')
3. 哈希原理图解
🧂 为什么需要"盐"(Salt)?
# 没有盐的哈希——相同密码 = 相同哈希(危险!)
hash("123456") → "e10adc3949ba59abbe56e057f20f883e"
hash("123456") → "e10adc3949ba59abbe56e057f20f883e" # 完全一样!
# 黑客可以用彩虹表(预先计算的哈希对照表)反查密码
# 有盐的哈希——相同密码 = 不同哈希(安全!)
hash("123456", salt="a1b2c3") → "x9k2m4..."
hash("123456", salt="d4e5f6") → "p7q8r9..." # 不一样!
# 每个用户有独立的随机盐,彩虹表失效
📐 哈希算法选择
💡 为什么哈希要"慢"?
密码哈希故意设计得很慢(PBKDF2 默认 260000 次迭代)。
用户登录验证一次只需要 0.3 秒,感觉不到。但黑客要暴力破解 100 万个密码?
0.3秒 × 1,000,000 = 83 小时——太慢了,不划算!
速度是防御暴力破解的核心武器。
4. Flask-Login 安装与配置
📥 安装
pip install flask-login
⚙️ 初始化
# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
app = Flask(__name__)
app.secret_key = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
# 初始化 LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login' # 未登录时重定向到的页面
login_manager.login_message = '请先登录!' # 提示信息
login_manager.login_message_category = 'warning' # 消息类别
5. User 模型实现
👤 UserMixin 四个必备属性
Flask-Login 要求 User 模型实现 4 个属性/方法,UserMixin 帮你全部搞定:
from flask_login import UserMixin
class User(UserMixin, db.Model):
__tablename__ = 'users'
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(256), nullable=False)
role = db.Column(db.String(20), default='user') # user/admin
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 __repr__(self):
return f'<User {self.username}>'
📋 UserMixin 提供的属性
| | |
|---|
is_authenticated | | |
is_active | | |
is_anonymous | | |
get_id() | | |
🔧 user_loader 回调
@login_manager.user_loader
def load_user(user_id):
"""Flask-Login 用这个函数从数据库加载用户"""
return User.query.get(int(user_id))
💡 user_loader 是什么?
Flask-Login 把用户 ID 存在 Cookie 里。每次请求时,它会调用 user_loader 把 ID 转换成 User 对象。
类比:Cookie 是"工号",user_loader 是"人事系统"——拿着工号去查员工档案。
6. 登录流程
🔄 完整登录流程图
用户填写登录表单 → POST /login
↓
验证 CSRF token
↓
表单验证(WTForms)
↓
查询用户(User.query.filter_by)
↓
验证密码(user.check_password)
↓
密码正确? ─── 否 → 返回错误信息
↓ 是
login_user(user) ← Flask-Login 创建会话
↓
重定向到首页(或 next 参数指定的页面)
🔑 核心函数
from flask_login import login_user, logout_user, login_required, current_user
# 登录:创建用户会话
login_user(user) # 基本登录
login_user(user, remember=True) # 记住我(365天有效)
login_user(user, duration=timedelta(hours=2)) # 自定义有效期
# 登出:销毁会话
logout_user()
# 装饰器:保护需要登录的页面
@app.route('/dashboard')
@login_required # 未登录用户会被重定向到 login 页面
def dashboard():
return '这是需要登录才能看到的页面'
7. 登录视图与模板
🖥️ 登录视图
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required
@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 is None or not user.check_password(form.password.data):
flash('用户名或密码错误', 'error')
return redirect(url_for('login'))
# 登录成功
login_user(user, remember=form.remember.data)
flash(f'欢迎回来,{user.username}!', 'success')
# 跳转到登录前的页面(如果有的话)
next_page = request.args.get('next')
return redirect(next_page or url_for('index'))
return render_template('login.html', form=form)
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('已安全退出', 'info')
return redirect(url_for('index'))
🎨 登录模板
<!-- templates/login.html -->
{% extends "base.html" %}
{% block content %}
<h2>🔑 用户登录</h2>
{# 显示闪现消息 #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}<br>
{{ form.username(class="input", placeholder="请输入用户名") }}
{% for e in form.username.errors %}
<span class="error">{{ e }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label }}<br>
{{ form.password(class="input", placeholder="请输入密码") }}
{% for e in form.password.errors %}
<span class="error">{{ e }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.remember() }} {{ form.remember.label }}
</div>
{{ form.submit(class="btn") }}
<p>还没有账号?<a href="{{ url_for('register') }}">立即注册</a></p>
</form>
{% endblock %}
8. 注册流程
📝 注册视图
@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():
# 检查用户名是否已存在
if User.query.filter_by(username=form.username.data).first():
flash('该用户名已被注册', 'error')
return redirect(url_for('register'))
# 检查邮箱是否已存在
if User.query.filter_by(email=form.email.data).first():
flash('该邮箱已被注册', 'error')
return redirect(url_for('register'))
# 创建用户
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data) # 哈希密码
db.session.add(user)
db.session.commit()
# 自动登录
login_user(user)
flash('注册成功!欢迎加入!', 'success')
return redirect(url_for('index'))
return render_template('register.html', form=form)
9. 用户权限与 current_user
👤 current_user 对象
from flask_login import current_user
@app.route('/profile')
@login_required
def profile():
# current_user 就是当前登录的 User 对象
return render_template('profile.html',
username=current_user.username,
email=current_user.email,
role=current_user.role
)
👑 角色权限管理
from functools import wraps
def admin_required(f):
"""自定义装饰器:要求管理员权限"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != 'admin':
flash('需要管理员权限', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
# 使用
@app.route('/admin')
@login_required
@admin_required
def admin_panel():
return '管理后台'
# 在模板中根据角色显示不同内容
<nav>
<a href="/">首页</a>
{% if current_user.is_authenticated %}
<a href="/profile">{{ current_user.username }}</a>
{% if current_user.role == 'admin' %}
<a href="/admin">管理后台</a>
{% endif %}
<a href="/logout">退出</a>
{% else %}
<a href="/login">登录</a>
<a href="/register">注册</a>
{% endif %}
</nav>
10. 会话管理
🍪 remember_me
# 登录时选择"记住我"
login_user(user, remember=True)
# Flask-Login 会在浏览器设置一个长期 Cookie(默认 365 天)
# 用户关闭浏览器后再次访问,仍然保持登录状态
# 配置 remember Cookie 有效期
app.config['REMEMBER_COOKIE_DURATION'] = timedelta(days=30) # 30天
🔒 Session 安全配置
# app.py 安全配置
app.secret_key = 'a-very-long-random-secret-key-at-least-32-chars'
# Session 有效期
app.permanent_session_lifetime = timedelta(hours=2) # 2小时后过期
# Cookie 安全设置
app.config['SESSION_COOKIE_HTTPONLY'] = True # JS 无法读取 Cookie
app.config['SESSION_COOKIE_SECURE'] = True # 仅 HTTPS 传输(生产环境)
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # 防 CSRF
💡 session vs login_user:
• session:Flask 原生会话,存在 Cookie 中,可以存任意数据
• login_user:Flask-Login 封装,专门处理用户认证,底层用 session
两者不要混用——用户相关的用 login_user/current_user,临时数据用 session。
11. 完整认证示例
把今天学的所有内容整合成一个完整的迷你应用:
📁 项目结构
auth-app/
├── app.py # 主应用
├── models.py # 数据模型
├── forms.py # 表单
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── login.html
│ ├── register.html
│ └── profile.html
└── static/
└── style.css
📄 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()
class User(UserMixin, db.Model):
__tablename__ = 'users'
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(256), nullable=False)
role = db.Column(db.String(20), default='user')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
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 __repr__(self):
return f'<User {self.username}>'
📄 forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo
class LoginForm(FlaskForm):
username = StringField('用户名', validators=[DataRequired()])
password = PasswordField('密码', validators=[DataRequired()])
remember = BooleanField('记住我')
submit = SubmitField('登录')
class RegistrationForm(FlaskForm):
username = StringField('用户名', validators=[
DataRequired(), Length(min=3, max=20)
])
email = StringField('邮箱', validators=[DataRequired(), Email()])
password = PasswordField('密码', validators=[
DataRequired(), Length(min=8)
])
confirm_password = PasswordField('确认密码', validators=[
DataRequired(), EqualTo('password', message='密码不一致')
])
submit = SubmitField('注册')
📄 app.py
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from models import db, User
from forms import LoginForm, RegistrationForm
app = Flask(__name__)
app.secret_key = 'change-this-to-a-random-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///auth.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route('/')
def index():
return render_template('index.html')
@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()
login_user(user)
flash('注册成功!', 'success')
return redirect(url_for('index'))
return render_template('register.html', form=form)
@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.data)
return redirect(request.args.get('next') or url_for('index'))
flash('用户名或密码错误', 'error')
return render_template('login.html', form=form)
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('已安全退出', 'info')
return redirect(url_for('index'))
@app.route('/profile')
@login_required
def profile():
return render_template('profile.html')
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
12. 今日练习
🏋️ 练习 1:完整认证系统
# 基于上面的完整示例,完成以下功能:
# 1. 注册时检查用户名和邮箱是否已存在
# 2. 登录失败 3 次后锁定账号 5 分钟
# 3. 个人资料页面显示注册时间和基本信息
# 4. 退出登录后跳转到首页并显示提示
🏋️ 练习 2:修改密码
# 创建修改密码功能:
# - 表单:旧密码、新密码、确认新密码
# - 验证旧密码是否正确
# - 新密码不能和旧密码相同
# - 修改成功后强制重新登录
🏋️ 练习 3:权限控制
# 实现角色权限:
# - 普通用户:只能看自己的资料
# - 管理员:可以看所有用户列表
# - 创建一个 admin 账号初始化脚本
# - 管理员可以禁用/启用用户账号
13. 今日小结
| |
|---|
| generate_password_hash / check_password_hash,永远不存明文密码 |
| |
| LoginManager + UserMixin + login_user/logout_user |
| 从 Cookie 中的用户 ID 加载 User 对象 |
| 表单验证 → 查用户 → 验密码 → login_user → 重定向 |
| 查重 → 创建用户 → set_password → 自动登录 |
| current_user + @login_required + 自定义装饰器 |
| remember_me + session 配置 + Cookie 安全 |
🚀 Flask 基础阶段完结!
Day 51-55 你已经掌握了 Flask 的核心:
✅Day 51:路由、视图、模板、Debug
✅Day 52:模板继承、宏、过滤器、静态文件
✅Day 53:WTForms 表单处理、CSRF、文件上传
✅Day 54:Flask-SQLAlchemy ORM、数据库迁移
✅Day 55:用户认证、密码哈希、Flask-Login
接下来 Day 56 会学习RESTful API——让你的 Flask 应用不仅能渲染网页,还能作为后端 API 给前端和移动端提供数据!