使用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格式的对象,状态码为,并且在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 CREATED
Content-Type: application/json
Content-Length:
Location: http://127.0.0.1:5000/api/users/1
Server: Werkzeug/0.9. Python/2.7.
Date: Thu, Nov :: 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 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.9. Python/2.7.
Date: Thu, Nov :: GMT {
"data": "Hello, ok!"
}
如果登录错误,会返回以下内容:
$ curl -u miguel:ruby -i -X GET http://127.0.0.1:5000/api/resource
HTTP/1.0 UNAUTHORIZED
Content-Type: text/html; charset=utf-
Content-Length:
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/0.9. Python/2.7.
Date: Thu, Nov :: 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 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.9. Python/2.7.
Date: Thu, Nov :: 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 OK
Content-Type: application/json
Content-Length:
Server: Werkzeug/0.9. Python/2.7.
Date: Thu, Nov :: 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接口
大数据时代 Just a record. 使用Flask设计带认证token的RESTful API接口[翻译] 上一篇文章, 使用python的Flask实现一个RESTful API服务器端 简 ...
- 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 ...
随机推荐
- .Net使用Redis详解之ServiceStack.Redis(七)
序言 本篇从.Net如何接入Reis开始,直至.Net对Redis的各种操作,为了方便学习与做为文档的查看,我做一遍注释展现,其中会对list的阻塞功能和事务的运用做二个案例,进行记录学习. Redi ...
- Java Collection开发技巧
Java Collection(集合) 集合中的一些技巧: 通过Collections类的静态方法,可以对集合进行一些操作 1 java.util.List<Integer> number ...
- c 数组与指针的使用注意事项
数组变量和指针变量有一点小小的区别 所以把数组指针赋值给指针变量的时候千万要小心 加入把数组赋值给指针变量,指针变量只会包含数组的地址信息 而对数组的长度一无所知 相当于指针丢失了一部分信息,我们把这 ...
- 野路子出身PowerShell 文件操作实用功能
本文出处:http://www.cnblogs.com/wy123/p/6129498.html 因工作需要,处理一批文件,本想写C#来处理的,后来想想这个是PowerShell的天职,索性就网上各种 ...
- Mybatis框架 的快速入门
MyBatis 简介 什么是 MyBatis? MyBatis 是支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架.MyBatis 消除 了几乎所有的 JDBC 代码和参数的手工设置以及结果 ...
- 11.JAVA之GUI编程菜单
功能:添加菜单组件 知识总结: 代码如下: import java.awt.FlowLayout; import java.awt.Frame; import java.awt.Menu; impor ...
- VS2015企业版,社区版,专业版详细对比
VS2015 微软出了3个大版本,其实在前天晚上就放出了三个版本的对比说明.,但是昨天挂掉了..今天特意去看了..截取了自己觉得比较重要的分享一下. 首先我们最常用的 诊断调试工具 其次测试工具(区别 ...
- SQL Server里在文件组间如何移动数据?
平常我不知道被问了几次这样的问题:“SQL Server里在文件组间如何移动数据?“你意识到这个问题:你只有一个主文件组的默认配置,后来围观了“SQL Server里的文件和文件组”后,你知道,有多 ...
- Jsp的九大对象,七大动作,三大指令
jsp九大内置对象:1>out 向客户端输出数据,字节流.如out.print(" dgaweyr"); 2>request 接收客户端的http请求.String g ...
- session的使用方法详解
session的使用方法详解 Session是什么呢?简单来说就是服务器给客户端的一个编号.当一台WWW服务器运行时,可能有若干个用户浏览正在运正在这台服务器上的网站.当每个用户首次与这台WWW服务器 ...