使用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 ...
随机推荐
- 《学技术练英语》PPT分享
之前做的一个PPT,分享给博客园的同学. 下载地址: 学技术练英语.pdf 技术是靠自己去学的,学技术不能仅仅是看书看博客,最好是有实践,不管是做实验去验证,还是写各种代码去玩各种特性,还是造轮子都是 ...
- [Java Collection]List分组之简单应用.
前言 今天有一个新需求, 是对一个List进行分组, 于是便百度到一些可用的代码以及我们项目使用的一些tools, 在这里总结下方便以后查阅. 一: 需求 现在我们一个数据库表t_series_val ...
- PHP 面向对象编程和设计模式 (4/5) - 异常的定义、扩展及捕获
PHP高级程序设计 学习笔记 2014.06.12 异常经常被用来处理一些在程序正常执行中遇到的各种类型的错误.比如做数据库链接时,你就要处理数据库连接失败的情况.使用异常可以提高我们程序的容错特性, ...
- markdown常用语法总结
转自markdown示例[模板] 1.1.段落标题 根据原文中的文档标题可以对应设置标题. # 一级标题## 二级标题### 三级标题 效果 => 一级标题 二级标题 三级标题 1.2.斜体.加 ...
- Centos下搭建 tomcat https服务器详解(原创)
一 .安装java jdk配置环境变量 1. 卸载原有openjdk yum -y remove java-1.7.0-openjdk* yum -y remove tzdata-java.noarc ...
- spring框架之javaconfig
简介:随着java5的推出,加上当年基于纯java annotation的依赖注入框架Guice的出现,spring推出并持续完善了基于java代码和annotation元信息的依赖关系绑定描述方法, ...
- 一个Java文件至多包含一个公共类
编写一个java源文件时,该源文件又称为编译单元.一个java文件可以包含多个类,但至多包含一个公共类,作为编译时该java文件的公用接口,公共类的名字和源文件的名字要相同,源文件名字的格式为[公共类 ...
- PHP unserialize()
定义和用法 unserialize() 将已序列化的字符串还原回 PHP 的值. 序列化请使用 serialize() 函数. 语法 unserialize(str) 参数 描述 str 必需.一个序 ...
- 记录一次bug解决过程:eclipse Installed JREs 配置引出的问题
一 总结 eclipse Installed JREs 配置引出的问题:编译以来JDK,不是JRE spring boot内嵌tomcat运行程序,tomcat:run 二 Bug描述:eclipse ...
- gearman 安装
yum install gperfyum install libevent-develyum install libuuid-develwget https://launchpad.net/gearm ...