上一篇文章, 使用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