⚠️ 免责声明 :本文项目仅供技术学习参考,严禁直接作为毕业设计或课程作业提交。请结合自身需求进行二次开发与创新,学术诚信从我做起 🙂
这模块我翻过车
登录注册这模块我第一版写得稀烂,密码明文存的,被导师一眼盯上------这次彻底重写。
做不好,答辩老师上来第一印象就差。
登录注册这条线,我一次讲完:密码怎么加密、前后端双重校验怎么做、Session 登录态怎么管、注册流程怎么防刷。
这几个点过一遍,你的登录注册模块基本就齐了。
技术选型
| 功能 | 技术方案 | 说明 |
|---|---|---|
| 后端框架 | Flask | 轻量、灵活 |
| 登录管理 | Flask-Login | 官方推荐,成熟稳定 |
| 密码加密 | Werkzeug generate_password_hash | 自带PBKDF2算法,不用自己造轮子 |
| 会话管理 | Session(服务端) | Flask session 客户端签名,配合 Flask-Login |
| 表单校验 | WTForms + 前端 JS | 前后端各做一次 |
为什么选 Flask-Login 不用自己写?
因为自己写登录态管理,容易出各种安全问题------Cookie 伪造、会话劫持、CSRF 攻击。Flask-Login 都帮你处理好了,专注业务逻辑就行。
密码加密:绝对不能存明文
这是底线。
存明文密码的项目,答辩直接扣分,老师一句话就给你打回来:"你密码明文存的?"
加密方案
用 Werkzeug 自带的 generate_password_hash 和 check_password_hash。
python
from werkzeug.security import generate_password_hash, check_password_hash
# 注册时加密
password_hash = generate_password_hash(password)
# 登录时校验
if check_password_hash(user.password, password):
# 密码正确
底层用的是 PBKDF2-SHA256,加盐 + 多轮哈希,安全系数足够毕设用。
为什么不用 MD5 / SHA1?
因为已经被破解了------彩虹表几秒就能撞出来。
MD5 早在 2004 年就被王小云教授攻破了,现在还在用 MD5 存密码,等于裸奔。
SHA1 也不安全,2017 年 Google 就演示过碰撞攻击。
毕设最起码得上 PBKDF2 或 bcrypt。
我最早图快用了 MD5,被同学指出来才换,这步千万别省。
注册流程:三重校验
注册这一步,前端校验、后端校验、数据库唯一约束,三层都不能少。
注册页长这样------用户名、密码、手机号、验证码,输入框旁边直接标着校验规则(用户名3-20位、密码至少6位):

