今天的主要任务是为 Flask 项目添加邮箱验证功能,用户在注册账户后,系统会自动发送验证邮件,用户点击邮件中的链接后,即可完成账户验证。
-
注册用户后发送邮箱验证邮件
- 用户在
/register
注册后,系统会使用Flask-Mail
发送验证邮件。 - 生成令牌 (
token
),并将其附加到验证链接中。 - 用户点击邮件中的链接后,
/confirm-user/<token>
端点会校验token
,验证通过则激活账户。
- 用户在
-
令牌 (
token
) 机制- 采用
itsdangerous.Serializer
进行令牌加密,防止篡改。 token
包含用户 ID,并设置过期时间(如 1 小时)。- 通过
confirm_user(token)
方法进行验证。
- 采用
-
邮件模板
- 使用 Jinja2 模板引擎创建 HTML + 纯文本邮件模板(存放在
templates/email
)。 - 确保邮件正文支持 HTML 和 纯文本(适配不同客户端)。
- 使用 Jinja2 模板引擎创建 HTML + 纯文本邮件模板(存放在
-
用户未验证时限制部分操作
- 未验证用户只能进行有限操作,如不能创建内容。
- 访问受限页面时,会被重定向到
/unconfirmed_user
页面,并提示用户需要完成邮箱验证。
-
Flask-Mail 发送邮件
- 通过 SMTP 服务器发送邮件(Gmail、QQ、Outlook 等)。
- 采用多线程处理邮件发送,以避免影响主进程响应请求。
def send_async_email(app, msg): with app.app_context(): Mail(app).send(msg)
-
itsdangerous 令牌管理
- 令牌 (
token
) 采用SECRET_KEY
进行加密。 generate_confirm_user_token()
生成令牌。confirm_user(token)
解析令牌并验证用户身份。
def generate_confirm_user_token(self): return self.serializer.dumps({'confirm_user': self.id})
- 令牌 (
-
邮件模板
- HTML 模板 (
confirm_user.html
) - 纯文本模板 (
confirm_user.txt
) - 通过
render_template()
生成邮件内容。
<a href="{{ url_for('front.confirm_user', token=token, _external=True) }}">点击这里验证邮箱</a>
- HTML 模板 (
问题 | 原因 | 解决方案 |
---|---|---|
KeyError: 'mail' |
Mail(app) 没有初始化 |
在 app.py 的 register_extensions(app) 里初始化 Mail(app) |
SMTP Authentication Required |
没有启用 Gmail 应用专用密码 | 生成 Gmail 专用密码,或改用 QQ/Outlook 邮箱 |
ModuleNotFoundError: No module named 'email_validator' |
缺少 email_validator 依赖 |
pip install email_validator 重新安装 |
-
SMTP 服务器登录失败
- 仍然无法成功通过 SMTP 发送邮件,Gmail、QQ 和 Outlook 账号均遇到认证问题:
- Gmail: "5.7.0 Authentication Required"
- QQ: "Error: need EHLO and AUTH first"
- 计划尝试:
- 检查 Gmail 应用专用密码
- 确保 QQ SMTP 配置正确
- 测试 Mailtrap 进行调试
- 仍然无法成功通过 SMTP 发送邮件,Gmail、QQ 和 Outlook 账号均遇到认证问题:
-
完善用户体验
- 添加更换邮箱功能。
- 用户登录时,如果账户未验证,提供按钮"重新发送验证邮件"。
今天的开发任务主要围绕用户邮箱验证进行,成功实现了用户注册后发送验证邮件的功能,并且利用 itsdangerous
进行 token
令牌管理。尽管仍然面临 SMTP 认证失败的问题,但已确定了进一步排查方案。
今天的主要任务是完善用户权限管理系统,包括角色权限的初始化、权限检查功能的优化,以及添加了更直观的权限检查输出信息。
-
角色权限系统的完善
- 实现了角色的自动初始化功能
- 完善了权限检查机制
- 添加了详细的权限检查输出信息
-
权限检查方法的优化
- 改进了
is_administrator
属性 - 改进了
is_moderator
属性 - 优化了
has_permission
方法 - 添加了详细的权限信息输出
- 改进了
-
权限系统的可视化输出
- 添加了用户角色信息的打印
- 显示具体的权限值
- 提供了权限检查的详细结果
-
位运算权限管理
class Permission: FOLLOW = 1 # 00000001 COMMENT = 2 # 00000010 WRITE = 4 # 00000100 MODERATE = 8 # 00001000 ADMINISTER = 128 # 10000000
-
权限检查方法
@property def is_administrator(self): result = self.role.permissions & Permission.ADMINISTER print(f"用户 {self.name}:") print(f"- 当前角色: {self.role.name}") print(f"- 管理员权限: {'是' if result else '否'} (权限值: {result})") return result
-
通用权限检查
def has_permission(self, permission): result = self.role.permissions & permission permission_name = { Permission.FOLLOW: "关注", Permission.COMMENT: "评论", Permission.WRITE: "写作", Permission.MODERATE: "管理", Permission.ADMINISTER: "超级管理" }.get(permission, "未知权限") # ... 输出详细信息 ... return result
问题 | 原因 | 解决方案 |
---|---|---|
TimedJSONWebSignatureSerializer 导入错误 |
itsdangerous 版本兼容性问题 | 改用 URLSafeSerializer |
权限值显示不直观 | 缺少详细输出信息 | 添加了格式化的权限信息打印 |
expires_in 参数被移除
今天主要完成了用户权限系统的优化工作,通过添加详细的权限检查输出信息,使系统的权限管理更加直观和易于调试。虽然遇到了一些拼写错误和版本兼容性的问题,但都得到了及时解决。后续还需要继续完善权限管理的用户界面,提升整体用户体验。
今天的开发任务是实现用户个人主页的渲染,包括用户和管理员编辑信息的功能,以及相应的视图函数、表单类和前端模板。
- 完善 User 映射类
- 添加了
age
,gender
,phone_number
,location
,about_me
属性 - 其中
gender
是枚举类型的数据,定义了一个特别的 Gender 类,继承自 enum.Enum 类
- 添加了
- 展示用户最近活动时间
- 在主页模板中添加了用户最近活动时间的展示
- 定义了一个新方法 ping(),每次登录时 @front.before_app_request 装饰器会调用它
- 该方法会更新用户的 last_seen 属性
self.last_seen = datetime.utcnow()
- 创建了 User 蓝图
- 实现了用户主页的展示功能,用户可以通过
/user/<name>/index
路由访问自己的主页。 - 提供了用户编辑个人信息的功能,用户可以通过
/user/edit-profile
路由修改自己的信息。 - 实现了管理员编辑用户信息的功能,管理员可以通过
/user/admin-profile/<int:id>
路由修改任意用户的信息。 - 使用了权限控制,确保只有登录用户才能访问编辑功能,并且只有管理员才能访问管理员编辑功能。
- 实现了用户主页的展示功能,用户可以通过
- 创建了 ProfileForm 和 AdminProfileForm 表单类
- 继承自 Flask-WTF 的 Form 类
- 定义了用户和管理员编辑信息的表单
- 表单类中定义了性别选择框,使用 Flask-Bootstrap 的 Form.RadioItem 类来生成单选按钮
- 定义了新方法 validate_name() 和 validate_phone_number() 来验证用户名和手机号是否已存在
- 创建了 decorators.py 文件
- 定义了 admin_required 装饰器,用于检查用户是否具有管理员权限
- 定义了 permission_required 装饰器,用于检查用户是否具有特定权限
问题 | 原因 | 解决方案 |
---|---|---|
AttributeError: 'User' object has no attribute 'role' |
缺少 role 属性 | 修改权限检查逻辑为 self.role.permissions & permission 而非 self.role(permission) == permission |
蓝图注册错误 | 蓝图注册错误 | 蓝图注册地点应该为 handlers/__init__.py 文件中 |
表单验证错误 | 表单验证错误 | 修改表单前 user 的 confirmed 属性应为 True |
今天的主要任务是实现用户头像功能,包括添加 Gravatar 头像支持、注册流程集成和用户界面展示。
-
添加 Gravatar 头像支持
- 在 User 模型中添加
avatar_hash
字段,用于存储头像 URL - 实现
gravatar()
方法,生成基于邮箱的默认头像
def gravatar(self, size=256, default='identicon', rating='g'): url = 'https://www.gravatar.com/avatar' hash = self.avatar_hash or hashlib.md5(self.email.encode()).hexdigest() return '{url}/{hash}?s={size}&d={default}&r={rating}'.format( url=url, hash=hash, size=size, default=default, rating=rating)
- 默认使用 identicon 风格,生成独特的几何图案作为头像
- 支持自定义头像大小、默认样式和评级
- 在 User 模型中添加
-
注册流程集成
- 在用户注册时自动生成并保存头像 URL
def create_user(self): user = User() self.populate_obj(user) user.avatar_hash = user.gravatar() # 生成并保存头像URL db.session.add(user) db.session.commit()
-
用户界面展示
- 在用户主页添加头像显示
<div class="col-md-3"> <img class="img-rounded profile-thumbnail" src="{% if user.avatar_hash %} {{ user.avatar_hash }} {% else %} {{ user.gravatar(size=256) }} {% endif %}" /> </div>
- 支持头像回退机制:如果
avatar_hash
为空,则实时生成头像
- 使用 Gravatar 服务作为头像提供方
- 通过 MD5 哈希用户邮箱来生成唯一的头像标识
- 支持的参数:
- size: 头像大小(默认 256px)
- default: 默认头像样式(使用 identicon)
- rating: 内容分级(使用 G 级)
问题 | 原因 | 解决方案 |
---|---|---|
数据库字段错误 | 模型类和数据库表字段名不一致 | 统一使用 create_at 作为字段名 |
角色分配失败 | 数据库中没有初始化角色数据 | 需要先调用 Role.insert_roles() 初始化角色 |
今天主要完成了用户账户管理的核心功能,包括密码修改、忘记密码时的重置功能,以及邮箱更换功能。重点是通过邮件验证来确保这些敏感操作的安全性。
-
密码管理功能
- 已登录用户修改密码(需验证旧密码)
- 忘记密码时通过邮箱重置
- 重置密码时的邮件验证机制
-
邮箱验证系统
- 复用已有的令牌验证机制
- 统一的邮件发送流程
- HTML/纯文本双格式邮件模板
-
表单验证机制
class ChangePasswordForm(FlaskForm): old_password = PasswordField('旧密码', validators=[DataRequired()]) password = PasswordField('新密码', validators=[ DataRequired(), Length(3, 22), NotEqualTo('old_password', message='新密码不能与旧密码相同') ])
-
令牌验证复用
- 复用
generate_confirm_user_token()
生成重置密码令牌 - 复用
confirm_user()
验证令牌有效性 - 统一的安全验证机制
- 复用
-
视图函数的双重请求处理
@user.route('/reset-password/<name>/<token>', methods=['GET', 'POST']) def reset_password(name, token): # 先验证用户身份和令牌 if user and user.confirm_user(token): form = ResetPasswordForm() # GET 请求:显示表单页面 if request.method == 'GET': return render_template('template.html', form=form) # POST 请求:处理表单提交 if form.validate_on_submit(): # 更新数据库 return redirect(url_for('success_page'))
-
重置密码链接无效
- 问题:使用了错误的令牌验证方法名
- 解决:将
confirm_user_token
改为confirm_user
-
邮件模板语法错误
- 问题:URL 生成函数中多余的单引号
- 解决:修正模板中的语法错误
- 所有密码相关操作都需要邮件验证
- 重置密码链接具有时效性
- 令牌包含用户ID,确保操作针对特定用户
今天主要完成了博客系统的核心功能开发,包括博客的发布、展示、编辑功能,以及分页显示和 Markdown 支持。
-
博客数据模型
- 创建 Blog 表,包含博客内容、HTML 内容、时间戳等字段
- 实现与 User 表的一对多关联
class Blog(db.Model): '''博客映射类''' id = db.Column(db.Integer, primary_key=True) body = db.Column(db.Text) body_html = db.Column(db.Text) time_stamp = db.Column(db.DateTime, index=True, default=datetime.now) author_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) # 当用户被删除时,删除该用户发表的博客 # 建立与 User 模型的关系 author = db.relationship('User', backref=db.backref('blogs', lazy='dynamic', cascade='all, delete-orphan')) # 建立与 User 模型的关系 # relationship() 提供了访问关联对象的方式,而不是直接使用外键 # backref 会在 User 模型中添加 blogs 属性,方便反向查询 # lazy='dynamic' 使得查询延迟执行,返回查询对象而不是结果列表 # cascade='all, delete-orphan' 设置级联行为: # - all: 所有操作都级联(包括 save-update, merge, refresh-expire, expunge, delete) # - delete-orphan: 当记录与父对象解除关联时,自动删除这条记录 author = db.relationship('User', backref=db.backref('blogs', lazy='dynamic', cascade='all, delete-orphan'))
-
博客发布功能
- 实现博客发布表单
- 支持 Markdown 编辑和预览
- 自动转换 Markdown 为 HTML
class BlogForm(FlaskForm): '''博客表单类''' # 这里使用 Flask-PageDown 提供的字段类,以支持 Markdown 编辑 # 前端再设置一下预览,就可以在输入框输入 Markdown 语句并显示在页面上 body = PageDownField('博客内容', validators=[DataRequired()]) submit = SubmitField('提交')
-
博客展示系统
- 创建博客列表模板
- 实现分页功能
- 支持时间本地化显示
weblog/templates/_blogs.html
-
博客编辑功能
- 作者编辑权限控制
- 管理员编辑权限
- Markdown 实时预览
@user.route('/edit-blog/<int:id>', methods=['GET', 'POST']) @login_required def edit_blog(id): '''编辑博客''' blog = Blog.query.get_or_404(id) if current_user != blog.author and not current_user.is_administrator(): abort(403) form = BlogForm(obj=blog) if form.validate_on_submit(): form.populate_obj(blog) db.session.add(blog) db.session.commit() flash('博客已更新', 'success') return redirect(url_for('front.blog', id=blog.id)) return render_template('user/edit_blog.html', form=form)
-
分页查询实现
pagination = Blog.query.order_by(Blog.time_stamp.desc()).paginate( page=page, per_page=current_app.config['BLOGS_PER_PAGE'], error_out=False )
-
Markdown 支持
- 使用 Flask-PageDown 提供编辑器界面
- 使用 Markdown 库进行格式转换
- 使用 Bleach 清理 HTML,防止 XSS 攻击
-
权限控制
if current_user != blog.author and not current_user.is_administrator(): abort(403)
问题 | 原因 | 解决方案 |
---|---|---|
编辑链接格式错误 | URL 生成模板中多余空格 | 修正模板语法,移除多余空格 |
头像大小不一致 | CSS 样式未统一 | 统一设置头像大小为 32px |
- 添加博客评论功能
- 实现博客分类和标签
- 添加博客搜索功能
- 优化编辑器界面
- 实现图片上传功能
- Flask-PageDown
- Markdown
- Bleach