Vue 二.0 起步(四) 轻量级后端Flask用户认证 – 微信公众号LX570SS

参考:

本篇达成

  1. Flask框架搭建
  2. 后端用户注册、认证
    (注意:改用Flask-Security了:http://www.jianshu.com/p/f37871e31231)
  3. 跨域(Access-Control-Allow-Origin)本地调节和测试
  4. 表单验证validation

Demo(http://vue2.heroku.com)

本章完效用果

使用RESTful想想,互联网应用的后端仅提供API接口来提供鉴权服务和财富,前端Vue使用Ajax访问获取数据并显示。
MVVM模型
这里Flask担任Model(数据模型)、View(路由)剧中人物,Vue担任VM(ViewModel视图模型)剧中人物。

工具选拔

  • 后端:使用Flask那一广受好评的Python微(Micro)框架,10分适合快速支付。当然,使用Node.js、PHP、Java等等别的语言,思路也是大概的。Flask被号称“micro
    framework”,是因为它使用简便的骨干,用extension扩大别的职能。Flask未有暗中认可使用的数据库、窗体等等验证工具。可是,Flask保留了扩大与扩张的弹性,能够用Flask-extension参与这一个意义:ORM、窗体验证工具、文件上传、各个开放式身份验证技术。是否跟vue很像啊?

flask logo

Flask最主旨的hello-world,用几行代码、八个文本就能兑现。但为了适应大型项目扩大,跟vue-cli创设的脚手架类似,Flask也有推荐的门类优秀目录,结构如下:

vue-tutorial/
    |--app/
        |--api_1_0/    # api目录,对于REST访问返回数据
            |--users.py
        |--main/
            |--views.py  # 路由文件,SPA里,只需要返回"/"根路由
        |--static/      # js, css
        |--templates/    # SPA里,只需要index.html  
        |--__init__.py  # flask app初始化
        |--models.py    # model数据库定义
    |--config.py  # Flask配置
    |--manage.py  # Flask启动文件,包含命令行

参考:Flask官网
自作者的Flask连忙入门

  • 用户鉴权:使用JWT(JSON Web
    Token)
    。本实例是SPA(单页面应用),前端vue-router插件已经落到实处路由作用,后端Flask只要求提供api接口就行。所以不需求利用Flask_login来保管session。JWT是多少个不行轻快的规范,允许大家应用JWT在用户和服务器之间传递安全可靠的新闻。

参考:我的Flask-JWT入门

jwt.png

(注意:改用Flask-Security了:http://www.jianshu.com/p/f37871e31231)

一. Flask框架搭建

请先下载品类源码,对照源码阅读和实施会功能更加高

首先,设计三个结构清晰的关系型数据库(Model):

  • User用户表,肯定是要有个别,记录用户名、密码Hash和他关切的众生号
  • Mp公众号表,记录哪些人关切了那个公众号,以及那些群众号有怎么着小说
  • Article作品表,相对简单,记录群众号小说

关系

  • Mp和Article是一对多的涉及
  • User和Mp,看起来像一对多,但实质上是多对多的关系:一个用户关注多少个公众号,同一个民众号也说不定被四个用户一起关切,所以须要另加一张“关联表” –
    Subscription
  • User和Subscription:一对多
  • Mp和Subscription:一对多

数据库EER

上面就来创建model,在Flask models.py达成。

/app/models.py

  • Subscription:关联到User和Mp两个表

# encoding: utf-8
from datetime import datetime
import hashlib
from werkzeug.security import generate_password_hash, check_password_hash
from flask import current_app, request, url_for, jsonify
from flask_login import UserMixin, AnonymousUserMixin
from . import db

# 订阅公众号和User是多对多关系
class Subscription(db.Model):
    __tablename__ = 'subscriptions'
    # follower_id
    subscriber_id = db.Column(db.Integer, db.ForeignKey('users.id'),
                            primary_key=True)
    # followed_id
    mp_id = db.Column(db.Integer, db.ForeignKey('mps.id'),
                            primary_key=True)
    subscribe_timestamp = db.Column(db.DateTime, default=datetime.utcnow)
  • User:功用最棒复杂。
    1. 关联到Subscription表
    2. password.setter:用户注册时,密码转载为hash存款和储蓄。永恒不要存款和储蓄密码明文!
    3. subscribed_mps方法:用过滤器和集合查询,重回该用户订阅的具备民众号

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
    mps = db.relationship('Subscription',
                               foreign_keys=[Subscription.subscriber_id],
                               backref=db.backref('subscriber', lazy='joined'),
                               lazy='dynamic',
                               cascade='all, delete-orphan')

    @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)

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

    @property
    def subscribed_mps(self):
        # SQLAlchemy 过滤器和联结
        return Mp.query.join(Subscription, Subscription.mp_id == Mp.id)\
            .filter(Subscription.subscriber_id == self.id)

    def __repr__(self):
        return '<User %r>' % self.username
  • Mp:
    1. 关联到Subscription表
    2. to_json方法:在REST拜会时,用来回到json格式的万众号数量

# 公众号
class Mp(db.Model):
    __tablename__ = 'mps'
    id = db.Column(db.Integer, primary_key=True)
    weixinhao = db.Column(db.Text)
    image = db.Column(db.Text)
    summary = db.Column(db.Text)
    sync_time = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    articles = db.relationship('Article', backref='mp', lazy='dynamic')
    subscribers = db.relationship('Subscription',
                               foreign_keys=[Subscription.mp_id],
                               backref=db.backref('mp', lazy='joined'),
                               lazy='dynamic',
                               cascade='all, delete-orphan')
    def to_json(self):
        json_mp = {
            'weixinhao': self.weixinhao,
            'image': self.image,
            'summary': self.summary,
            'articles_count': self.articles.count()
        }
        return json_mp
  • Article:最为简单
    1. 关联到Mp表
    2. to_json方法:在REST走访时,用来回到json格式的篇章数量

# 公众号的文章
class Article(db.Model):
    __tablename__ = 'articles'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    image = db.Column(db.Text)
    summary = db.Column(db.Text)
    url = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    mp_id = db.Column(db.Integer, db.ForeignKey('mps.id'))

    def to_json(self):
        json_article = {
            'url': url_for('api.get_comment', id=self.id, _external=True),
            'body': self.body,
            'timestamp': self.timestamp
        }
        return json_article

第三步,在app开始化时,引用models.py,并创设JWT

/app/_init_.py

# encoding: utf-8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_jwt import JWT
from config import config

db = SQLAlchemy()

# models引用必须在 db之后,不然会循环引用
from .models import User

# JWT鉴权:默认参数为username/password,在数据库里查找并比较password_hash
def authenticate(username, password):
    print 'JWT auth argvs:', username, password
    user = User.query.filter_by(username=username).first()
    if user is not None and user.verify_password(password):
        return user
# JWT检查user_id是否存在
def identity(payload):
    print 'JWT payload:', payload
    user_id = payload['identity']
    user = User.query.filter_by(id=user_id).first()
    return user_id if user is not None else None
# 创建jwt实例
jwt = JWT(authentication_handler=authenticate, identity_handler=identity)

def create_app(config_name):
    app = Flask(__name__)
# 引入Flask用户配置
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)
# 初始化数据库和JWT
    db.init_app(app)
    jwt.init_app(app)
# 注册main/api蓝本,这样用户访问路径“/xxx”指向main,“/api/v1.0”指向api
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)
    from .api_1_0 import api as api_1_0_blueprint
    app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')

    return app

