Python个人博客程序开发实例用户验证功能

目录
  • 1.安全存储密码
  • 2.使用Flask-Login管理用户认证
    • 2.1 获取当前用户
    • 2.2 登入用户
    • 2.3 登出用户
    • 2.4 视图保护
  • 3.使用CSRFProtect实现CSRF保护

在Python个人博客程序开发实例框架设计中,我们已经完成了 数据库设计、数据准备、模板架构、表单设计、视图函数设计、电子邮件支持 等总体设计的内容。

在Python个人博客程序开发实例信息显示中,我们一起实现了 显示文章列表、博客信息、文章内容和评论 等功能。

那么,本篇文章将会介绍如何 初始化博客、利用 Flask-Login 管理用户认证、使用 CSRFProtect 实现 CSRF 保护。

1.安全存储密码

创建管理员用户需要存储用户名和密码,密码的存储需要特别注意。密码不能直接以明文的形式存储在数据库中,因为一旦数据库被窃取或是被攻击者使用暴 力破解或字典法破解,用户的账户、密码将被直接泄露。如果发生泄漏,常常会导致用户在其他网站上的账户处于危险状态,因为通常用户会在多个网站使用同一个密码。一般的做法是不存储密码本身,而是存储通过密码生成的散列值(hash)。每一个密码对应着独一无二的散列值,从而避免明文存储密码。

如果只是简单地计算散列值,攻击者可以使用彩虹表的方式逆向破解密码。这时我们需要加盐计算散列值。加盐后,散列值的随机性会显著提高。但仅仅把盐和散列值连接在一起可能还不够,我们还需要使用 HMAC(hash-based message authentication code) 来重复计算很多次(比如 5000 次)最终获得派生密钥,这会增大攻击者暴 力破解密码的难度,这种方式被称为 密钥扩展(key stretching)。

在密码学中,盐(salt)是一串随机生成的字符,用来增加散列值计算的随机性。经过这一系列处理后,即使攻击者获取到了密码的散列值,也无法逆向获取真实的密码值。在生产环节中,尽管对密码加密存储安全性很强,仍然需要使用安全的 HTTP 以加密传输数据,避免密码在传输过程中被截获。

Werkzeug 在 security 模块中提供了一个 generate_password_hash(password,method=‘pbkdf2:sha256’,salt_length=8) 函数用于为给定的密码生成散列值,参数 method 用来指定计算散列值的方法,salt_length 参数用来指定盐(salt)的长度。security 模块中的 check_password_hash(pwhash,password) 函数接收散列值(pwhash)和密码(password)作为参数,用于检查密码散列值与密码是否对应。

>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> password_hash = generate_password_hash('cat')
>>> password_hash
'pbkdf2:sha256:50000$mIeMzTvb$ba3c0a274c6b53fda2ab39f864254dfb0a929848b7ec99f81e3bf721d8860fdc'
>>> check_password_hash(password_hash, 'dog')
False
>>> check_password_hash(password_hash, 'cat')
True
>>> password_hash = generate_password_hash('cat')
>>> password_hash
'pbkdf2:sha256:150000$AITKk6jv$5c0b732535cae83677fdf2e666153f82b5db30e6f40ec7a625678ad2b5f4ad25'

generate_password_hash() 函数生成的密码散列值的格式如下:

method$salt$hash

因为在计算散列值时会加盐,而盐是随机生成的,所以即使两个用户的密码相同,最终获得的密码散列值也是不同的。我们没法从密码散列值逆向获取密码,但是如果密码、计算方法、盐相同,最终获得的散列值结果也会是相同的,所以 check_password_hash() 函数会根据密码散列值中的方法、盐重新对传入的密码进行散列值计算,然后对比散列值。

from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
	...
	password_hash = db.Column(db.String(128))
	...
	def set_password(self, password):
		self.password_hash = generate_password_hash(password)
	def validate_password(self, password):
		return check_password_hash(self.password_hash, password)

set_password() 方法用来设置密码,它接收密码的原始值作为参数,将密码的散列值设为 password_hash 的值。validate_password() 方法用于验证密码是否和对应的散列值相符,返回布尔值。

2.使用Flask-Login管理用户认证

