使用Flask设计带认证token的RESTful API接口
大数据时代
Just a record.
使用Flask设计带认证token的RESTful API接口[翻译]
上一篇文章, 使用python的Flask实现一个RESTful API服务器端 简单地演示了Flask实的现的api服务器,里面提到了因为无状态的原则,没有session cookies,如果访问需要验证的接口,客户端请求必需每次都发送用户名和密码。通常在实际app应用中,并不会每次都将用户名和密码发送。
这篇里面就谈到了产生token的方法。
完整的例子的代码
可以在github:REST-auth 上找到。作者欢迎大家上去跟他讨论。
创建用户数据库
这个例子比较接近真实的项目,将会使用Flask-SQLAlchemy (ORM)的模块去管理用户数据库。
user model 非常简单。每个用户只有 username 和 password_hash 两个属性。
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(32), index = True)
password_hash = db.Column(db.String(128))
因为安全的原因,明文密码不可以直接存储,必需经过hash后方可存入数据库。如果数据库被脱了,也是比较难破解的。
密码永远不要明文存在数据库中。
Password Hashing
这里使用PassLib库对密码进行hash。
PassLib提供几种hash算法。custom_app_context模块是基于sha256_crypt加密算法,使用十分简单。
对User model增加密码hash和验证有两办法:

from passlib.apps import custom_app_context as pwd_context class User(db.Model):
# ... def hash_password(self, password):
self.password_hash = pwd_context.encrypt(password) def verify_password(self, password):
return pwd_context.verify(password, self.password_hash)

当一个新的用户注册,或者更改密码时,就会调用hash_password()函数,将原始密码作为参数传入hash_password()函数。
当验证用户密码时就会调用verify_password()函数,如果密码正确,就返回True,如果不正确就返回False。
hash算法是单向的,意味着它只能hash密码,但是无法还原密码。但是这些算法是绝对可靠的,输入相同的内容,那么hash后的内容也会是一样的。通常注册或者验证时,对比的是hash后的结果。
用户注册
在这个例子里,客户端通过发送 POST 请求到 /api/users 上,并且请求的body部份必需是JSON格式,并且包含 username 和 password 字段。
Flask 实现的代码:

@app.route('/api/users', methods = ['POST'])
def new_user():
username = request.json.get('username')
password = request.json.get('password')
if username is None or password is None:
abort(400) # missing arguments
if User.query.filter_by(username = username).first() is not None:
abort(400) # existing user
user = User(username = username)
user.hash_password(password)
db.session.add(user)
db.session.commit()
return jsonify({ 'username': user.username }), 201, {'Location': url_for('get_user', id = user.id, _external = True)}

这个函数真是简单极了。只是用请求的JSON里面拿到 username 和 password 两个参数。
如果参数验证通过,一个User实例被创建,密码hash后,用户资料就存到数据库里面了。
请求响应返回的是一个JSON格式的对象,状态码为201,并且在http header里面定义了Location指向刚刚创建的用户的URI。
注意:get_user函数没有在这里实现,具体查以查看github。
试试使用curl发送一个注册请求:

$ curl -i -X POST -H "Content-Type: application/json" -d '{"username":"ok","password":"python"}' http://127.0.0.1:5000/api/users
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 27
Location: http://127.0.0.1:5000/api/users/1
Server: Werkzeug/0.9.4 Python/2.7.3
Date: Thu, 28 Nov 2013 19:56:39 GMT {
"username": "ok"
}

通常在正式的服务器里面,最好还是使用https通讯。这样的登录方式,明文通讯是很容易被截取的。
基于简单密码的认证
现在我们假设有一个API只向已经注册好的用户开放。接入点是/api/resource。
这里使用HTTP BASIC Authentication的方法来进行验证,我计划使用Flask-HTTPAuth这个扩展来实现这个功能。
导入Flask-HTTPAuth扩展模块后,为对应的函数添加login_required装饰器:

from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth() @app.route('/api/resource')
@auth.login_required
def get_resource():
return jsonify({ 'data': 'Hello, %s!' % g.user.username })

那么Flask-HTTPAuth(login_required装饰器)需要知道如何验证用户信息,这就需要具体去实现安全验证的方法了。
有一种办法是十分灵活的,通过实现verify_password回调函数去验证用户名和密码,验证通过返回True,否则返回False。然后Flask-HTTPAuth再调用这个回调函数,这样就可以轻松自定义验证方法了。(注:Python修饰器的函数式编程)
具体实现代码如下:

@auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username = username).first()
if not user or not user.verify_password(password):
return False
g.user = user
return True

如果用户名与密码验证通过,user对像会被存储到Flask的g对像中。(注:对象 g 存储在应用上下文中而不再是请求上下文中,这意味着即使在应用上下文中它也是可访问的而不是只能在请求上下文中。)方便其它函数使用。
让我们使用已经注册的用户来请求看看:

$ curl -u ok:python -i -X GET http://127.0.0.1:5000/api/resource
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 30
Server: Werkzeug/0.9.4 Python/2.7.3
Date: Thu, 28 Nov 2013 20:02:25 GMT {
"data": "Hello, ok!"
}