其三步,在运营文件中,创立命令行(数据库、布置、测试等等),运维app

/manage.py

#!/usr/bin/env python
import os
from app import create_app, db, jwt
from app.models import User, Subscription, Mp, Article
from flask_script import Manager, Shell
from flask_migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
    return dict(app=app, db=db, User=User, Subscription=Subscription, Mp=Mp,
                Article=Article)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

@manager.command
def deploy():
    """Run deployment tasks."""
    from flask_migrate import upgrade
    from app.models import User

    # migrate database to latest revision
    upgrade()

if __name__ == '__main__':
    manager.run()

好了,总算起初框架都实现了。看上去很复杂,但跟vue-cli脚手架工具①样,你下载项目源码,框架都以现成的(而且是成熟笃定的),稍微修改一下就能跑起来了。主尽管数据库定义models.py,完全要自己来定!

Python的依靠模块安装:
跟<code>npm install</code>
node_modules类似,直接用<code>pip install -r
requirements.txt</code>就一步成功了。

前日数据库还未有,大家来创立吧,很简短,三行命令:
打开CMD(Windows),或Shell(Linux)

c:\git\vue-tutorial>python manage.py db init
Creating directory c:\git\vue-tutorial\migrations ... done
Creating directory c:\git\vue-tutorial\migrations\versions ... done
Generating c:\git\vue-tutorial\migrations\alembic.ini ... done
Generating c:\git\vue-tutorial\migrations\env.py ... done
Generating c:\git\vue-tutorial\migrations\env.pyc ... done
Generating c:\git\vue-tutorial\migrations\README ... done
Generating c:\git\vue-tutorial\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in 'c:\\git\\vue-tutorial\\migrations\\alembic.ini' before proceed
ing.