博客程序需要根据用户的身份开放不同的功能,对于程序使用者——管理员来说,他可以撰写文章、管理博客;而普通的用户(匿名用户)则只能阅读文章、发表评论。为了让程序识别出用户的身份,我们需要添加用户认证功能。具体来说,使用用户名和密码登入博客程序的用户被视为管理员,而未登录的用户则被视为匿名用户。

扩展 Flask-Login 为 Flask 提供了用户会话管理功能,使用它可以轻松的处理用户登录、登出等操作。

extensions.py 脚本中实例化扩展提供的 LoginManager 类,创建一个 login_managerlogin 对象。

from flask_login import LoginManager
...
login_manager = LoginManager(app)

然后在程序包的工厂函数中对 login 对象调用 init_app() 方法进行初始化扩展

login_manager.init_app(app)

Flask-Login 要求表示用户的类必须实现下表中所示的这几个属性和方法,以便用来判断用户的认证状态。

通过对用户对象调用各种方法和属性即可判断用户的状态,比如是否登录等。方便的做法是让用户类继承 Flask-Login 提供的 UserMixin 类,它包含了这些方法和属性的默认实现。

from flask_login import UserMixin
class Admin(db.Model, UserMixin):
	...

UserMinxin 表示通过认证的用户,所以 is_authenticatedis_active 属性会返回 True,而 is_anonymous 则返回 Falseget_id() 默认会查找用户对象的 id 属性值作为 id,而这正是我们的 Admin 类中的主键字段。

使用 Flask-Login 登入/登出某个用户非常简单,只需要在视图函数中调用 Flask-Login 提供的 login_user()logout_user() 函数,并传入要登入/登出的用户类对象。在这两个函数背后,Flask-Login 使用 Flask 的 session 对象将用户的 id 值存储到用户浏览器的 cookie 中(名为 user_id),这时表示用户被登入。相对来说,登出则意味着在用户浏览器的 cookie 中删除这个值。默认情况下,关闭浏览器时,通过 Flask 的 session 对象存储在客户端的 session cookie 会被删除,所以用户会登出。

另外,Flask-Login 还支持记住登录状态,通过在 login_user() 中将 remember 参数设为 True 即可实现。这时 Flask-Login 会在用户浏览器中创建一个名为 remember_tokencookie,当通过 session 设置的 user_id cookie 因为用户关闭浏览器而失效时,它会重新恢复 user_id cookie 的值。

为了防止破坏 Flask-Login 提供的认证功能,我们在视图函数中操作 session 时要避免使用 user_idremember_token 作为键。remember_token cookie 的默认过期时间为 365 天。你可以通过配置变量 REMEMBER_COOKIE_DURATION 进行设置,设为 datetime.timedelta 对象即可。

2.1 获取当前用户

那么我们如何判断用户的认证状态呢?答案是使用 Flask-Login 提供的 current_user 对象。它是一个和 current_app 类似的代理对象(Proxy),表示当前用户。调用时会返回与当前用户对应的用户模型类对象。因为 session 中只会存储登录用户的 id,所以为了让它返回对应的用户对象,我们还需要设置一个用户加载函数。这个函数需要使用 login_manager.user_loader 装饰器,它接收用户 id 作为参数,返回对应的用户对象。

@login_manager.user_loader
def load_user(user_id):
    from bluelog.models import Admin
    user = Admin.query.get(int(user_id))
    return user

现在,当我们调用 current_user 时,Flask-Login 会调用用户加载函数并返回对应的用户对象。如果当前用户已经登录,会返回 Admin 类实例;如果用户未登录,current_user 默认会返回 Flask-Login 内置的 AnonymousUserMixin 类对象,它的 is_authenticatedis_active 属性会返回 False,而 is_anonymous 属性则返回 True

current_user 存储在请求上下文堆栈上,所以只有激活请求上下文程序的情况下才可以使用,比如在视图函数中或是模板中调用。

最终,我们可以通过对 current_user 对象调用 is_authenticated 等属性来判断当前用户的认证状态。它也和我们自定义的模板全局变量一样注入到了模板上下文中,可以在所有模板中使用,所以我们可以在模板中根据用户状态渲染不同的内容

