使用Flask設計帶認證token的RESTful API接口[翻譯]上一篇文章, 使用python的Flask實現(xiàn)一個RESTful API服務器端 簡單地演示了Flask實的現(xiàn)的api服務器,里面提到了因為無狀態(tài)的原則,沒有session cookies,如果訪問需要驗證的接口,客戶端請求必需每次都發(fā)送用戶名和密碼。通常在實際app應用中,并不會每次都將用戶名和密碼發(fā)送。 這篇里面就談到了產(chǎn)生token的方法。 完整的例子的代碼可以在github:REST-auth 上找到。作者歡迎大家上去跟他討論。 創(chuàng)建用戶數(shù)據(jù)庫這個例子比較接近真實的項目,將會使用Flask-SQLAlchemy (ORM)的模塊去管理用戶數(shù)據(jù)庫。 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)) 因為安全的原因,明文密碼不可以直接存儲,必需經(jīng)過hash后方可存入數(shù)據(jù)庫。如果數(shù)據(jù)庫被脫了,也是比較難破解的。 密碼永遠不要明文存在數(shù)據(jù)庫中。 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) 當一個新的用戶注冊,或者更改密碼時,就會調(diào)用hash_password()函數(shù),將原始密碼作為參數(shù)傳入hash_password()函數(shù)。 當驗證用戶密碼時就會調(diào)用verify_password()函數(shù),如果密碼正確,就返回True,如果不正確就返回False。 hash算法是單向的,意味著它只能hash密碼,但是無法還原密碼。但是這些算法是絕對可靠的,輸入相同的內(nèi)容,那么hash后的內(nèi)容也會是一樣的。通常注冊或者驗證時,對比的是hash后的結(jié)果。 用戶注冊在這個例子里,客戶端通過發(fā)送 POST 請求到 /api/users 上,并且請求的body部份必需是JSON格式,并且包含 username 和 password 字段。 Flask 實現(xiàn)的代碼: @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)} 這個函數(shù)真是簡單極了。只是用請求的JSON里面拿到 username 和 password 兩個參數(shù)。 如果參數(shù)驗證通過,一個User實例被創(chuàng)建,密碼hash后,用戶資料就存到數(shù)據(jù)庫里面了。 請求響應返回的是一個JSON格式的對象,狀態(tài)碼為201,并且在http header里面定義了Location指向剛剛創(chuàng)建的用戶的URI。 注意:get_user函數(shù)沒有在這里實現(xiàn),具體查以查看github。 試試使用curl發(fā)送一個注冊請求: $ 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通訊。這樣的登錄方式,明文通訊是很容易被截取的。 基于簡單密碼的認證現(xiàn)在我們假設有一個API只向已經(jīng)注冊好的用戶開放。接入點是/api/resource。 這里使用HTTP BASIC Authentication的方法來進行驗證,我計劃使用Flask-HTTPAuth這個擴展來實現(xiàn)這個功能。 導入Flask-HTTPAuth擴展模塊后,為對應的函數(shù)添加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裝飾器)需要知道如何驗證用戶信息,這就需要具體去實現(xiàn)安全驗證的方法了。 有一種辦法是十分靈活的,通過實現(xiàn)verify_password回調(diào)函數(shù)去驗證用戶名和密碼,驗證通過返回True,否則返回False。然后Flask-HTTPAuth再調(diào)用這個回調(diào)函數(shù),這樣就可以輕松自定義驗證方法了。(注:Python修飾器的函數(shù)式編程) 具體實現(xiàn)代碼如下: @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 存儲在應用上下文中而不再是請求上下文中,這意味著即使在應用上下文中它也是可訪問的而不是只能在請求上下文中。)方便其它函數(shù)使用。 讓我們使用已經(jīng)注冊的用戶來請求看看: $ 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!" } 如果登錄錯誤,會返回以下內(nèi)容: $ 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的認證因為需要每次請求都要發(fā)送用戶名和密碼,客戶端需要把驗證信息存儲起來進行發(fā)送,這樣十分不方便,就算在HTTPS下的傳輸,也是有風險存在的。 比前面的密碼驗證方法更好的是使用Token認證請求。 原理是第一次客戶端與服務器交換過認證信息后得到一個認證token,后面的請求就使用這個token進行請求。 Token通常會給一個過期的時間,當超過這個時間后,就會變成無效,需要產(chǎn)生一個新的token。這樣就算token泄漏了,危害也只是在有效的時間內(nèi)。 好多種辦法去實現(xiàn)token。一種簡單的做法就是產(chǎn)生一個固定長度的隨機序列字符與用戶名和密碼一同存儲在數(shù)據(jù)庫當中,有可能帶上一個過期時間。這樣token就變成了一串普通的字符,可以十分容易地和其它字符串驗證對比,并且可以檢查時間是否過期。 更復雜的實現(xiàn)辦法是不需要服務器端進行存儲token,而是使用數(shù)字簽名信息作為token。這樣做的好處是經(jīng)過用戶數(shù)字簽名生成的token是可以防篡改的。 Flask使用與數(shù)字簽名有些相似的辦法去實現(xiàn)加密的cookies的,這里我們使用itsdangerous的庫去實現(xiàn)。 生成token和驗證token的方法可以附加到User model上實現(xiàn): 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()函數(shù)中,token其實就是一個加密過的字典,里面包含了用戶的id和默認為10分鐘(600秒)的過期時間。 verify_auth_token()的實現(xiàn)是一個靜態(tài)方法,因為token只是一次解碼檢索里面的用戶id。獲取用戶id后就可以在數(shù)據(jù)庫中取得用戶資料了。 試試使用一個新的接入點,讓客戶端請求一個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協(xié)議沒有具體要求必需使用用戶名和密碼進行驗證,HTTP頭可以使用兩個字段去傳輸認證信息,對于token認證,只需要把token當成用戶名發(fā)送即可,密碼字段可以乎略。 綜上所說,一些認證還是要使用用戶名和密碼認證,另外一部份直接使用獲取的token認證。verify_password回調(diào)函數(shù)則需要包括兩種驗證的方式: @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回調(diào)函數(shù),添加兩種驗證。開始用用戶名字段當作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協(xié)議。 So what is OAuth? 通常是允許一個應用接入到另外一個應用的數(shù)據(jù)或者服務的驗證方法。 舉個例子,如果一個網(wǎng)站或者應用問你權(quán)限接入你的facebook賬號,并且提交一些東西到你的時間軸上面。這個例子,你就是資源擁有者(你擁有你的facebook時間軸),第三方應用是消費者,facebook是提供者。如果你授權(quán)接入允許消費者寫東西到你的時間軸上面,是不需要提供你的facebook登錄信息的。 OAuth并不合適用在client/server的RESTful API上面,一般是用在你的RESTful API允許第三方應用(消費者)去接入。 上面的例子是,客戶端/服務器端之間直接通訊并不需要去隱藏認證信息,客戶端是直接發(fā)送認證請求信息到服務器端的。
分類: Python
|
|