退出登录模块
功能概述
退出登录功能是系统安全性的重要组成部分,它确保用户能够安全地结束会话,防止未授权访问。
代码实现与讲解
# 退出登录
@manage_bp.route('/logout')
def logout():
# 重定向到登录页
response = make_response(redirect(url_for('manage.login')))
# 删除cookie
response.delete_cookie(Config.AUTH_COOKIE_NAME)
return response
代码详解:
路由定义:
使用
@manage_bp.route('/logout')
装饰器定义退出登录的路由这是一个GET请求路由,不需要处理表单数据
响应对象创建:
make_response(redirect(url_for('manage.login')))
创建一个重定向到登录页面的响应对象url_for('manage.login')
使用Flask的反向解析功能生成登录页面的URL
Cookie删除:
response.delete_cookie(Config.AUTH_COOKIE_NAME)
删除认证CookieConfig.AUTH_COOKIE_NAME
是在配置文件中定义的Cookie名称("lgsp_food")
返回响应:
返回处理后的响应对象,浏览器会重定向到登录页面并删除认证Cookie
安全考虑:
退出登录后立即删除认证Cookie,防止会话被滥用
重定向到登录页面,提供清晰的用户反馈
账号管理模块
功能概述
账号管理模块负责系统中用户账号的CRUD操作(创建、读取、更新、删除),包括列表展示、搜索、分页和编辑功能。
用户列表与分页
路由配置与数据处理
@manage_bp.route('/account/list')
def account_list():
# 默认分页起始从第一页开始
page = int(request.args.get('page', 1))
# 精简操作
query = User.query
# 默认状态
status_name = int(request.args.get('status', '-1'))
# 有值就取值
if status_name > -1:
query = query.filter(User.status == status_name)
# 姓名或者手机号验证,默认为空
mix_kw = request.args.get('mix_kw', '')
if mix_kw:
# 数据分页
rule = or_(User.nickname.contains('%s' % mix_kw), User.mobile.contains('%s' % mix_kw))
page_data = query.filter(rule).order_by(User.id.desc()).paginate(page=page, per_page=Config.PER_PAGE)
else:
page_data = query.order_by(User.id.desc()).paginate(page=page, per_page=Config.PER_PAGE)
# 将数据进行返回
resp_data = {
'list': page_data,
'status_mapping': constants.STATUS_MAPPING
}
# 显示页面,发送数据
return ops_render('account/index.html', resp_data)
代码详解:
获取分页参数:
page = int(request.args.get('page', 1))
从请求参数中获取页码,默认为第一页
构建查询:
query = User.query
初始化为所有用户的查询根据状态参数过滤:
query = query.filter(User.status == status_name)
关键字搜索:
获取搜索关键字:
mix_kw = request.args.get('mix_kw', '')
使用SQLAlchemy的
or_
函数实现多字段搜索:昵称或手机号包含关键字contains
方法实现模糊查询
分页处理:
使用SQLAlchemy的
paginate
方法进行分页per_page=Config.PER_PAGE
使用配置文件中定义的分页大小
准备响应数据:
分页数据:
page_data
状态映射:
constants.STATUS_MAPPING
(将状态码映射为可读文本)
渲染模板:
使用自定义的
ops_render
函数渲染模板并传递数据
分页宏实现
{% macro page_nav(page_data, obj_url) %}
<div class="pull-right">
<div>
{% if page_data %}
{% set status = request.args.get('status', -1) %}
{% set cat_id = request.args.get('cat_id', 0) %}
{% set mix_kw = request.args.get('mix_kw', '') %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm no-margin">
<li>
<a href="{{ url_for(obj_url, page=1) }}&status={{ status }}&cat_id={{ cat_id }}&mix_kw={{ mix_kw }}">首页</a>
</li>
{% if page_data.has_prev %}
<li>
<a href="{{ url_for(obj_url, page=page_data.prev_num) }}&status={{ status }}&cat_id={{ cat_id }}&mix_kw={{ mix_kw }}" aria-label="Previous">
<span aria-hidden="true">上一页</span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="javascript:;"
aria-label="Previous">
<span aria-hidden="true">上一页</span>
</a>
</li>
{% endif %}
{% for page_num in page_data.iter_pages() %}
{% set page_num = page_num|d('...', True) %}
{% if page_num == '...' %}
<li><a href="javascript:;">{{ page_num }}</a></li>
{% else %}
{% if page_num == page_data.page %}
<li class="active">
<a href="javascript:;">{{ page_num }}</a>
</li>
{% else %}
<li>
<a href="{{ url_for(obj_url, page=page_num) }}&status={{ status }}&cat_id={{ cat_id }}&mix_kw={{ mix_kw }}">{{ page_num }}</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
{% if page_data.has_next %}
<li>
<a href="{{ url_for(obj_url, page=page_data.next_num) }}&status={{ status }}&cat_id={{ cat_id }}&mix_kw={{ mix_kw }}"
aria-label="Next">
<span aria-hidden="true">下一页</span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="javascript:;"
aria-label="Next">
<span aria-hidden="true">下一页</span>
</a>
</li>
{% endif %}
<li>
<a href="{{ url_for(obj_url, page=page_data.pages) }}&status={{ status }}&cat_id={{ cat_id }}&mix_kw={{ mix_kw }}">尾页</a>
</li>
</ul>
</nav>
{% endif %}
</div>
</div>
{% endmacro %}
代码详解:
宏定义:
使用
{% macro %}
定义可重用的分页组件参数:
page_data
(分页数据对象)和obj_url
(路由端点名称)
保留查询参数:
获取当前的状态、分类ID和搜索关键字参数
在分页链接中保留这些参数,确保翻页后筛选条件不变
分页导航:
首页链接:直接跳转到第一页
上一页/下一页:根据当前页面状态启用或禁用
页码迭代:使用
page_data.iter_pages()
生成页码列表尾页链接:直接跳转到最后一页
样式处理:
当前页使用
active
类高亮显示禁用状态使用
disabled
类并移除链接功能
用户编辑和添加
路由配置与数据处理
# 用户编辑和添加
@manage_bp.route('/account/edit', methods=['GET', 'POST'])
def account_edit():
# 编辑或者是添加的页面
if request.method == 'GET':
u_id = int(request.args.get('id', 0))
user_obj = None
if u_id:
user_obj = User.query.get(u_id)
resp_data = {'info': user_obj}
return ops_render('account/set.html', resp_data)
if request.method == 'POST':
resp = {'code': 200, 'msg': '修改成功!', 'data': {}}
req = request.values
u_id = req['id'] if 'id' in req else 0
nickname = req['nickname'] if 'nickname' in req else ''
mobile = req['mobile'] if 'mobile' in req else ''
email = req['email'] if 'email' in req else ''
login_name = req['login_name'] if 'login_name' in req else ''
login_pwd = req['login_pwd'] if 'login_pwd' in req else ''
# 数据验证
if nickname is None or len(nickname) < 2:
resp['code'] = -1
resp['msg'] = '请输入符合规范的昵称!'
return jsonify(resp)
mobile_pattern = re.compile(r'^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$')
if mobile is None or not mobile_pattern.match(mobile):
resp['code'] = -1
resp['msg'] = '请输入有效的手机号!'
return jsonify(resp)
if email is None or len(email) < 1:
resp['code'] = -1
resp['msg'] = 'Email不能为空!'
return jsonify(resp)
email_pattern = re.compile(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$')
if not email_pattern.match(email):
resp['code'] = -1
resp['msg'] = '请输入有效的Email地址!'
return jsonify(resp)
if login_name is None or len(login_name) < 2:
resp['code'] = -1
resp['msg'] = '请输入符合规范的用户名!'
return jsonify(resp)
if login_pwd is None or len(login_pwd) < 6:
resp['code'] = -1
resp['msg'] = '请输入符合规范的密码!'
return jsonify(resp)
# 检查用户名是否已存在
has_user = User.query.filter(User.login_name == login_name, User.id != u_id).first()
if has_user:
resp['code'] = -1
resp['msg'] = '该用户登录名已存在,请更换一个重新注册!'
return jsonify(resp)
# 获取或创建用户对象
user_obj = User.query.get(u_id)
if not user_obj:
resp['msg'] = '新增成功!'
user_obj = User()
# 更新用户信息
user_obj.nickname = nickname
user_obj.mobile = mobile
user_obj.email = email
user_obj.login_name = login_name
# 密码加密处理
login_salt = random_salt()
user_obj.login_salt = login_salt
user_obj.login_pwd = gene_pwd(login_pwd, login_salt)
# 保存到数据库
db.session.add(user_obj)
db.session.commit()
return jsonify(resp)
代码详解:
双方法路由:
支持GET和POST两种HTTP方法
GET:显示编辑/添加页面
POST:处理表单提交
GET请求处理:
从请求参数获取用户ID
根据ID查询用户信息(编辑时)或创建空对象(添加时)
渲染编辑页面并传递用户数据
POST请求处理:
初始化响应对象
获取并验证表单数据
检查用户名是否已存在(排除当前编辑的用户)
根据ID判断是更新还是新增用户
对密码进行加密处理
保存到数据库并返回操作结果
数据验证:
昵称:不能为空且长度至少2字符
手机号:使用正则表达式验证格式
邮箱:不能为空且格式正确
用户名:不能为空且长度至少2字符
密码:不能为空且长度至少6字符
工具方法
随机盐生成
import random
import string
def random_salt(length=9):
"""生成随机盐值"""
return ''.join(random.sample(string.ascii_letters + string.digits, length))
代码详解:
从字母和数字中随机选择字符生成指定长度的字符串
默认长度为9个字符
用于密码加密的盐值,增加密码安全性
密码加密
import hashlib
import base64
def gene_pwd(pwd, salt):
"""加密密码(MD5+salt)"""
m = hashlib.md5()
_str = "%s--%s" % (base64.encodebytes(pwd.encode('utf-8')), salt)
m.update(_str.encode('utf-8'))
return m.hexdigest()
代码详解:
使用base64编码密码字符串
将编码后的密码与盐值组合
使用MD5算法对组合字符串进行哈希计算
返回十六进制格式的哈希值作为加密后的密码
安全考虑:
使用盐值防止彩虹表攻击
MD5算法虽然不再是最安全的选项,但结合盐值仍提供基本的安全性
在实际生产环境中,可以考虑使用更安全的算法如bcrypt
总结
Day04的内容主要实现了退出登录和账号管理模块:
退出登录:
删除认证Cookie
重定向到登录页面
账号管理:
用户列表展示与分页
搜索和筛选功能
可重用的分页组件
用户编辑和添加功能
完整的数据验证机制
安全特性:
密码加盐加密存储
输入数据验证和过滤
防止用户名重复
这些功能共同构成了一个完整的用户管理系统,为后台管理提供了必要的账号管理能力。