2.2 登入用户

个人博客的登录链接可以放在次要的位置,因为只有博客作者才会真正用到它。我们把它放到页脚,并根据用户的状态来选择渲染出不同的链接。

<small>
	{% if current_user.is_authenticated %}
	<!-- 如果用户已经登录,显示下面的“登出”链接-->
	<a href="{{ url_for('auth.logout', next=request.full_path) }}" rel="external nofollow" >Logout</a>
	{% else %}
	<!-- 如果没有登录,则显示下面的“登录”按钮 -->
	<a href="{{ url_for('auth.login', next=request.full_path) }}" rel="external nofollow" >Login</a>
	{% endif %}
</small>

通过 current_useris_authenticated 值判断用户是否登录,如果用户已登录(is_authenticatedTrue)就渲染注销按钮,否则就渲染登录按钮。按钮中的 URL 分别指向用于登录和登出的 loginlogout 视图,url_for() 函数中加入的 next 参数用来存储当前页面的路径,以便在执行登录或登出操作后将用户重定向回上一个页面。

from flask_login import login_user
from bluelog.forms import LoginForm
from bluelog.models import Admin
from bluelog.utils import redirect_back
...
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('blog.index'))
    form = LoginForm()
    if form.validate_on_submit():
        username = form.username.data
        password = form.password.data
        remember = form.remember.data
        admin = Admin.query.first()
        if admin:
            # 验证用户名和密码
            if username == admin.username and admin.validate_password(password):
                login_user(admin, remember)  # 登入用户
                flash('Welcome back.', 'info')
                return redirect_back()  # 返回上一个页面
            flash('Invalid username or password.', 'warning')
        else:
            flash('No account.', 'warning')
    return render_template('auth/login.html', form=form)

登录视图负责渲染 login.html 模板和验证登录表单。在函数一开始,为了避免已经登录的用户不小心访问这个视图,我们添加一个if判断将已经登录的用户重定向到首页。

与其他表单处理流程相同,当用户提交表单且数据通过验证后,我们分别从表单中获取用户名(username)、密码(password)和 “记住我”(remember)字段的数据。接着,从数据库中查询出 Admin 对象,判断 username 的值,并使用 Admin 类中的 validate_password() 方法验证密码。如果通过验证就调用 login_user() 方法登录用户,传入用户对象和 remember 字段的值作为参数,最后使用 redirect_back() 函数重定向回上一个页面;如果用户名和密码验证出错就发送错误提示,并渲染模板。另外,如果 Admin 对象不存在,就发送一个提示消息,然后重新渲染表单。

登录表单 LoginForm 在新创建的 login.html 模板中使用 Bootstrap-Flask 提供的 render_form() 宏渲染。为了编写一个更简单的登录页面,我们打算不在登录页面显示页脚,因为我们在基模板中为页脚的代码定义了 footer 块,所以在登录页面模板只需要定义这个块并留空就可以覆盖基模板中的对应内容。

{% extends 'base.html' %}
{% from 'bootstrap/form.html' import render_form %}
{% block title %}Login{% endblock %}
{% block content %}
    <div class="container h-100">
        <div class="row h-100 page-header justify-content-center align-items-center">
            <h1>Log in</h1>
        </div>
        <div class="row h-100 justify-content-center align-items-center">
            {{ render_form(form, extra_classes='col-6') }}
        </div>
    </div>
{% endblock %}
{% block footer %}{% endblock %}

2.3 登出用户

注销登录比登录还要简单,只需要调用 Flask-Login 提供的 logout_user() 函数即可。这会登出用户并清除 session 中存储的用户 id 和 “记住我” 的值。

from flask_login import logout_user
...
@auth_bp.route('/logout')
@login_required
def logout():
	logout_user()
	flash('Logout success.', 'info')
	return redirect_back()

2.4 视图保护

程序中的许多操作要求用户登录后才能进行,因此我们要把这些需要登录才能访问的视图 “保护” 起来。如果用户访问了某个需要认证才能访问的资源,我们不会返回对应的响应,而是把程序重定向到登录页面。

