码迷,mamicode.com
首页 > 其他好文 > 详细

Flask学习之六——用户认证

时间:2015-05-23 22:42:59      阅读:1199      评论:0      收藏:0      [点我收藏+]

标签:

1. 密码安全性

使用Werkzeug实现密码hash

generate_password_hash(password, method, salt_length)

将原始密码作为输入,以字符串形式输出密码的hash值,输出的值可保存在用户数据库中。
method 和 salt_length的默认值就能满足大多数需求。

check_password_hash(hash, password)

将数据库中取回的密码hash和用户输入的密码比较,如果密码正确则返回True
app/models.py:在User模型中加入密码hash

from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    #...
    password_hash = db.Column(db.String(128))

    @property
    def password(self):
        raise AttributeError(password is not a readable attribute)

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

 

 

 

2. 创建用户认证 blueprint 

对于程序的不同功能,使用不同的 blueprint 定义路由(保持代码整齐有序)
与用户认证系统相关的路由可在 auth blueprint 定义。
app/auth/__init__.py: 创建 blueprint

from flask import Blueprint

auth = Blueprint(auth, __name__)

from . import views

app/auth/views.py: 定义 blueprint 中的路由和视图函数

from flask import render_template
from . import auth

@auth.route(/login)
def login():
    return render_template(auth/login.html)

为避免与 main 蓝本和后续添加的蓝本发生模板命名冲突,可以把蓝本使用的模板保存在单独的文件夹中。这个文件夹必须在app/templates 中创建,因为 Flask 认为模板的路径是相对于程序模板文件夹而言的。
app/__init__.py: 注册 blueprint

def create_app(config_name):
    #...
    from .auth import auth as auth_blueprint 
    app.register_blueprint(auth_blueprint, url_prefix=/auth)

注册时的url_prefix参数可选,如果使用了该参数,则该blueprint上定义的所有路由都会加上该前缀。 比如/login路由会注册成/auth/login。

 

 

 

3. 使用Flask-Login认证用户

3.1 准备用于登录的用户模型

Flask-Login要求实现的用户方法

 技术分享

 

Flask-Login提供一个UserMixin类,包含这些用户方法

app/models.py: 修改User模型,支持这些用户方法

from flask.ext.login import UserMixin

class User(db.Model, UserMixin):
    __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(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))
    role_id = db.Column(db.Integer, db.ForeignKey(roles.id))

 

app/__init__.py:初始化Flask-Login

from flask.ext.login import LoginManager

login_manager = LoginManager()
login_manager.session_protection = strong
login_manager.login_view = auth.login

def create_app(config_name):
    #...
    login_manager.init_app(app)
    #...

session_protection可以设为None, ‘basic‘ 或‘strong‘, 以提供不同的安全等级防止用户会话遭篡改
login_view设置登录页面的端点(因为登录路由在蓝本中定义,因此在前面加上蓝本名字)

 

最后, Flask_Login要求程序实现一个回调函数,使用指定的标志符加载用户.

rom . import login_manager

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

加载用户的回调函数接受以Unicode表示的用户标志符。如果能找到用户,这个函数必须返回用户对象,否则返回None。

 

3.2 保护路由

为了保护某些路由只让认证用户访问,Flask-Login提供了一个login_required修饰器

from flask.ext.login import login_required

@app.route(/secret)
@login_required
def secret():
    return Only authenticated users are allowed

如果未认证的用户访问这个路由,Flask-Login会拦截请求,把用户发往登录页面。

 

3.3 添加用户登录表单

app/auth/forms.py: 添加登录表单

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email

class LoginForm(Form):
    email = StringField(Email, validators=[Required(), Length(1, 64), Email()])
    password = PasswordField(Password, validators=[Required()])
    remember_me = BooleanField(Keep me logged in)
    submit = SubmitField(Login)

 

app/templates/base.html: 导航条中的Sign In和Sign Out

<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated() %}
<li><a href="{{ url_for(‘auth.logout‘) }}">Sign Out</a></li>
{% else %}
<li><a href="{{ url_for(‘auth.login‘) }}">Sign In</a></li>
{% endif %}
</ul>

判断条件中的变量 current_user 由 Flask-Login 定义,且在视图函数和模板中自动可用。
这个变量的值是当前登录的用户,如果用户尚未登录,则是一个匿名用户。如果是匿名用户,is_authenticated() 方法返回 False。所以这个方法可用来判断当前用户是否已经登录。

 

3.4 添加用户登入路由

app/auth/views.py: 添加登录路由

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

@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(Invalid username or password)
    return render_template(auth/login.html, form=form)

为了登入用户,视图函数首先使用表单中填写的 email 从数据库中加载用户。如果电子邮件地址对应的用户存在,再调用用户对象的 verify_password() 方法,其参数是表单中填写的密码。如果密码正确,则调用 Flask-Login 中的 login_user() 函数,在用户会话中把用户标记为已登录。
login_user() 函数的参数是要登录的用户,以及可选的“remember_me”布尔值。如果值为 False,那么关闭浏览器后用户会话就过期了,所以下次用户访问时要重新登录。如果值为 True,那么会在用户浏览器中写入一个长期有效的 cookie,使用这个 cookie 可以复现用户会话。
未登录用户访问未授权的 URL 时会显示登录表单(保护路由的功能),Flask-Login会把原地址保存在查询字符串的 next 参数中,这个参数可从 request.args 字典中读取。(这样登录后就能跳转到登录前的页面)。
而且如果查询字符串中没有 next 参数,则重定向到首页。
如果用户输入的电子邮件或密码不正确,程序会设定一个 Flash 消息,再次渲染表单,让用户重试登录。

 

3.5 添加用户登出路由

app/auth/views.py: 添加登出路由

from flask.ext.login import logout_user, login_required

