上一篇:Vue 2.0 起步(4) 轻量级后端Flask用户认证 - 微信公众号RSS
上一篇采用了Flask-JWT token验证。由于网站新的需要,除了token,现在要加上session管理,跟Flask-Security比较之后,决定创建第4篇的第二版:Flask-Security同时管理token和session
如果你是克隆git里源码的,注意工程目录名是vue-tutorial/
,步骤:http://www.jianshu.com/p/b3c76962e3d4
2019延伸阅读推荐:带你进入异步Django+Vue的世界 - Didi打车实战(1)
技术准备
Flask-Security 如何Session CSRF和REST Token兼得
注意:python版本!源码只能用Python2,用Python3就会报错:
AttributeError: 'NoneType' object has no attribute 'get_user'
。具体原因已查到,是因为Flask_security初始化时,python3处理方式不同。源码已更新。
步骤:
- 后端Flask app更新
- 前端vue.js ajax更新
1. 后端Flask app更新
app初始化时,引入Flask-Security,绑定数据库里的User/Role
/app/_init_.py (隐藏了无关代码)
from flask_security import Security, SQLAlchemyUserDatastore, current_user, \
UserMixin, RoleMixin, login_required, auth_token_required, http_auth_required
from flask_security.utils import encrypt_password
db = SQLAlchemy()
# models引用必须在 db/login_manager之后,不然会循环引用
from .models import User, Role
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security()
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)
security.init_app(app, datastore=user_datastore)
return app
Flask-Security相关配置
/config.py (部分)
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess xxxx'
# 允许追踪 login activities
SECURITY_TRACKABLE = True
# 密码加密存储
SECURITY_PASSWORD_HASH = 'pbkdf2_sha512'
SECURITY_PASSWORD_SALT = 'super-xxxx'
# 允许注册 register,暂不需要邮件确认
SECURITY_REGISTERABLE = True
SECURITY_SEND_REGISTER_EMAIL = False
针对SECURITY_TRACKABLE
,我们的Model也需要增加字段
/app/models.py
roles_users
表:多对多关系的联结表,一个User有多个Role,一个Role有多个User
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('roles.id')))
Role
表:保存各个角色,如superuser, admin, author, editor, user
class Role(db.Model, RoleMixin):
__tablename__ = 'roles'
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
User
表:添加last_login_at, current_login_at, last_login_ip, current_login_ip, login_count
字段,Flask-Security会自动追踪用户登录
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(64), unique=True, index=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
last_login_at = db.Column(db.DateTime())
current_login_at = db.Column(db.DateTime())
last_login_ip = db.Column(db.String(63))
current_login_ip = db.Column(db.String(63))
login_count = db.Column(db.Integer)
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users'))#, lazy='dynamic'))
member_since = 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')
注意:数据模型变动之后,需要用Flask-migration更新一下数据库。
可以在管理命令行加入这些常用命令:
/manage.py
执行python manage.py deploy
--> 初始化/升级数据模型
执行python manage.py dropall
--> 删除数据库里所有表
执行python manage.py initrole
--> 创建Roles和admin用户,赋于superuser角色
@manager.command
def deploy():
"""Run deployment tasks."""
from flask_migrate import init, migrate, upgrade
# migrate database to latest revision
try: init()
except: pass
migrate()
upgrade()
@manager.command
def dropall():
db.drop_all()
print "all tables dropped! remember to delete directory: migrations"
@manager.command
def initrole():
db.session.add(Role(name="superuser"))
db.session.add(Role(name="admin"))
db.session.add(Role(name="editor"))
db.session.add(Role(name="author"))
db.session.add(Role(name="user"))
pwd = os.getenv('FLASK_ADMIN_PWD') or raw_input("Pls input Flask admin pwd:")
db.session.add(User(email="admin", password=encrypt_password(pwd), active=True))
ins=roles_users.insert().values(user_id="1", role_id="1")
db.session.execute(ins)
db.session.commit()
print "Roles added!"
2. 前端vue.js ajax更新
Flask-Security一些默认地址:
注册:/register
登录:/login
注销:/logout
比如,你手动打开 http://localhost:5000/login, Flask-Security会自动渲染一个登录页面,很简陋,当然你可以自己写页面override它。
不过前端vue.js是用ajax访问的,不在乎页面漂亮与否。
根据Flask-Security 如何Session CSRF和REST Token兼得的步骤,来改写vue
/src/components/Siderbar.vue
登录函数login(),注意Flask-Security是用email
作为唯一用户名的,先用正则表达式获得csrf_token,再获取json格式的token
methods: {
login() {
if (!this.validation) return;
// get CSRF
var csrf_token = '';
this.$http.get('/login').then((response) => {
// 响应成功回调
var data = response.body;
// alert(JSON.stringify(response));
// <input id="csrf_token" name="csrf_token" type="hidden" value="1483433916##5b057abdef66da070c8385752b78f6c584f6ba41"><input
var csrf_token = '';
try {
csrf_token = data.match(/name="csrf_token" type="hidden" value="(.*?)">/)[1];
// alert(csrf_token);
} catch (exception) {
// 如果已经登陆,则302,redirect to home
// alert(exception); // exception: TypeError: Cannot read property '1' of null
alert('登录异常,请重新登录');
return window.location = '/logout';
}
this.$http.post('/login',
//body
{
email: this.username,
password: this.password,
csrf_token: csrf_token
},
//options
{
headers: {
'Content-Type': 'application/json; charset=UTF-8'
}
}).then((response) => {
// 响应成功回调
var jsondata = response.body;
alert(JSON.stringify(jsondata));
this.token = jsondata.response.user.authentication_token;
this.is_login = true;
// alert('token:\n'+ this.token);
var userData = {
'username': this.username,
'token': this.token
};
window.localStorage.setItem("user", JSON.stringify(userData));
// this.$nextTick(function () { });
// getSubscription()
}, (response) => {
// 响应Login-POST错误回调
alert('登录出错了! ' + response.status + response.statusText)
});
}, (response) => {
// 响应login-GET 错误回调
alert('登录出错了(CSRF)! ' + JSON.stringify(response))
});
},
注册函数register(),跟login()类似,也需要先获取csrf_token
register() {
if (!this.validation) return;
// get CSRF
var csrf_token = '';
this.$http.get('/register').then((response) => {
// 响应成功回调
var data = response.body;
// <input id="csrf_token" name="csrf_token" type="hidden" value="1483433916##5b057abdef66da070c8385752b78f6c584f6ba41"><input
var csrf_token = data.match(/name="csrf_token" type="hidden" value="(.*?)">/)[1]
// alert(csrf_token);
this.$http.post('/register',
//body
{
email: this.username,
password: this.password,
csrf_token: csrf_token
},
//options
{
headers: {
'Content-Type': 'application/json; charset=UTF-8'
}
}).then((response) => {
// 响应成功回调
var data = response.body;
// alert('Server rsp:\n'+ JSON.stringify(response));
//"body":{"meta":{"code":400},"response":{"errors":{"email":["aaa@bbb.com is already associated with an account."]}}},
if (data.meta.code !== 200) {
return alert(JSON.stringify(data.response.errors))
}
this.token = data.response.user.authentication_token;
this.is_login = true;
// alert(this.token);
var userData = {
'username': this.username,
'token': this.token
};
window.localStorage.setItem("user", JSON.stringify(userData));
}, (response) => {
// 响应错误回调
alert('注册出错了! ' + JSON.stringify(response))
});
}, (response) => {
// 响应register-GET 错误回调
alert('注册出错了(CSRF)! ' + JSON.stringify(response))
});
},
如果返回代码不是200,则说明注册出错,比如用户名已被占用,进一步,可由vue来产生中文友好的提示信息:
注销函数logout():GET /logout
就行
logout() {
this.$http.get('/logout').then((response) => {
// 响应成功回调
this.is_login = false;
this.password = '';
this.token = '';
window.localStorage.removeItem("user")
}, (response) => {
// 响应错误回调
alert('Logout出错了! ' + JSON.stringify(response))
});
},
好了,更新不算麻烦,以后就一劳永逸了,session、token管理、email验证、密码找回等等功能都有了。
Demo:http://vue2.heroku.com
源码:https://github.com/kevinqqnj/vue-tutorial
请使用新的template: https://github.com/kevinqqnj/flask-template-advanced