c:\git\vue-tutorial>python manage.py db migrate -m "init"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'mps'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_mps_sync_time' on '['sync_time']'
INFO  [alembic.autogenerate.compare] Detected added table 'users'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_users_username' on '['username']'
INFO  [alembic.autogenerate.compare] Detected added table 'articles'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_articles_timestamp' on '['timestamp']'
INFO  [alembic.autogenerate.compare] Detected added table 'subscriptions'
Generating c:\git\vue-tutorial\migrations\versions\599e99548c86_init.py ... done

c:\git\vue-tutorial>python manage.py db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 599e99548c86, init

当今,数据库文件已经发出了,暗许是config.py文件里定义的<code>/data-dev.sqlite</code>,打开来看看:
models.py里定义的表,都成立好了,是还是不是很通晓啊?比如:Subscription表,有多少个Foreign_Key,指向Mp和User。

sqlite数据库.png

二. 后端用户注册、认证

回去Vue.js,大家准备把注册功效,放在左侧Siderbar.vue

  • template第1有的:假若已经报到is_login,则呈现用户头像和退出按钮
  • template第1局地:若是未有登录,则显得二个表单,能够输入username/password,注册和登录七个按钮

# /src/components/Sidebar.vue (部分)
<template>
    <div class="card">
        <div v-if="is_login" class="card-header" align="center">
            <img src="http://avatar.csdn.net/1/E/E/1_kevin_qq.jpg"
                 class="avatar img-circle img-responsive" />
            <p><strong v-text="username"></strong>
                <a href="javascript:" @click="logout()" title="退出">
                    <i class="fa fa-sign-out float-xs-right"></i></a>
            </p>
        </div>
        <div v-else class="card-header" align="center">
            <form class="form" @submit.prevent>
                <div class="form-group">
                    <input class="form-control" name="username" type="text" placeholder="用户名" v-model="username"
                           required pattern="\w{3,12}" />
                           <p class="text-muted"><small>3~12位字母、数字、下划线</small></p>
                </div>
                <div class="form-group">
                    <input class="form-control" name= "password" type="password" placeholder="密码" v-model="password"
                           required pattern="\w{4,}"/>
                           <p class="text-muted"><small>至少4位,字母数字下划线</small></p>
                </div>
                <div class="form-group clearfix">
                    <input type="submit" @click="register()" class="btn btn-outline-danger float-xs-left"                       value="注册" />
                    <input type="submit" @click="login()" class="btn btn-outline-success float-xs-right"                        value="登录" />
                </div>
            </form>
        </div>
。。。
    </div>
</template>
  • Script部分:插手新的data和methods
  • methods.register(),用vue-resource
    post功能,提交username/password到Flask,由<code>/api/users.py</code>处理