如果登录错误,会返回以下内容:

$ curl -u miguel:ruby -i -X GET http://127.0.0.1:5000/api/resource
HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/0.9.4 Python/2.7.3
Date: Thu, 28 Nov 2013 20:03:18 GMT Unauthorized Access

再次重申,真实的API服务器最好在HTTPS下通讯。
基于Token的认证
因为需要每次请求都要发送用户名和密码,客户端需要把验证信息存储起来进行发送,这样十分不方便,就算在HTTPS下的传输,也是有风险存在的。
比前面的密码验证方法更好的是使用Token认证请求。
原理是第一次客户端与服务器交换过认证信息后得到一个认证token,后面的请求就使用这个token进行请求。
Token通常会给一个过期的时间,当超过这个时间后,就会变成无效,需要产生一个新的token。这样就算token泄漏了,危害也只是在有效的时间内。
好多种办法去实现token。一种简单的做法就是产生一个固定长度的随机序列字符与用户名和密码一同存储在数据库当中,有可能带上一个过期时间。这样token就变成了一串普通的字符,可以十分容易地和其它字符串验证对比,并且可以检查时间是否过期。
更复杂的实现办法是不需要服务器端进行存储token,而是使用数字签名信息作为token。这样做的好处是经过用户数字签名生成的token是可以防篡改的。
Flask使用与数字签名有些相似的办法去实现加密的cookies的,这里我们使用itsdangerous的库去实现。
生成token和验证token的方法可以附加到User model上实现:

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer class User(db.Model):
# ... def generate_auth_token(self, expiration = 600):
s = Serializer(app.config['SECRET_KEY'], expires_in = expiration)
return s.dumps({ 'id': self.id }) @staticmethod
def verify_auth_token(token):
s = Serializer(app.config['SECRET_KEY'])
try:
data = s.loads(token)
except SignatureExpired:
return None # valid token, but expired
except BadSignature:
return None # invalid token
user = User.query.get(data['id'])
return user

在generate_auth_token()函数中,token其实就是一个加密过的字典,里面包含了用户的id和默认为10分钟(600秒)的过期时间。
verify_auth_token()的实现是一个静态方法,因为token只是一次解码检索里面的用户id。获取用户id后就可以在数据库中取得用户资料了。
试试使用一个新的接入点,让客户端请求一个token:
@app.route('/api/token')
@auth.login_required
def get_auth_token():
token = g.user.generate_auth_token()
return jsonify({ 'token': token.decode('ascii') })
注意,这个接入点是被Flask-HTTPAuth扩展的auth.login_required装饰器保护的,请求需要提供用户名和密码。
上面返回的是一个token字符串,下面的请求将会包含这个token。
HTTP Basic Authentication协议没有具体要求必需使用用户名和密码进行验证,HTTP头可以使用两个字段去传输认证信息,对于token认证,只需要把token当成用户名发送即可,密码字段可以乎略。
综上所说,一些认证还是要使用用户名和密码认证,另外一部份直接使用获取的token认证。verify_password回调函数则需要包括两种验证的方式:

@auth.verify_password
def verify_password(username_or_token, password):
# first try to authenticate by token
user = User.verify_auth_token(username_or_token)
if not user:
# try to authenticate with username/password
user = User.query.filter_by(username = username_or_token).first()
if not user or not user.verify_password(password):
return False
g.user = user
return True

修改原来的verify_password回调函数,添加两种验证。开始用用户名字段当作token,如果不是token来的,就采用用户名和密码验证。
使用curl测试请求获取一个认证token:

$ curl -u ok:python -i -X GET http://127.0.0.1:5000/api/token
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 139
Server: Werkzeug/0.9.4 Python/2.7.3
Date: Thu, 28 Nov 2013 20:04:15 GMT {
"token": "eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4NTY2OTY1NSwiaWF0IjoxMzg1NjY5MDU1fQ.eyJpZCI6MX0.XbOEFJkhjHJ5uRINh2JA1BPzXjSohKYDRT472wGOvjc"
}

再试试使用token一访问受保护的API:

$ curl -u eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4NTY2OTY1NSwiaWF0IjoxMzg1NjY5MDU1fQ.eyJpZCI6MX0.XbOEFJkhjHJ5uRINh2JA1BPzXjSohKYDRT472wGOvjc:unused -i -X GET http://127.0.0.1:5000/api/resource
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 30
Server: Werkzeug/0.9.4 Python/2.7.3
Date: Thu, 28 Nov 2013 20:05:08 GMT {
"data": "Hello, ok!"
}

