目录

 

  • 目录
  • 前文列表
  • 扩展阅读
  • 用户登录帐号
  • 用户登录状态
  • Flask-Login
  • 使用 Flask-Login 来保护应用安全
  • 小结

 

前文列表

用 Flask 来写个轻博客 (1) — 创建项目
用 Flask 来写个轻博客 (2) — Hello World!
用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy
用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表
用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解
用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many)
用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many)
用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级
用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览
用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法
用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数
用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板
用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验
用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板
用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单
用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图
用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目
用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象
用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单
用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码
用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录
用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面
用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录

扩展阅读

用户登录功能
Flask-Login

用户登录帐号

Web上的用户登录功能应该是最基本的功能了,但这个功能可能并没有你所想像的那么简单,这是一个关系到用户安全的功能. 在现代这样速度的计算速度下,用穷举法破解账户的密码会是一件很轻松的事。所以在设计用户口令登录功能的时候需要注意下列几点:

  • 用正则表达式限制用户输入一些非常容易被破解的口令
  • 密文保存用户的口令
  • 不让浏览器记住你的密码
  • 使用 HTTPS 在网上传输你的密码

上述的手段都可以帮助我们的应用程序更好的保护用户的账户信息, 后两点是否实现, 视乎于应用的安全级别是否严格, 我们在这里仅实现前面两点. 除此之外我们还要解决一个非常重要的问题, 就是用户登录状态的处理.

用户登录状态

因为 HTTP 是无状态的协议,也就是说,这个协议是无法记录用户访问状态的,所以用户的每次请求都是独立的无关联的. 而我们的网站都是设计成多个页面的,所在页面跳转过程中我们需要知道用户的状态,尤其是用户登录的状态,这样我们在页面跳转后我们才知道用户是否有权限来操作该页面上的一些功能或是查看一些数据。

所以,我们每个页面都需要对用户的身份进行认证。在这样的应用场景下, 保存用户的登录状态的功能就显得非常重要了. 为了实现这一功能:

  • 第一种方法, 用得最多的技术就是 session 和 cookie,我们会把用户登录的信息存放在客户端的 cookie 里,这样,我们每个页面都从这个 cookie 里获得用户是否已经登录的信息,从而达到记录状态,验证用户的目的.
  • 第二种方法, 我们这里会使用 Flask-Login 扩展是提供支撑.

NOTE: 两种方法是不能够共存的.

Flask-Login

Flask-Login 为 Flask 提供用户 session 的管理机制。它可以处理 Login、Logout 和 session 等服务。
作用:

  • 将用户的 id 储存在 session 中,方便用于 Login/Logout 等流程。
  • 让你能够约束用户 Login/Logout 的视图
  • 提供 remember me 功能
  • 保护 cookies 不被篡改
使用 Flask-Login 来保护应用安全
  • 安装
pip install flask-login
pip freeze . requirements.txt
  • 初始化 LoginManager 对象
    vim extensions.py
from flask.ext.login import LoginManager


# Create the Flask-Login's instance
login_manager = LoginManager()
  • 设置 LoginManager 对象的参数
    vim extensions.py
# Setup the configuration for login manager.
#     1. Set the login page.
#     2. Set the more stronger auth-protection.
#     3. Show the information when you are logging.
#     4. Set the Login Messages type as `information`.
login_manager.login_view = "main.login"
login_manager.session_protection = "strong"
login_manager.login_message = "Please login to access this page."
login_manager.login_message_category = "info"


@login_manager.user_loader
def load_user(user_id):
    """Load the user's info."""

    from models import User
    return User.query.filter_by(id=user_id).first()

NOTE 1: login_view 指定了登录页面的视图函数
NOTE 2: session_protection 能够更好的防止恶意用户篡改 cookies, 当发现 cookies 被篡改时, 该用户的 session 对象会被立即删除, 导致强制重新登录.
NOTE 3: login_message 指定了提供用户登录的文案
NOTE 4: login_category 指定了登录信息的类别为 info
NOTE 5: 我们需要定义一个 LoginManager.user_loader 回调函数,它的作用是在用户登录并调用 login_user() 的时候, 根据 user_id 找到对应的 user, 如果没有找到,返回None, 此时的 user_id 将会自动从 session 中移除, 若能找到 user ,则 user_id 会被继续保存.

  • 修改 User models, 以更好支持 Flask-Login 的用户状态检验
class User(db.Model):
    """Represents Proected users."""

    # Set the name for table
    __tablename__ = 'users'
    id = db.Column(db.String(45), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))
    # one to many: User ==> Post 
    # Establish contact with Post's ForeignKey: user_id
    posts = db.relationship(
        'Post',
        backref='users',
        lazy='dynamic')

    roles = db.relationship(
        'Role',
        secondary=users_roles,
        backref=db.backref('users', lazy='dynamic'))

    def __init__(self, id, username, password):
        self.id = id
        self.username = username
        self.password = self.set_password(password)

        # Setup the default-role for user.
        default = Role.query.filter_by(name="default").one()
        self.roles.append(default)

    def __repr__(self):
        """Define the string format for instance of User."""
        return "<Model User `{}`>".format(self.username)

    def set_password(self, password):
        """Convert the password to cryptograph via flask-bcrypt"""
        return bcrypt.generate_password_hash(password)

    def check_password(self, password):
        return bcrypt.check_password_hash(self.password, password)

    def is_authenticated(self):
        """Check the user whether logged in."""

        # Check the User's instance whether Class AnonymousUserMixin's instance.
        if isinstance(self, AnonymousUserMixin):
            return False
        else:
            return True

    def is_active():
        """Check the user whether pass the activation process."""

        return True

    def is_anonymous(self):
        """Check the user's login status whether is anonymous."""

        if isinstance(self, AnonymousUserMixin):
            return True
        else:
            return False

    def get_id(self):
        """Get the user's uuid from database."""

        return unicode(self.id)