视图保护可以使用 Flask-Login 提供的 login_required 装饰器实现。在需要登录才能访问的视图前附加这个装饰器,比如博客设置页面。

当为视图函数附加多个装饰器时,route() 装饰器应该置于最外层。

from flask_login import login_required
@admin_bp.route('/settings')
@login_required
def settings():
	...
	return render_template('admin/settings.html')

当未登录的用户访问使用了 login_required 装饰器的视图时,程序会自动重定向到登录视图,并闪现一个消息提示。在此之前,我们还需要在 extension.py 脚本中使用 login_manager 对象的 login_view 属性设置登录视图的端点值(包含蓝本名的完整形式)。

login_manager = LoginManager(app)
...
login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'warning'

使用可选的 login_message_category 属性可以设置消息的类别,默认类别为 “message”。另外,使用可选的 login_message 属性设置提示消息的内容,默认消息内容为“Please log in to access this page.”

当用户访问某个被保护的 URL 时,在重定向后的登录 URL 中,Flask-Login 会自动附加一个包含上一个页面 URL 的 next 参数,所以我们只需要使用 redirect_back() 函数就可以将登录成功后的用户重定向回上一个页面。

当在未登录状态下访问设置页面 http://localhost:5000/admin/settings 时,程序会重定向到登录页面,并显示提示消息,URL 中包含上一个页面的 next 参数。

仔细观察地址栏,你会看到附加的 next 参数包含上一个页面的地址,我们经常在上网时在地址栏发现类似的参数,比如 ReturnUrlRedirectUrl 等。当我们登录后,程序会重定向回我们想要访问的设置页面。

有些时候,你会希望为整个蓝本添加登录保护。比如,管理后台的所有页面都需要登录后才能访问,也就是说,我们需要为所有 admin 蓝本中的视图函数附加 login_required 装饰器。有一个小技巧可以避免这些重复:为 admin 蓝本注册一个 before_request 处理函数,然后为这个函数附加 login_required 装饰器。因为使用 before_request 钩子注册的函数会在每一个请求前运行,所以这样就可以为该蓝本下所有的视图函数添加保护,函数内容可以为空。

@admin_bp.before_request
@login_required
def login_protect():
	pass
  • 虽然这个技巧很方便,但是为了避免在书中单独给出视图函数代码时造成误解,Bluelog程序中并没有使用这个技巧。
  • 如果没有使用这个技巧,那么 admin 蓝本下的所有视图都需要添加 login_required 装饰器,否则会导致博客资源被匿名用户修改。

3.使用CSRFProtect实现CSRF保护

CSRF攻击,全称为 Cross-site request forgery,中文名为 跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。XSS 主要是利用站点内的信任用户,而 CSRF 则通过伪装来自受信任用户的请求,来利用受信任的网站。与 XSS 相比,CSRF 更具危险性。攻击者盗用用户身份,发送恶意请求。比如:模拟用户发送邮件,发消息,以及支付、转账等。

博客管理后台会涉及对资源的局部更新和删除操作,这时我们就要考虑到 CSRF 保护问题。为了应对 CSRF 攻击,当需要创建、修改和删除数据时,我们需要将这类请求通过 POST 方法提交,同时在提交请求的表单中添加 CSRF 令牌。对于删除和某些修改操作来说,单独创建表单类的流程太过烦琐,我们可以使用 Flask-WTF 内置的 CSRFProtect 扩展为这类操作实现更简单和完善的 CSRF 保护。

CSRFProtect 是 Flask-WTF 内置的扩展,也是 Flask-WTF 内部使用的 CSRF 组件,单独使用可以实现对程序的全局 CSRF 保护。它主要提供了生成和验证 CSRF 令牌的函数,方便在不使用 WTForms 表单类的情况下实现 CSRF 保护。因为我们已经安装了 Flask-WTF,所以可以直接使用它。首先在 extensions.py 脚本中实例化 Flask-WTF 提供的 CSRFProtect 类。

from flask_wtf.csrf import CSRFProtect
...
csrf = CSRFProtect()
...

在程序包的构造文件中初始化扩展 CSRFProtect

from bluelog.extensions import csrf
def create_app(config_name=None):
	...
	register_extensions(app)
	return app