# /src/components/Sidebar.vue (部分)
<script>
    export default {
        name : 'Sidebar',
        data() {
            return {
                is_login: false,
                username: '',
                password: '',
                token: ''
            }
        },
。。。
        methods : {
            register() {
                this.$http.post('http://127.0.0.1:5000/api/v1.0/register',
//body
                        {   username: this.username,
                            password: this.password
                        },
                        //options
                        {
                            headers: {'Content-Type':'application/json; charset=UTF-8'}
                        }                 ).then((response) => {
                    // 响应成功回调
                    var data = response.body;
                if (data.status=='success') {
                    alert('Success! ' + data.msg)
                }
                else {
                    alert(data.msg)
                }
                this.password = ''
            }, (response) => {
                    // 响应错误回调
                    alert('注册出错了! '+ JSON.stringify(response))
                });
            }
</script>
  • 后端处理register请求:
    对此ajax
    post请求,上边是用json提交的,所以用<code>request.get_json</code>来获得数码。然后检查数据是或不是有效,用户名是还是不是曾经注册。一切无误的话就会加上用户到数据库。

# /app/api_1_0/users.py
@api.route('/register', methods=['GET', 'POST'])
def register():
    username = request.get_json()['username']
    password = request.get_json()['password']
    print 'register Header: %s\nusername: %s, password:%s'% (request.headers, username, password)
    if username <> '' and password <> '':
        if User.query.filter_by(username=username).first():
             return jsonify({
            'status': 'failure',
            'msg': u'用户名已被占用,换一个吧'
            })           

        user = User(username=username, password=password)
        db.session.add(user)
        db.session.commit()
        return jsonify({
        'status': 'success',
        'msg': 'register OK, please login!'
        })
    return jsonify({
    'status': 'failure',
    'msg': 'register fail, check username and password.'
    })

Flask跑起来,用的是5000端口:

c:\git\vue-tutorial>python manage.py runserver
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 302-156-201
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

前者点击注册按钮,应该成功了!咦,怎么再次回到Bad Request (400)?

127.0.0.1 - - [15:25:09] "OPTIONS /api/v1.0/register HTTP/1.1" 400 -

本身明明发的是POST,为啥服务器端说接受是的OPTIONS呢?哈哈,那即是名扬四海的COSportageS跨域请求了!请看下一节的灵活消除方案!

3. 跨域(Access-Control-Allow-Origin)本地调节和测试

前端跨域Post请求,由于COQashqaiS(cross origin resource
share)规范的留存,浏览器会首首发送叁次Options嗅探,同时header带上origin,判断是还是不是有跨域请求权限,服务器响应access
control allow
origin的值,供浏览器与origin相称,借使合营,浏览器则正式发送post请求。
假定有服务器程序权限,设置headeraccess control allow
origin等于*,就足以允许前端跨域访问了。
本身原先也是如此化解的:Flask: Ajax
设置Access-Control-Allow-Origin完毕跨域访问

CORS深入

当下jsonp是最不难易行跨域方案,不过只可以GET,不扶助POST。假使要POST,则服务器端设置ACAO很麻烦,或用其余的绕路方法。

但是:
大家上1篇,不是有那几个方案吗:Vue+Flask轻量级前端、后端框架,怎么样完善同步开发
如此就不存在跨域烦恼了,我们自家就在二个服务器(localhost:陆仟,包蕴端口号)下啊!

好了,立刻来试试看。上壹篇的步子是适用于最简的Flask项目,在那里有个细微改动,因为大家用了新的Flask目录框架,要求把修改后的index.html放入

/app/templates/index.html

相同,static文件放入新的目录:

/app/static/font-awesome/

再修改Siderbar.vue,POST不需求跨域了,直接是相同服务器+端口号上的路线<code>/api/v1.0/register</code>:

# /src/components/Sidebar.vue (部分)
<script>
。。。
        methods : {
            register() {
                this.$http.post('/api/v1.0/register',
。。。
            }
</script>

Flask跑起来,再点击“注册”,成功啦!

注册成功.png

对应的Flask log:

register Header: Referer: http://localhost:5000/
Origin: http://localhost:5000
Content-Length: 41
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTM
537.36
Connection: keep-alive
X-Requested-With: XMLHttpRequest
Host: localhost:5000
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4
Content-Type: application/json; charset=UTF-8
Accept-Encoding: gzip, deflate


username: hellovue, password:1111
127.0.0.1 - - [23/Dec/2016 12:16:03] "POST /api/v1.0/register HTTP/1.1" 200 -

不料,我们贰个纤维革新,不仅前后端完美同步开发,而且还消除了CO帕杰罗S跨域难题!

OK,继续形成用户登录、登出职能,很不难,在Siderbar.vue里添加methods就行

  • login():/auth是Flask-JWT暗许的鉴权路由,鉴权方法已经在/app/_init_.py里写好了。假若登录成功,本地LocalStorage把获得的token保存下去,以往REST请求会用到那一个。
  • logout():is_login设成false,然后本地LocalStorage删除user(指标是剔除保存的token)

# /src/components/Sidebar.vue (部分)
       methods : {
            login() {
                this.$http.post('/auth',
                    //body
                        {   username: this.username,
                            password: this.password
                        },
                        //options
                        {
                            headers: {'Content-Type':'application/json; charset=UTF-8'}
                        }                 ).then((response) => {
                    // 响应成功回调
                    var data = response.body;
                this.token = data.access_token;
                this.is_login = true;
   //             alert(data.access_token);
                var userData = {'username': this.username, 'token': this.token};
                window.localStorage.setItem("user", JSON.stringify(userData))

            }, (response) => {
                    // 响应错误回调
                    alert('登录出错了! '+ response.status+ response.statusText)
                });
            },
            logout() {
                this.is_login = false;
                this.password = '';
                this.token = '';
                window.localStorage.removeItem("user")
            },

肆. 表单验证validation

表单验证是个很常见的必要,借使用户不停地收取输入错误的报警,会很抓狂滴!所以,最佳在提交前做好验证。
vue-validator是Vue全家桶里用到的表单验证插件,但适用于Vue二.0的本子迟迟没生产。
那就协调写三个简便的呗,几行代码而已。
对于HTML5,自身就有核心的表单验证成效,提交时浏览器会自行检查,但仅限于部分浏览器

失效输入,按钮不可点击

输入有效,按钮可点击

  • template input里,<code>required
    pattern=”\w{3,12}”</code>是HTML5的功能
  • 按钮上,绑定三个计算属性validation:
    <code>:disabled=”!validation”</code>
  • 付出事件: <form class=”form”
    @submit.prevent>,阻止了暗许submit事件,由vue方法接手

# /src/components/Sidebar.vue (部分)
          <form class="form" @submit.prevent>
                <div class="form-group">
                    <input class="form-control" name="username" type="text" placeholder="用户名" v-model="username"
                           required pattern="\w{3,12}" />
                           <p class="text-muted"><small>3~12位字母、数字、下划线</small></p>
                </div>
                <div class="form-group">
                    <input class="form-control" name= "password" type="password" placeholder="密码" v-model="password"
                           required pattern="\w{4,}"/>
                           <p class="text-muted"><small>至少4位,字母数字下划线</small></p>
                </div>
                <div class="form-group clearfix">
                    <input type="submit" @click="register()" class="btn btn-outline-danger float-xs-left" 
                        value="注册" :disabled="!validation" />
                    <input type="submit" @click="login()" class="btn btn-outline-success float-xs-right" 
                        value="登录" :disabled="!validation" />
                </div>
  • 计量属性validation,实时总计用户输入内容是不是可行。比如:/(\w{三,12})/是判断是或不是为:3到11位的数字、字母、下划线。
  • login/register方法:在post提交前,即使总结属性validation为false(输入有误),就不付出

# /src/components/Sidebar.vue (部分)
       computed : {
            validation() {
                var patt1 = /(\w{3,12})/;
                var patt2 = /(\w{4,})/;
//              alert(this.username + patt1.test(this.username));
                return patt1.test(this.username) && patt2.test(this.password)
            }
        },
        methods : {
            login() {
                if (!this.validation) return;
                this.$http.post('/auth',
。。。

舒了一口气,那篇写得时刻较长,因为相当于把后端Flask启蒙了二回。。。
再而三的ajax保存、请求订阅列表,绝相比较较简单明了,我们有啥样别的须求,请评论留言哦!
品种源码

TODO:

  • 后端保存用户订阅的公众号,搜狗的链接都以一时的
  • 大众号小说的更新,这么些Python爬虫最擅长了

敬请关怀Vue 二.0 起步(5) 订阅列表上传和下载 –
微信公众号LacrosseSS

http://www.jianshu.com/p/ab778fde3b99

网站地图xml地图