@auth.route(/logout)
@login_required
def logout():
    logout_user()
    flash(You have been log out)
    return redirect(url_for(main.index))

调用Flask-Login中的logout-user()函数,删除并重设用户会话

 

 

 

4. 注册新用户

添加用户注册表单

app/auth/forms.py: 用户注册表单

from flask.ext.wtf import forms
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User

class RegistrationForm(Form):
    email = StringField(Emial, validators=[Required(), Length(1, 64), Email()])
    username = StringField(Username, validators=[
        Required(), Length(),  Regexp(^[A-Za-z][A-Za-z0-9_.]*$, 0,
                                        Usernames must have only letters, 
                                        numbers, dots or underscores)])
    password = PasswordField(Password, validators=[
        Required(), EqualTo(password2, message=Passwords must match)])
        password2 = PasswordField(Confirm password, validators=[Required()])
        submit = SubmitField(Register)                                                                                            

    def validate_email(self, field):
        if User.query.filter_by(email=field.data).first():
            raise ValidationError(Email already register)

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError(Username already in use)

安全起见,密码要输入两次。此时要验证两个密码字段中的值是否一致,这种验证可使用WTForms 提供的另一验证函数实现,即 EqualTo。这个验证函数要附属到两个密码字段中的一个上,另一个字段则作为参数传入。
如果表单类中定义了以validate_ 开头且后面跟着字段名的方法,这个方法就和常规的验证函数一起调用。

 

添加注册路由

app/auth/viewspy: 用户注册路由

@auth.route(/register, methods=[GET, POST])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        flash(You can now login.)
        return redirect(url_for(auth.login))
    return render_template(auth/register.html, form=form)

 

 

 

5. 确认用户

5.1  用itsdangerous生成confirmation tokens

app/models.py: User模型添加生成和检验token功能

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from . import db

class User(UserMixin, db.Model):
    #...
    confirmed = db.Column(db.Boolean, default=False)

    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config[SECRET_KEY], expiration)
        return s.dumps({confirm: self.id})

    def confirm(self, token):
        s =Serializer(current_app.config[SECRET_KEY])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get(confirm) != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

itsdangerous 提供了多种生成token的方法。其中,TimedJSONWebSignatureSerializer 类生成具有过期时间的 JSON Web 签名(JSON Web Signatures,JWS)(接收的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置)
dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成token字符串。expires_in 参数设置token的过期时间,单位为秒。
loads() 解码token。这个方法会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的token不正确或过期了,则抛出异常。

 

5.2发送确认邮件

app/auth/views.py: 修改注册路由使其能发送确认邮件

from ..email import send_email

@auth.route(/register, methods = [GET, POST])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        db.session.commit()
        token = user.generate_confirmation_token()
        send_email(user.email, Confirm Your Account,
                    auth/email/confirm, user=user, token=token)
        flash(A confirmation email has been sent to you by email)
        return redirect(url_for(main.index))
    return render_template(auth/register.html, form=form)

注意,即便通过配置,程序已经可以在请求末尾自动提交数据库变化,这里也要添加db.session.commit() 调用。因为,提交数据库之后才能赋予新用户 id 值,而确认令牌需要用到 id,所以不能在请求结束后提交,而应即时提交。

ps:在电子邮件中url_for()函数要设定_external=True生成完整URL。

 

app/auth/views.py: 添加确认用户的路由

from flask.ext.login import current_user

@auth.route(/confirm/<token>)
@login_required
def confirm(token):
    if current_user.confirmed:
        return redirect(url_for(main.index))
    if current_user.confirm(token):
        flash(You have confirmed your account. Thanks!)
    else:
        flash(The confirmation link is invalid or has expired)
    return redirect(url_for(main.index))

login_required保护路由:用户点击确认邮件中的链接后,后先跳转到登录界面,登录后才会执行这个视图函数。

 

每个程序都可以决定用户确认账户之前可以做哪些操作。比如,允许未确认的用户登录,但只显示一个页面,这个页面要求用户在获取权限之前先确认账户。

这一步可使用 Flask 提供的 before_request 钩子完成。对蓝本来说,before_request 钩子只能应用到属于蓝本的请求上。若想在蓝本中使用针对程序全局请求的钩子,必须使用 before_app_request 修饰器。

app/auth/views.py: 在before_app_request中过滤为确认的账户

@auth.before_app_request
def before_request():
    if current_user.is_authenticated()             and not current_user.confirmed             and request.endpoint[:5] != auth.:
        return redirect(url_for(auth.unconfirmed))

@auth.route(/unconfirmed)
def unconfirmed():
    if current_user.is_anonymous() or current_user.confirmed:
        return redirect(main.index)
    return render_template(auth/unconfirmed.html)

同时满足一下三个条件时,before_app_request 处理程序会拦截请求:

  • 1. 用户已登录(即 current_user.is_authenticated 返回True)
  • 2. 用户的账户还未确认
  • 3. 请求的端点(使用 request.endpoint 获取)不在auth蓝本中。(因为认证蓝本就是让用户确认账户的。。。)

 

显示给未确认用户的页面只渲染一个模板,其中有如何确认账户的说明,此外还应提供了一个链接,用于请求发送新的确认邮件,以防之前的邮件丢失。

app/auth/views.py: 添加重新发送确认邮件的路由

@auth.route(/confirm)
@login_required
def resend_confirmation():
    token = current_user.generate_confirmation_token()
    send_email(current_user.email, Confirm Your Account,
                auth/email/confirm, user=current_user, token=token)
    flash(A new confirmation email has been sent to you by email.)
    return redirect(url_for(main.index))

 

 

 

 

2015-05-23

 

Flask学习之六——用户认证

标签:

原文地址:http://www.cnblogs.com/whuyt/p/4524946.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!