注意,请求里面带了unused字段。只是为了标识而已,替代密码的占位符。
OAuth 认证
谈到RESTful认证,通常会提到OAuth协议。
So what is OAuth?
通常是允许一个应用接入到另外一个应用的数据或者服务的验证方法。
举个例子,如果一个网站或者应用问你权限接入你的facebook账号,并且提交一些东西到你的时间轴上面。这个例子,你就是资源拥有者(你拥有你的facebook时间轴),第三方应用是消费者,facebook是提供者。如果你授权接入允许消费者写东西到你的时间轴上面,是不需要提供你的facebook登录信息的。
OAuth并不合适用在client/server的RESTful API上面,一般是用在你的RESTful API允许第三方应用(消费者)去接入。
上面的例子是,客户端/服务器端之间直接通讯并不需要去隐藏认证信息,客户端是直接发送认证请求信息到服务器端的。
原文来自:http://blog.miguelgrinberg.com/post/restful-authentication-with-flask
使用Flask设计带认证token的RESTful API接口的更多相关文章
- 使用Flask设计带认证token的RESTful API接口[翻译]
上一篇文章, 使用python的Flask实现一个RESTful API服务器端 简单地演示了Flask实的现的api服务器,里面提到了因为无状态的原则,没有session cookies,如果访问 ...
- Flask设计带认证token的RESTful API接口[翻译]
上一篇文章, 使用python的Flask实现一个RESTful API服务器端 简单地演示了Flask实的现的api服务器,里面提到了因为无状态的原则,没有session cookies,如果访问 ...
- Spring Boot入门系列(二十一)如何优雅的设计 Restful API 接口版本号,实现 API 版本控制!
前面介绍了Spring Boot 如何快速实现Restful api 接口,并以人员信息为例,设计了一套操作人员信息的接口.不清楚的可以看之前的文章:https://www.cnblogs.com/z ...
- Java 调用Restful API接口的几种方式--HTTPS
摘要:最近有一个需求,为客户提供一些Restful API 接口,QA使用postman进行测试,但是postman的测试接口与java调用的相似但并不相同,于是想自己写一个程序去测试Restful ...
- Restful API 接口设计标准及规范
Restful API 接口设计标准以及规范 RESTful概念 理解和评估以网络为基础的应用软件的架构设计,得到一个功能强.性能好.适宜通信的架构.REST指的是一组架构约束条件和原则." ...
- Spring Boot入门系列(二十)快速打造Restful API 接口
spring boot入门系列文章已经写到第二十篇,前面我们讲了spring boot的基础入门的内容,也介绍了spring boot 整合mybatis,整合redis.整合Thymeleaf 模板 ...
- 用 shell 脚本做 restful api 接口监控
问题的提出 基于历史原因,公司有一个"三无"采集服务--无人员.无运维.无监控--有能力做的部门不想接.接了的部门没能力.于是就一直这样裸奔,直到前几天一个依赖于这个采集服务的大数 ...
- Postman如何通过xmysql工具的Restful API 接口访问MySQL
GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 导语 有时候用 Postman 接口测试需要获取MySQL的查询结果做接口输出的校验,这里介绍下 Postman 通过 R ...
- SpringMVC Restful api接口实现
[前言] 面向资源的 Restful 风格的 api 接口本着简洁,资源,便于扩展,便于理解等等各项优势,在如今的系统服务中越来越受欢迎. .net平台有WebAPi项目是专门用来实现Restful ...
随机推荐
- hdu 2844 多重背包的转化问题 以及这个dp状态的确定
在杭电上测试了下 这里的状态转移方程有两个.,. 现在有价值val[1],val[2],…val[n]的n种硬币, 它们的数量分别为num[i]个. 然后给你一个m, 问你区间[1,m]内的所有数目, ...
- [Vue]导航守卫:全局的、单个路由独享的、组件级的
正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航.有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的. 记住参数或查询的改变并不会触发进入/离开的 ...
- VS2017清除工具、用于清除Microsoft Visual Studio最近打开项目
最近每天在用VS2017,但是每次打开它都会弹出最近项目的记录,很是烦人. 最主要是我不想别人得知我最近的项目和项目进度,每次加密项目会比较麻烦. 所以经过简单的研究,编写了这个小工具,打开直接单击就 ...
- .netcore 上传
BS 上传文件,就是 <input type="file" name="file" /> 这个选择文件之后,浏览器保存了文件路径,上传的时候,把这 ...
- luogu题解 P3950部落冲突--树链剖分
题目链接 https://www.luogu.org/problemnew/show/P3950 分析 大佬都用LCT,我太弱只会树链剖分 一个很裸的维护边权树链剖分题.按照套路,对于一条边\(< ...
- Go 修改字符串中的字符(中文乱码)
问题复现:修改字符串的第一个中文 先对原字符串做切片,然后进行拼接,得到新的字符串 func ModifyString(str string) string { tempStr := str[1:] ...
- Docker启动Elasticsearch报错vm.max_map_count
报错信息如下 max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144] 临 ...
- S2-032
前言 S2-032漏洞的影响范围是Struts 2.3.20 - Struts Struts 2.3.28,当开启了动态方法调用时可RCE.这次的漏洞分析以及后面的漏洞分析都是使用的Struts 2. ...
- Java入门学习总结_02
一:注释 注释主要就是用来解释某句或者某段代码使得其他人调试更加方便.特点是在编译代码的时候不会编译注释, 不管发不发生错误.注释主要分为单行注释//(快捷键ctrl+/).多行注释/* */(快捷键 ...
- linux同步onedrive文件
定时任务 # 开机自启动 @reboot /root/system/start.sh # 从零点开始每小时执行一次任务 0 0 0/1 * * ? nohup rclone sync onedrive ...