def register_extensions(app):
	...
	csrf.init_app(app)

CSRFProtect 在模板中提供了一个 csrf_token() 函数,用来生成 CSRF 令牌值,我们直接在表单中创建这个隐藏字段,将这个字段的 name 值设为 csrf_token。下面是用来删除文章的表单示例:

<form method="post" action="{{ url_for('.delete_post', post_id=post.id) }}">
	<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
	<input type="submit" value="Delete Post"/>
</form>

在对应的 delete_post 视图中,我们直接执行相关删除操作,CSRFProtect 会自动获取并验证 CSRF 令牌。注意,在 app.route() 装饰器中使用 methods 参数限制仅监听 POST 请求。

@app.route('/post/delete/<id>', methods=['POST'])
def delete_post(id):
	post = Post.query.get(id)
	post.delete()
	return redirect(url_for('index'))

默认情况下,当令牌验证出错或过期时,程序会返回 400 错误,和 Werkzeug 内置的其他 HTTP 异常类一样,CSRFError 将错误描述保存在异常对象的 description 属性中。

如果你想将与 CSRF 相关的错误描述显示在模板中,那么你可以在 400 错误处理函数中将异常对象的 description 属性传入模板,也可以单独创建一个错误处理函数捕捉令牌出错时抛出的 CSRFError 异常。

from flask_wtf.csrf import CSRFError
def register_errors(app):
	...
	@app.errorhandler(CSRFError)
	def handle_csrf_error(e):
		return render_template('400.html', description=e.description), 400

这个错误处理函数仍然使用 app.errorhandler 装饰器注册,传入 flask_wtf.csrf 模块中的 CSRFError 类。这个错误处理函数返回 400 错误响应,通过异常对象的 description 属性获取内置的错误消息(英文),传入模板 400.html 中。在模板中,我们渲染这个错误消息,并为常规 400 错误设置一个默认值。

<p>{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->{ description|default('Bad Request') }}</p>

在实际应用中,除了使用内置的错误描述,更合适的方法是自己编写错误描述信息。默认的错误描述为 “Invalid CSRF token.” 和 “The CSRF token is missing.” 因为包含太多术语,不容易理解,所以在实际的程序中,我们应该使用更简单的错误提示,比如 “会话过期或失效,请返回上一页面重试”。

