Flask-Security 如何Session CSRF和REST Token兼得

How to use both CSRF and auth Token in Flask-Security

以前学习的《Flask Web开发:基于Python的Web应用开发实战》,用到了Flask-Login,管理用户Session、Cookie
我们的应用:Vue 2.0 起步(4) 轻量级后端Flask用户认证 - 微信公众号RSS,用到了Flask-JWT,管理REST访问用的Token
用户权限管理,像Admin/User/Editor,是用Flask-Principal
当然,还有E-mail验证、密码修改。。。
如果我们的应用,即想管理Session(网页),又想要管理Token(REST访问),是不是两者不能兼得?抑或很繁琐呢?
幸好,Flask用于鉴权管理,有个集大成者:Flask-Security

Flask-Security.png

功能

先浏览一下Flask-Security官网所列的功能:

  • 登录追踪 Login Tracking

    • Last login date
    • Current login date
    • Last login IP address
    • Current login IP address
    • Total login count
  • JSON/Ajax Support
    Flask-Security supports JSON/Ajax requests where appropriate. Just remember that all endpoints require a CSRF token just like HTML views.
    JSON is supported for the following operations:

    • Login requests
    • Registration requests
    • Change password requests
    • Confirmation requests
    • Forgot password requests
    • Passwordless login requests

是不是眼花缭乱了?哈哈,你能想到的和想不到的,Flask-Security都帮你做到了。

注意看功能列表最后一段:JSON访问必须也带上CSRF。这跟我们以前用的Flask-JWT是不一样的哦!
下面我们来使用Flask-Security,实现Session CSRF和REST Token兼得的效果!

CSRF跨站请求伪造(Cross-site request forgery)

也被称为one-click attack 或者session riding,通常缩写为CSRF或者XSRF, 是一种挟制用户在当前已登录的Web应用程序上,执行非本意的操作的攻击方法。
因为表单登录以后,会保存session信息到用户电脑。如果使用了“Remember me”功能,会保存session_token到用户cookie到用户电脑,那更加危险!攻击者拿到这个cookie,就能直接访问了。
对于web站点,将持久化的授权方法(例如cookie或者HTTP授权)切换为瞬时的授权方法(一般是在每个form中提供隐藏csrf_token field),这将帮助网站防止这些攻击。

步骤:

1. get csrf_token

正常浏览器登录,都会用到表单(Forms),表单里带有隐藏的CSRF。所以,如果REST访问,我们先拿到这个CSRF

# ajax send request:
GET http://localhost:5000/login
 
# Server response:
<h1>Login</h1>
<form action="/login" method="POST" name="login_user_form">
 <input id="csrf_token" name="csrf_token" type="hidden" value="34c942cc61f0bxxx">
 ...
# javascript fetch "csrf_token"
document.getElementById("csrf_token").value

2. post email/password

REST访问,使用POST,带上email, password和上一步的csrf_token

# ajax send request:
POST http://localhost:5000/login
Headers: Content-Type   application/json

Body:
{
"email":"aaa@bbb.com", 
"password":"password",
"csrf_token":"34c942cc61f0bxxx"
}

3. get auth_token

服务器检查CSRF是否有效,以及email/password是否匹配。成功则返回auth_token:

# Server response:
{
  "meta": {
    "code": 200
  }, 
  "response": {
    "user": {
      "authentication_token": "WyIxIiwiNWY0ZGNjM2I1Yxxx", 
      "id": "1"
    }
  }
}

4. access views with @auth_token_required

终于拿到auth_token了!
下面的REST访问,带上它,就能访问服务器端@auth_token_required保护的所有路由了

# ajax send request:
GET http://localhost:5000/token_protected
Headers: Authentication-Token WyIxIiwiNWY0ZGNjM2I1Yxxx

到此,我们Server端,即可以管理Session(网页),又可以管理Token(REST访问)了!


Quickstart源码

源码很短,60来行

  1. 保存为app.py,然后运行python app.py
  2. 浏览器Session尝试:http://localhost:5000/loginhttp://localhost:5000/logouthttp://localhost:5000/register
  3. JSON Token尝试:使用Curl或浏览器REST插件,来模拟Ajax GET/POST
# encoding: utf-8
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, \
    UserMixin, RoleMixin, login_required, auth_token_required, http_auth_required

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['SECURITY_TRACKABLE'] = True
app.config['SECURITY_REGISTERABLE'] = True
app.config['SECURITY_SEND_REGISTER_EMAIL'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///.security-dev.sqlite'

# Create database connection object
db = SQLAlchemy(app)

# Define models
roles_users = db.Table('roles_users',
        db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
        db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))

class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=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'))
    def __repr__(self):
        return '<User %r>' % self.email

# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)

# Create a user to test with
@app.before_first_request
def create_user():
    db.create_all()
    if not User.query.first():
        user_datastore.create_user(email='aaa@bbb.com', password='password')
        db.session.commit()

# Views
@app.route('/')
@login_required
def home():
    return 'you\'re logged in!'

@app.route('/api')
#@http_auth_required
@auth_token_required
def token_protected():
    return 'you\'re logged in by Token!'
    
if __name__ == '__main__':
    app.run()

TODO:
后台管理,一般用Flask-Admin。它也可以由Flask-Security来保护
My source code

参考:
Flask-Admin Use Flask-Security to authenticate users
Token Based Authentication with Flask-Security

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容