本文是在 基礎上, 講功能模塊:API認證的具體實現,方便大家可以按需擴展或者實現自己的認證方法。
因為CMDB實現是前後端完全分離的,客戶端向API服務發起請求時,服務需要驗證客戶端的身份是否合法。HTTP API常見的認證方法有以下幾種:
- API Key + Secret
- Cookie-Session
- JWT(JSON Web Token)
- OAuth
本系統實現了前3種認證方式,下面講講實現過程,並不會分析這幾種認證方式的優缺點,網上有很多文章做了深入的對比分析。
API Key + Secret
服務端會每個用戶自動生成一個密鑰對(Key和Secret),Key相當於標識用戶的身份,Secret則是用來加密的秘鑰。實際上這種認證方式的簽名算法並沒有統一的標準,因此實現各異。本系統的實現方法如下圖,當然大家可以按需實現自己的簽名算法。
https://github.com/pycook/cmdb/blob/master/cmdb-api/api/lib/perm/auth.py
<code>def _auth_with_key():
key = request.values.get('_key')
secret = request.values.get('_secret')
path = request.path
keys = sorted(request.values.keys())
req_args = [request.values[k] for k in keys if k not in ("_key", "_secret")]
user, authenticated = User.query.authenticate_with_key(key, secret, req_args, path)
if user and authenticated:
login_user(user)
return True
return False/<code>
<code>def authenticate_with_key(self, key, secret, args, path):
user = self.filter(User.key == key).filter(User.deleted.is_(False)).filter(User.block == 0).first()
if not user:
return None, False
if user and hashlib.sha1('{0}{1}{2}'.format(
path, user.secret, "".join(args)).encode("utf-8")).hexdigest() == secret:
authenticated = True
else:
authenticated = False
return user, authenticated/<code>
簽名過程如下:
- 客戶端首先拿到自己的Key,Secret
- 請求的參數,按照參數名的字典序排列,並連接到url path + Secret之後
- SHA1(步驟2的字符串)
- HTTP請求的參數里加上_key和_secret, 其中_key就是用戶的API Key, _secret就是步驟3生成的簽名
客戶端請求籤名可以實現如下:
<code>def build_api_key(path, params):
key = "your key"
secret = "your secret"
values = "".join([str(params[k]) for k in sorted(params.keys())
if params[k] is not None]) if params.keys() else ""
_secret = "".join([path, secret, values]).encode("utf-8")
params["_secret"] = hashlib.sha1(_secret).hexdigest()
params["_key"] = key
return params/<code>
這種方法需要每次HTTP請求的時候都要進行簽名!
Cookie-Session
這種認證方法適用於web客戶端即瀏覽器請求API的場景。其認證的過程大致如下:
- 用戶在登錄頁面輸入用戶名、密碼後,密碼取md5值,點登錄POST請求給API認證服務
- 服務收到認證請求後,驗證用戶是否合法,如果不合法返回401錯誤,否則使用HMAC-SHA1對用戶信息進行簽名
- 服務響應登錄請求,會把步驟2生成的帶有過期時間的session寫進瀏覽器的cookie中
- 後面的的HTTP請求都會帶上這個加密的cookie,服務便可識別用戶的身份
步驟2的簽名,是Flask使用itsdangerous來對用戶認證信息進行簽名和序列化。
簽名流程:
- 使用itsdangerous的session_json_serializer對session dict進行序列化生成pyload
- 使用 HMAC-SHA1(SECRET_KEY, salt = 'cookie-session')生成key
- 用第二步生成的key對第一步的序列化的session進行簽名, payload + '.' + base64_encode(HAMC-SHA1(key, payload))
JWT
JWT(JSON Web Token)是一種用以產生訪問令牌的開源標準, 簡單來說就是對JSON數據進行簽名生成Token。JWT生成的數據如下圖:
JWT由3部分組成:
- base64_encode(header)
- base64_encode(payload)
- HMAC-HS256(base64_encode(header) + "." + base64_encode(payload), SECRET_KEY)
header和payload長下面這樣,都是JSON格式的數據
header一般用來存放元數據,payload會存放用戶的信息,過期時間等,但是敏感信息不能存放在payload裡,因為畢竟是base64編碼,算是明文。第三部分用HMAC-HS256進行簽名,無非是防止前面2部分數據被篡改。
CMDB裡在用戶通過認證後會生成一個JWT的Token,其代碼如下:
https://github.com/pycook/cmdb/blob/master/cmdb-api/api/views/account.py
<code>token = jwt.encode({
'sub': user.email,
'iat': datetime.datetime.now(),
'exp': datetime.datetime.now() + datetime.timedelta(minutes=24 * 60 * 7)},
current_app.config['SECRET_KEY'])/<code>
用戶認證通過後會拿到這個token,後面請求在HTTP Headers裡設置Access-Token為該值,便可以通過認證,其基於JWT的認證代碼如下:
https://github.com/pycook/cmdb/blob/master/cmdb-api/api/views/account.py
<code>def _auth_with_token():
auth_headers = request.headers.get('Access-Token', '').strip()
if not auth_headers:
return False
try:
token = auth_headers
data = jwt.decode(token, current_app.config['SECRET_KEY'])
user = User.query.filter_by(email=data['sub']).first()
if not user:
return False
login_user(user)
return True
except jwt.ExpiredSignatureError:
return False
except (jwt.InvalidTokenError, Exception):
return False/<code>
閱讀更多 運維雜談 的文章