flask forum开发笔记(3)登录注册(上)

一个完整的认证系统并不是简简单单的字符串相等,还应当考虑到安全性问题,比如用户的密码总不能直接暴露在数据库中吧,万一数据库被入侵,那么用户的密码就全都泄露了。所以我将使用Flask-Login、Werkzeug、itsdangerous三个包做一个完整的认证系统。

1.使用Werkzeug实现密码散列


在models.py文件中创建一个User模型

#coding:utf-8

from . import db
from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key = True)
    email = db.Column(db.String(64), unique = True, index = True)
    username = db.Column(db.String(32), unique = True, index = True)
    password_hash = db.Column(db.String(128))

    @property
    def password(self):
        raise AttributeError('密码不是可读属性')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

这个User模型用Werkzeug实现了密码散列,Werkzeug中的security模块能够很方便地实现密码散列值的计算。这个功能的实现需要两个函数,分别用在注册用户和验证用户阶段。

generate_password_hash(password, method=pdkdf2:sha1, salt_length=8)

这个函数将输入的原始密码以字符串形式输出密码的散列值。

check_password_hash(hash, password)

这个函数从数据库中取回密码散列值和用户输入的密码,若返回值为True则表明密码正确。
至此密码散列功能就已经完成了,可在shell模式下进行测试。

2.使用Flask-Login认证用户


首先要创建一个新的认证蓝本auth,专门用来存放于用户认证相关的路由(auth蓝本要在create_app()工厂函数中附加到程序上,所以创建蓝本后要记得在app/__init__.py中附加蓝本)。

Flask-Login要求User模型必须实现几个方法,可以用Flask-Login提供的UserMixin类默认实现这些方法。修改后的User模型如下

#coding:utf-8

from . import db
from werkzeug.security import generate_password_hash, check_password_hash
from . import login_manager
from flask.ext.login import UserMixin

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key = True)
    email = db.Column(db.String(64), unique = True, index = True)
    username = db.Column(db.String(32), unique = True, index = True)
    password_hash = db.Column(db.String(128))

    @property
    def password(self):
        raise AttributeError('密码不是可读属性')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

修改完User模型之后在程序的工厂函数(app/__init__.py)中初始化Flask-Login

#coding:utf-8
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.bootstrap import Bootstrap
from flask.ext.login import LoginManager
from config import config

db =  SQLAlchemy()
bootstrap = Bootstrap()
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'

def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    db.init_app(app)
    bootstrap.init_app(app)
    login_manager.init_app(app)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)
    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    return app

LoginManager对象的session_protection属性有三种设置(None,basic,strong),可以提供不同的安全等级防止用户会话被篡改,我用的是strong,会记录客户端IP地址和浏览器的用户代理信息。

最后在models.py文件中写一个回调函数,使用指定的标识符加载用户

#coding:utf-8

from . import db
from werkzeug.security import generate_password_hash, check_password_hash
from . import login_manager
from flask.ext.login import UserMixin

class User(UserMixin, db.Model):
    __tablename__ = 'users'
   ...

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

完成以上步骤之后在auth文件夹中的forms.py新建LoginForm表单

#coding:utf-8
from flask.ext.wtf import Form
from wtforms import StringField, SubmitField, PasswordField, BooleanField
from wtforms.validators import Required, Length
from wtforms import ValidationError
from ..models import User

class LoginForm(Form):
    email = StringField('Email', validators=[Required(), Length(1,64), Email()])
    password = PasswordField('Password', validators=[Required()])
    remember_me = BooleanField('remember me')
    submit = SubmitField('Log In')

在views.py中写login路由和视图函数

#coding:utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf8')

from .. import db
from . import auth
from ..models import User
from .forms import LoginForm
from flask import render_template, redirect, request, url_for, flash
from flask.ext.login import login_user, logout_user, login_required

@auth.route('/login', methods=['GET','POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user, form.remember_me.data)
            return redirect(request.args.get('next') or url_for('main.index'))
        flash('错误的用户名或密码')
    return render_template('auth/login.html', form=form)


@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('您已成功退出用户')
    return redirect(url_for('main.index'))

这里有一点需要注意,如果flash消息是中文的话,需在开头加上这三行代码才能正常显示,不然会提示编码错误的bug。

import sys
reload(sys)
sys.setdefaultencoding('utf8')

至于login的html模板则用了wtf表单

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}DX Studio forum-Login{% endblock %}

{% block page_content %}

{{ wtf.quick_form(form) }}

Forgot your password? Click here to reset it.

New user? Click here to register.

{% endblock %}

最终显示效果如下

flask forum开发笔记(3)登录注册(上)_第1张图片
9DDF1486-C9B3-4207-B022-ED66A7708BD8.png

其实可以发现现在的页面和之前起始篇中的html样子已经发生了改变,因为我用if current_user.is_authenticated修改了base模板,有些东西应当在用户登录后才显示,比如个人信息页面,以及没登录前显示login登录后显示logout等等。修改例子如下

{% if current_user.is_authenticated %}
  • 退出用户
  • {% else %}
  • 用户登录
  • {% endif %}

    这里也有一点需要注意,《flask web开发》上is_authenticated()那时还是一个方法,但现在新版本的Flask-Login中这已经不是一个方法了,而是作为一个属性来使用,需要去掉括号,否则会出现bug。

    OK,这章先写到这里,开头提到的its dangerous包并没有写到,是因为我这章只写了登录,itsdangerous包用在注册,关于注册方面的笔记将会在下一章出现。

    你可能感兴趣的:(flask forum开发笔记(3)登录注册(上))