到此这篇关于Python个人博客程序开发实例用户验证功能的文章就介绍到这了,更多相关Python个人博客内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Python个人博客程序开发实例后台编写

    目录 1.文章管理 1.1 文章管理主页 1.2 创建文章 1.3 编辑与删除 2.评论管理 2.1 关闭评论 2.2 评论审核 2.3 筛选评论 3.分类管理 本篇博客将是Python个人博客程序开发实例的最后一篇.本篇文章将会详细介绍博客后台的编写. 为了支持管理员管理文章.分类.评论和链接,我们需要提供后台管理功能.通常来说,程序的这一部分被称为管理后台.控制面板或仪表盘等.这里通常会提供网站的资源信息和运行状态,管理员可以统一查看和管理所有资源.管理员面板通常会使用独立样式的界面,所以你

  • Python个人博客程序开发实例框架设计

    目录 1.数据库(models.py) 1.1 管理员 Admin 1.2 分类 Category 1.3 文章 Post 1.4 评论 Comment 1.5 社交链接 Link 2.生成虚拟数据(fakes.py) 3.模板 3.1 模板上下文 3.2 渲染导航链接 3.3 Flash消息分类 4.表单(forms.py) 4.1 登录表单 4.2 文章表单 4.3 分类表单 4.4 评论表单 5.视图函数(blueprints:admin.auth.blog) 6.电子邮件支持(email

  • Python个人博客程序开发实例信息显示

    目录 1.分页显示文章列表 1.1 获取分页记录 1.2 渲染分页导航部件 2.显示文章正文 3.文章固定链接 4.显示分类文章列表 5.显示评论列表 6.发表评论与回复 7.支持回复评论 8.网站主题切换 Python个人博客程序开发实例框架设计中,我们已经完成了 数据库设计.数据准备.模板架构.表单设计.视图函数设计.电子邮件支持 等总体设计的内容,本篇博客将介绍博客前台的实现.博客前台需要开放给所有用户,这里包括 显示文章列表.博客信息.文章内容和评论 等功能. 1.分页显示文章列表 为了

  • PHP多用户博客系统分析[想做多用户博客的朋友,需要了解]第1/3页

    01,LxBlog 博客系统 这是phpwind推出的博客系统,值得推荐吧,国内推出php多用户博客的不多,如果你英文不好,就只能用这个系统了! PHPWind 博客系统 是一套基于php+mysql 数据库平台架构的多用户博客系统,该系统融合了Blog的最新元素,拥有强大的个人主页系统,独立的二级域名功能,灵活的用户模版系统,丰富的朋友圈和个性相册功能. 网站统筹化 1. 使用论坛整合接口,让博客论坛容为一体 在论坛里可以设置用户组权限,让特定的用户组有权限使用博客个人主页系统 用户在论坛里浏

  • Python初学时购物车程序练习实例(推荐)

    废话不多说,直接上代码 #Author:Lancy Wu product_list=[ ('Iphone',5800), ('Mac Pro',9800), ('Bike', 800), ('Watch', 10600), ('Coffee', 31), ('Lancy Python', 120) ] #商品列表 shopping_list=[] #定义一个列表来存储已购商品 salary=input("请输入工资:") if salary.isdigit(): #当输入的内容为数字

  • Python简单基础小程序的实例代码

    1 九九乘法表 for i in range(9):#从0循环到8 i += 1#等价于 i = i+1 for j in range(i):#从0循环到i j += 1 print(j,'*',i,'=',i*j,end = ' ',sep='') # end默认在结尾输出换行,将它改成空格 sep 默认 j,'*',i,'=',i*j 各元素输出中间会有空格 print()#这里作用是输出换行符 i = 1 while i <= 9: j = 1 while j <= i: print(&

  • python打开windows应用程序的实例

    可以加上时间判断,让程序在固定的时间启动. #coding=utf-8 #!/usr/bin/python import os def open_app(app_dir): os.startfile(app_dir) if __name__ == "__main__": app_dir = r'C:\Program Files\Sublime Text 2\sublime_text.exe' open_app(app_dir) 以上这篇python打开windows应用程序的实例就是小

  • 微信小程序实现限制用户转发功能的实例代码

    在上篇文章给大家提到微信小程序实现禁止分享代码实例,感兴趣的朋友可以点击查阅.今天继续给大家分享微信小程序实现限制用户转发功能,一起看看吧! 在小程序的开发过程,你是不是也经常遇到这么一个需求,用户希望某个页面只能自己转发分享,不希望被别人再次分享出去,接下来我们聊聊如何实现这个功能. 限制用户转发需要解决两个问题: 关闭系统右上角菜单栏中的转发功能 隐藏群聊会话中长按转发分享的功能 1.关闭系统右上角菜单栏中的转发功能 通过调用微信 API:wx.hideShareMenu({ }) 关闭当前

  • 基于Python+QT的gui程序开发实现

    最近帮朋友做了一个将文本文件按条件导出到excel里面的小程序.使用了PyQT,发现Python真是一门强大的脚本语言,开发效率极高. 首先需要引用 from PyQt4 import QtGui, uic, QtCore 很多控件像QPushButton是从QtGui的空间中得来的,下面def __init__(self, parent=None)中定义了界面的设计及与控件相互联系的方法. class AddressBook(QtGui.QWidget): def __init__(self,

  • Python实现博客快速备份的脚本分享

    目录 转存文章到MD 转存图片到本地 鉴于有些小伙伴在寻找博客园迁移到个人博客的方案,本人针对博客园实现了一个自动备份脚本,可以快速将博客园中自己的文章备份成Markdown格式的独立文件,备份后的md文件可以直接放入到hexo博客中,快速生成自己的站点,而不需要自己逐篇文章迁移,提高了备份文章的效率. 首先第一步将博客园主题替换为codinglife默认主题,第二步登录到自己的博客园后台,然后选择博客备份,备份所有的随笔文章,如下所示: 备份出来以后将其命名为backup.xml,然后新建一个

随机推荐