使用Flask設計帶認證token的RESTful API接口 使用Flask設計帶認證token的RESTful API接口[翻譯] 使用python的Flask實現一個RESTful API服務器

使用Flask設計帶認證token的RESTful API接口[翻譯]

上一篇文章, 使用python的Flask實現一個RESTful API服務器端  簡單地演示了Flask實的現的api服務器,裏面提到了由於無狀態的原則,沒有session cookies,若是訪問須要驗證的接口,客戶端請求必需每次都發送用戶名和密碼。一般在實際app應用中,並不會每次都將用戶名和密碼發送。python

這篇裏面就談到了產生token的方法。git

完整的例子的代碼

能夠在github:REST-auth 上找到。做者歡迎你們上去跟他討論。github

建立用戶數據庫

這個例子比較接近真實的項目,將會使用Flask-SQLAlchemy (ORM)的模塊去管理用戶數據庫。算法

user model 很是簡單。每一個用戶只有 username 和 password_hash 兩個屬性。shell

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。json

PassLib提供幾種hash算法。custom_app_context模塊是基於sha256_crypt加密算法,使用十分簡單。flask

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

相關文章
相關標籤/搜索