第一层:前端校验(用户体验)
用户还没点提交,就告诉他哪里错了,不用等服务器返回。
javascript
// 前端注册表单校验
function validateRegisterForm() {
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
var confirmPwd = document.getElementById('confirm_password').value;
var email = document.getElementById('email').value;
// 用户名长度
if (username.length < 3 || username.length > 20) {
alert('用户名长度在3-20个字符之间');
return false;
}
// 密码长度
if (password.length < 6) {
alert('密码至少6位');
return false;
}
// 两次密码一致
if (password !== confirmPwd) {
alert('两次输入的密码不一致');
return false;
}
// 邮箱格式
var emailReg = /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/;
if (!emailReg.test(email)) {
alert('邮箱格式不正确');
return false;
}
return true;
}
第二层:后端校验(安全防线)
前端校验是可以绕过的------用户禁用 JS、或者直接用 Postman 调接口,前端就失效了。
所以后端必须再校验一遍,这才是真防线。
python
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from .models import User
class RegisterForm(FlaskForm):
username = StringField('用户名', validators=[
DataRequired('用户名不能为空'),
Length(min=3, max=20, message='用户名长度3-20个字符')
])
password = PasswordField('密码', validators=[
DataRequired('密码不能为空'),
Length(min=6, message='密码至少6位')
])
confirm_password = PasswordField('确认密码', validators=[
DataRequired('请确认密码'),
EqualTo('password', message='两次密码不一致')
])
email = StringField('邮箱', validators=[
DataRequired('邮箱不能为空'),
Email('邮箱格式不正确')
])
submit = SubmitField('注册')
# 自定义校验:用户名不能重复
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('用户名已被注册')
# 自定义校验:邮箱不能重复
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('邮箱已被注册')
第三层:数据库唯一约束(最后兜底)
后端校验和数据库写入之间有时间差------并发注册时,两个请求可能都通过了"用户名未被注册"的校验,然后都去插入数据。
所以数据库层面必须加唯一索引,这是最后一道防线。
sql
-- 建表时加上
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_email` (`email`)
三层校验的关系:
- 前端:提升体验,减少无效请求
- 后端:主要防线,处理大部分情况
- 数据库:兜底,防止并发极端情况
登录流程:两种方式
登录页长这样------用户名、密码、图形验证码,三个输入项(验证码是防刷用的,后面细讲):

方式一:账号密码登录
python
from flask_login import login_user
from werkzeug.security import check_password_hash
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
# 1. 查用户
user = User.query.filter_by(username=username).first()
if not user:
flash('用户名或密码错误', 'error')
return render_template('login.html')
# 2. 检查账号状态
if user.status != 1:
flash('账号已被禁用,请联系管理员', 'error')
return render_template('login.html')
# 3. 校验密码
if not check_password_hash(user.password, password):
flash('用户名或密码错误', 'error')
return render_template('login.html')
# 4. 登录成功,写入 session
login_user(user, remember=True) # remember=True 会写记住我cookie
# 5. 更新最后登录时间
user.last_login = datetime.now()
db.session.commit()
return redirect(url_for('index'))
return render_template('login.html')
几个注意点:
-
错误信息统一说"用户名或密码错误"------别说"用户名不存在"或"密码不对",分开说等于告诉攻击者哪个是对的,方便他们暴力破解。
-
账号状态检查------用户被禁用了就不能登录,毕设里这个功能很加分,说明你考虑了后台管理。
-
密码比较用 check_password_hash------别自己解密对比,哈希是单向的,只能用内置方法验证。
方式二:手机号验证码登录(可选加分项)
如果毕设想做得更完善,可以加手机号+短信验证码登录。
接入阿里云 SMS 或腾讯云 SMS,几十行代码搞定。
流程:
- 用户输入手机号,点击"获取验证码"
- 后端生成6位随机验证码,调短信接口发送,同时存 Redis 或 Session,设置5分钟过期
- 用户输入验证码,后端校验,通过则登录
答辩时老师多问两句短信登录,你能顺着讲完整个流程,比只做账密的同学多一块谈资。
登录态管理:Flask-Login 怎么用
配置
python
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login' # 未登录时跳转到哪个页面
login_manager.login_message = '请先登录'
login_manager.login_message_category = 'warning'
# 用户加载回调------Flask-Login 需要知道怎么通过ID拿到用户对象
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
User 模型要实现的方法
User 模型需要继承 UserMixin,它已经帮你实现了 is_authenticated、is_active、is_anonymous、get_id 这四个方法。
python
from flask_login import UserMixin
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model, UserMixin):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
email = db.Column(db.String(100))
phone = db.Column(db.String(20))
nickname = db.Column(db.String(50))
avatar = db.Column(db.String(255))
status = db.Column(db.Integer, default=1)
created_at = db.Column(db.DateTime, default=datetime.now)
# 如果 status 字段控制账号状态,重写 is_active
@property
def is_active(self):
return self.status == 1
重写 is_active 方法,返回 status == 1------这样禁用的用户即使有密码也登不上。
在视图里判断登录状态
python
from flask_login import login_required, current_user
@app.route('/cart')
@login_required # 加了这个装饰器,未登录自动跳登录页
def cart():
# current_user 就是当前登录用户对象
user_id = current_user.id
username = current_user.username
# ...
模板里也能用:
html
{% if current_user.is_authenticated %}
<span>{{ current_user.nickname }}</span>
<a href="/logout">退出登录</a>
{% else %}
<a href="/login">登录</a>
<a href="/register">注册</a>
{% endif %}
退出登录
一句话的事:
python
from flask_login import logout_user
@app.route('/logout')
def logout():
logout_user()
flash('已退出登录', 'success')
return redirect(url_for('login'))
logout_user() 会自动清理 session 和 cookie。
注册防刷:验证码 + 频率限制
毕设虽然不用考虑线上安全,但加上这些功能,答辩能展示你有安全意识。
图形验证码
用 captcha 库生成图形验证码,注册时必须输入正确才能提交。
python
from captcha.image import ImageCaptcha
import random
import io
@app.route('/captcha')
def get_captcha():
# 生成4位随机验证码
code = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyz', k=4))
# 存到 session
session['captcha'] = code.lower()
# 生成图片
image = ImageCaptcha(width=120, height=40)
img_bytes = image.generate(code)
# 返回图片
return send_file(io.BytesIO(img_bytes.read()), mimetype='image/png')
注册接口里加校验:
python
captcha_input = request.form.get('captcha', '').lower()
if captcha_input != session.get('captcha'):
flash('验证码错误', 'error')
return render_template('register.html')
# 验证完清掉,防止重复使用
session.pop('captcha', None)
IP 频率限制
用 flask-limiter 限制同一个IP注册次数,比如一小时最多5次。
python
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=[]
)
@app.route('/register', methods=['POST'])
@limiter.limit("5 per hour") # 每小时最多5次
def register():
# ...
防刷这块毕设不强制,但加上去有好处。被问到安全措施时,你手里有验证码、有频率限制,不至于干瞪眼。
登录注册模块,老师爱问这些
1. 密码为什么不能明文存?
答:你想啊,库一漏,明文密码全摊在那儿,而且大家习惯一个密码走天下,连累一堆网站。加盐哈希之后呢,漏了也还原不回原文。
2. 前后端校验都要做吗?前端校验能不能省?
答:不能省前端校验(用户体验问题),但前端校验不是安全防线(可以绕过)。后端校验才是真正的安全防线,必须做。数据库唯一约束是兜底,防止并发极端情况。三层各司其职。
3. Session 和 Cookie 的区别?
答:Cookie 存在客户端浏览器,Session 存在服务端。Cookie 存的是 Session ID,通过 Session ID 去服务端取对应的用户信息。敏感信息不要存 Cookie,因为可以被篡改。Flask 的 session 是签名的客户端 Cookie,虽然不能改,但敏感信息还是建议存服务端。
4. 怎么防止暴力破解密码?
答:几个手段配合:密码错误次数限制(比如5次后锁定15分钟)、图形验证码、IP频率限制、强密码策略(长度+复杂度要求)。
5. Flask-Login 的 remember_me 是什么原理?
答:登录时传 remember=True,Flask-Login 会写一个长期有效的 remember cookie(默认365天),即使 session cookie 过期了,只要 remember cookie 还在,就能自动登录。底层存在 user 表的 remember_token 字段里。
登录注册看起来简单,其实细节很多。
密码加密、前后端校验、会话管理、防刷机制------每一块都有讲究。
登录注册该有的安全点基本都覆盖了------加密、双端校验、登录态、防刷,毕设这个深度够用了。
梅雅达编程笔记原创 · 首发 CSDN · 转载请注明出处