NOTE 1: is_authenticated() 检验 User 的实例化对象是否登录了.
NOTE 2: is_active() 检验用户是否通过某些验证
NOTE 3: is_anonymous() 检验用户是否为匿名用户
NOTE 4: get_id() 返回 User 实例化对象的唯一标识 id

在完成这些准备之后, 我们可以将 Flask-Login 应用到 Login/Logout 的功能模块中.

  • 在 LoginForm 加入 Remember Me 可选框
class LoginForm(Form):
    """Login Form"""

    username = StringField('Usermame', [DataRequired(), Length(max=255)])
    password = PasswordField('Password', [DataRequired()])
    remember = BooleanField("Remember Me")

    def validate(self):
        """Validator for check the account information."""
        check_validata = super(LoginForm, self).validate()

        # If validator no pass
        if not check_validata:
            return False

        # Check the user whether exist.
        user = User.query.filter_by(username=self.username.data).first()
        if not user:
            self.username.errors.append('Invalid username or password.')
            return False

        # Check the password whether right.
        if not user.check_password(self.password.data):
            self.password.errors.append('Invalid username or password.')
            return False

        return True
  • jmilkfansblog.controllers.main.py
@main_blueprint.route('/login', methods=['GET', 'POST'])
@openid.loginhandler
def login():
    """View function for login.

       Flask-OpenID will be receive the Authentication-information
       from relay party.
    """

    # Create the object for LoginForm
    form = LoginForm()
    # Create the object for OpenIDForm
    openid_form = OpenIDForm()

    # Send the request for login to relay party(URL).
    if openid_form.validate_on_submit():
        return openid.trg_login(
            openid_form.openid_url.data,
            ask_for=['nickname', 'email'],
            ask_for_optional=['fullname'])

    # Try to login the relay party failed.
    openid_errors = openid.fetch_error()
    if openid_errors:
        flash(openid_errors, category="danger")

    # Will be check the account whether rigjt.
    if form.validate_on_submit():

        # Using session to check the user's login status
        # Add the user's name to cookie.
        # session['username'] = form.username.data

        user = User.query.filter_by(username=form.username.data).one()

        # Using the Flask-Login to processing and check the login status for user
        # Remember the user's login status. 
        login_user(user, remember=form.remember.data)

        identity_changed.send(
            current_app._get_current_object(),
            identity=Identity(user.id))

        flash("You have been logged in.", category="success")
        return redirect(url_for('blog.home'))

    return render_template('login.html',
                           form=form,
                           openid_form=openid_form)

NOTE 1: login_user() 能够将已登录并通过 load_user() 的用户对应的 User 对象, 保存在 session 中, 所以该用户在访问不同的页面的时候不需要重复登录.
NOTE 2: 如果希望应用记住用户的登录状态, 只需要为 login_user()的形参 remember 传入 True 实参就可以了.

  • jmilkfansblog.controllers.main.py
@main_blueprint.route('/logout', methods=['GET', 'POST'])
def logout():
    """View function for logout."""

    # Remove the username from the cookie.
    # session.pop('username', None)

    # Using the Flask-Login to processing and check the logout status for user.
    logout_user()

    identity_changed.send(
        current_app._get_current_object(),
        identity=AnonymousIdentity())
    flash("You have been logged out.", category="success")
    return redirect(url_for('main.login'))

NOTE 1 Logout 时, 使用 logout_user 来将用户从 session 中删除.

  • jmilkfansblog.controllers.blog.py
    如果我们希望网站的某些页面不能够被匿名用户查看, 并且跳转到登录页面时, 我们可以使用 login_required 装饰器
from flask.ext.login import login_required, current_user


@blog_blueprint.route('/new', methods=['GET', 'POST'])
@login_required
def new_post():
    """View function for new_port."""

    form = PostForm()

    # Ensure the user logged in.
    # Flask-Login.current_user can be access current user.
    if not current_user:
        return redirect(url_for('main.login'))

    # Will be execute when click the submit in the create a new post page.
    if form.validate_on_submit():
        new_post = Post(id=str(uuid4()), title=form.title.data)
        new_post.text = form.text.data
        new_post.publish_date = datetime.now()
        new_post.users = current_user

        db.session.add(new_post)
        db.session.commit()
        return redirect(url_for('blog.home'))

    return render_template('new_post.html',
                           form=form)

引用了这个装饰器之后, 当匿名用户像创建文章时, 就会跳转到登录页面.
NOTE 1: Flask-Login 提供了一个代理对象 current_user 来访问和表示当前登录的对象, 这个对象在视图或模板中都是能够被访问的. 所以我们常在需要判断用户是否为当前用户时使用(EG. 用户登录后希望修改自己创建的文章).

小结

Flask-Login 为我们的应用提供了非常重要的功能, 让应用清楚的知道了当前登录的用户是谁和该用户的登录状态是怎么样的. 这就让我们为不同的用户设定特定的权限和功能提供了可能. 如果没有这个功能的话, 那么所有登录的用户都可以任意的使用应用的资源, 这是非常不合理的.