大資料時代
Just a record.
使用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格式的對象,狀态碼為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