今天的文章介紹一種適用于restful+json的API認證方法逆日,這個方法是基于jwt痊远,并且加入了一些從oauth2.0借鑒的改良宿百。
1. 常見的幾種實現(xiàn)認證的方法
首先要明白,認證和鑒權(quán)是不同的楞黄。認證是判定用戶的合法性兄裂,鑒權(quán)是判定用戶的權(quán)限級別是否可執(zhí)行后續(xù)操作句旱。這里所講的僅含認證阳藻。認證有幾種方法:
1.1 basic auth
這是http協(xié)議中所帶帶基本認證晰奖,是一種簡單為上的認證方式。原理是在每個請求的header中添加用戶名和密碼的字符串(格式為“username:password”腥泥,用base64編碼)匾南。
這種方式相當(dāng)于將“用戶名:密碼”綁定為一個開放式證書,這會有幾個問題:
- 每次請求都需要用戶名密碼蛔外,如果此連接未使用SSL/TLS蛆楞,或加密被破解,用戶名密碼基本就暴露了夹厌;
- 無法注銷用戶的登錄狀態(tài)豹爹;
- 證書不會過期,除非修改密碼矛纹。
總體來說臂聋,這種方法的特點就是,簡單但不安全或南。
1.2cookie
將認證的結(jié)果存在客戶端的cookie中孩等,通過檢查cookie中的身份信息來作為認證結(jié)果。
這種方式的特點是便捷采够,且只需要一次認證肄方,多次可用;也可以注銷登錄狀態(tài)和設(shè)置過期時間蹬癌;甚至也有辦法(比如設(shè)置httpOnly)來避免XSS攻擊权她。
但它的缺點十分明顯,使用cookie那便是有狀態(tài)的服務(wù)了逝薪。
1.3 token
JWT協(xié)議似乎已經(jīng)應(yīng)用十分廣泛隅要,JSON Web Token——一種基于token的json格式web認證方法∫砻觯基本的原理是拾徙,第一次認證通過用戶名密碼,服務(wù)端簽發(fā)一個json格式的token感局。后續(xù)客戶端的請求都攜帶這個token尼啡,服務(wù)端僅需要解析這個token暂衡,來判別客戶端的身份和合法性。
而JWT協(xié)議僅僅規(guī)定了這個協(xié)議的格式(<a >RFC7519</a>)崖瞭,它的序列生成方法在JWS協(xié)議中描述(https://tools.ietf.org/html/rfc7515)狂巢,分為三個部分:
1.3.1 header頭部:
聲明類型,這里是jwt
聲明加密的算法 通常直接使用 HMAC SHA256
一種常見的頭部是這樣的:
{
'typ': 'JWT',
'alg': 'HS256'
}
再將其進行base64編碼书聚。
1.3.2 payload載荷:
payload是放置實際有效使用信息的地方唧领。JWT定義了幾種內(nèi)容,包括:
- 標(biāo)準(zhǔn)中注冊的聲明雌续,如簽發(fā)者斩个,接收者,有效時間(exp)驯杜,時間戳(iat,issued at)等受啥;為官方建議但非必須
- 公共聲明
- 私有聲明
一個常見的payload是這樣的:
{'user_id': 123456,
'user_role': admin,
'iat': 1467255177}
事實上,payload中的內(nèi)容是自由的鸽心,按照自己開發(fā)的需要加入滚局。
Ps.有個小問題。使用itsdangerous包的TimedJSONWebSignatureSerializer進行token序列生成的結(jié)果顽频,exp是在頭部里的藤肢。這里似乎違背了jwt的協(xié)議規(guī)則。
1.3.3 signature
存儲了序列化的secreate key和salt key糯景。這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串嘁圈,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分莺奸。
2. 認證需求
目標(biāo)場景是一個前后端分離的后端系統(tǒng)丑孩,用于運維工作,雖在內(nèi)網(wǎng)使用灭贷,也有一定的保密性要求温学。
- API為restful+json的無狀態(tài)接口,要求認證也是相同模式
- 可橫向擴展
- 較低數(shù)據(jù)庫壓力
- 證書可注銷
- 證書可自動延期
選擇JWT甚疟。
3. JWT實現(xiàn)
2.1 如何生成token
這里使用python模塊itsdangerous仗岖,這個模塊能做很多編碼工作,其中一個是實現(xiàn)JWS的token序列览妖。
genTokenSeq這個函數(shù)用于生成token轧拄。其中使用的是TimedJSONWebSignatureSerializer進行序列的生成,這里secret_key密鑰讽膏、salt鹽值從配置文件中讀取檩电,當(dāng)然也可以直接寫死在這里。expires_in是超時時間間隔,這個間隔以秒記俐末,可以直接在這里設(shè)置料按,我選擇將其設(shè)為方法的形參(因為這個函數(shù)也用在了解決下提到的問題2)。
# serializer for JWT
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
"""
token is generated as the JWT protocol.
JSON Web Tokens(JWT) are an open, industry standard RFC 7519 method
"""
def genTokenSeq(self, expires):
s = Serializer(
secret_key=app.config['SECRET_KEY'],
salt=app.config['AUTH_SALT'],
expires_in=expires)
timestamp = time.time()
return s.dumps(
{'user_id': self.user_id,
'user_role': self.role_id,
'iat': timestamp})
# The token contains userid, user role and the token generation time.
# u can add sth more inside, if needed.
# 'iat' means 'issued at'. claimed in JWT.
使用這個Serializer可以幫我們處理好header卓箫、signature的問題载矿。我們只需要用s.dumps將payload的內(nèi)容寫進來。這里我準(zhǔn)備在每個token中寫入三個值:用戶id烹卒、用戶角色id和當(dāng)前時間(‘iat’是JWT標(biāo)準(zhǔn)注冊聲明中的一項)闷盔。
假設(shè)我所寫入的信息是
{
"iat": 1467271277.131803,
"user_id": "46501228343b11e6aaa6a45e60ed5ed5f973ba0fcf783bb8ade34c7b492d9e55",
"user_role": 3
}
采用以上的方法所生成的token為
eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ2NzM0MTQ3NCwiaWF0IjoxNDY3MzM3ODc0fQ.eyJpYXQiOjE0NjczMzc4NzQuNzE3MDYzLCJ1c2VyX2lkIjoiNDY1MDEyMjgzNDNiMTFlNmFhYTZhNDVlNjBlZDVlZDVmOTczYmEwZmNmNzgzYmI4YWRlMzRjN2I0OTJkOWU1NSIsInVzZXJfcm9sZSI6M30.23QD0OwLjdioKu5BgbaH2gHT2GoMz90n8VZcpvdyp7U
它是由“header.payload.signature”構(gòu)成的。
3.2 如何解析token
解析需要使用到同樣的serializer旅急,配置一樣的secret key和salt逢勾,使用loads方法來解析token。itsdangerous提供了各種異常處理類坠非,用起來也很方便:
如果是SignatureExpired敏沉,則可以直接返回過期;
如果是BadSignature,則代表了所有其他簽名錯誤的情況炎码,于是又分為:
- 能讀取到payload:那么這個消息是一個內(nèi)容被篡改、消息體加密過程正確的消息秋泳,secret key和salt很可能泄露了潦闲;
- 不能讀取到payload: 消息體直接被篡改,secret key和salt應(yīng)該仍然安全迫皱。
以上內(nèi)容寫成一個函數(shù)歉闰,用于驗證用戶token。如果實現(xiàn)在python flask卓起,可以考慮將此函數(shù)改為一個decorator修飾漆和敬,將修飾器@到所有需要驗證token的方法前面,則代碼可以更加優(yōu)雅戏阅。
# serializer for JWT
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
# exceptions for JWT
from itsdangerous import SignatureExpired, BadSignature, BadData
# Class xxx
# after definition of your class, here goes the auth method:
def tokenAuth(token):
# token decoding
s = Serializer(
secret_key=api.app.config['SECRET_KEY'],
salt=api.app.config['AUTH_SALT'])
try:
data = s.loads(token)
# token decoding faild
# if it happend a plenty of times, there might be someone
# trying to attact your server, so it should be a warning.
except SignatureExpired:
msg = 'token expired'
app.logger.warning(msg)
return [None, None, msg]
except BadSignature, e:
encoded_payload = e.payload
if encoded_payload is not None:
try:
s.load_payload(encoded_payload)
except BadData:
# the token is tampered.
msg = 'token tampered'
app.logger.warning(msg)
return [None, None, msg]
msg = 'badSignature of token'
app.logger.warning(msg)
return [None, None, msg]
except:
msg = 'wrong token with unknown reason'
app.logger.warning(msg)
return [None, None, msg]
if ('user_id' not in data) or ('user_role' not in data):
msg = 'illegal payload inside'
app.logger.warning(msg)
return [None, None, msg]
msg = 'user(' + data['user_id'] + ') logged in by token.'
# app.logger.info(msg)
userId = data['user_id']
roleId = data['user_role']
return [userId, roleId, msg]
檢查和判定的機制如下:
- 使用加密的類昼弟,再用來解密(用上之前的密鑰和鹽值),得到結(jié)果存入data奕筐;
- 如果捕獲到SignatureExpired異常舱痘,則代表根據(jù)token中的expired設(shè)置,token已經(jīng)超時失效离赫,返回‘token expired’芭逝;
- 如果是其他BadSignature異常,又要分為:
3.1 如果payload還完整渊胸,則解析payload旬盯,如果捕獲BadData異常,則代表token已經(jīng)被篡改,返回‘token tampered’胖翰;
3.2 如果payload不完整频丘,直接返回‘badSignature of token’; - 如果以上異常都不對泡态,那只能返回未知異陈‘wrong token with unknown reason’;
- 最后某弦,如果data能正常解析桐汤,則將payload中的數(shù)據(jù)取出來,驗證payload中是否有合法信息(這里是user_id和user_role鍵值的json數(shù)據(jù))靶壮,如果數(shù)據(jù)不合法怔毛,則返回‘illegal payload inside’。一旦出現(xiàn)這種情況腾降,則代表密鑰和鹽值泄露的可能性很大拣度。
4. 優(yōu)化
上述的方法可以做到基本的JWT認證,但在實際開發(fā)過程中還有其他問題:
token在生成之后螃壤,是靠expire使其過期失效的抗果。簽發(fā)之后的token,是無法收回修改的奸晴,因此涉及token的有效期的更改是個難題冤馏,它體現(xiàn)在以下兩個問題:
- 問題1.用戶登出
- 問題2.token自動延期
如何解決更改token有效期的問題,網(wǎng)上看到很多討論寄啼,主要集中在以下內(nèi)容:
- JWT是一次性認證完畢加載信息到token里的逮光,token的信息內(nèi)含過期信息。過期時間過長則被重放攻擊的風(fēng)險太大墩划,而過期時間太短則請求端體驗太差(動不動就要重新登錄)
- 把token存進庫里涕刚,很自然能想到的是把每個token存庫,設(shè)置一個valid字段乙帮,一旦注銷了就valid=0杜漠;設(shè)置有效期字段,想要延期就增加有效期時間蚣旱。openstack keystone就是這么做的碑幅。這個做法雖方便,但對數(shù)據(jù)庫的壓力較大塞绿,甚至在訪問量較大沟涨,簽發(fā)token較多的情況下,是對數(shù)據(jù)庫的一個挑戰(zhàn)异吻。況且這也有悖于JWT的初衷喜庞。
- 為了使用戶不需要經(jīng)常重新登錄,客戶端將用戶名密碼保存起來(cookie)棋返,然后使用用戶名密碼驗證延都,但那還得考慮防御CSRF攻擊的問題。
這里睛竣,筆者借鑒了第三方認證協(xié)議Oauth2.0(<a >RFC6749</a>)晰房,它采取了另一種方法:refresh token,一個用于更新令牌的令牌射沟。在用戶首次認證后殊者,簽發(fā)兩個token:
- 一個為access token,用于用戶后續(xù)的各個請求中攜帶的認證信息
- 另一個是refresh token验夯,為access token過期后猖吴,用于申請一個新的access token。
由此可以給兩類不同token設(shè)置不同的有效期挥转,例如給access token僅1小時的有效時間海蔽,而refresh token則可以是一個月。api的登出通過access token的過期來實現(xiàn)(前端則可直接拋棄此token實現(xiàn)登出)绑谣,在refresh token的存續(xù)期內(nèi)党窜,訪問api時可執(zhí)refresh token申請新的access token(前端可存此refresh token,access token過其實進行更新域仇,達到自動延期的效果)刑然。
refresh token不可再延期,過期需重新使用用戶名密碼登錄暇务。
這種方式的理念在于,將證書分為三種級別:
- access token 短期證書怔软,用于最終鑒權(quán)
- refresh token 較長期的證書垦细,用于產(chǎn)生短期證書,不可直接用于服務(wù)請求
- 用戶名密碼 幾乎永久的證書挡逼,用于產(chǎn)生長期證書和短期證書括改,不可直接用于服務(wù)請求
通過這種方式,使證書功效和證書時效結(jié)合考慮家坎。
ps.前面提到創(chuàng)建token的時候?qū)xpire_in(jwt的推薦字段嘱能,超時時間間隔)作為函數(shù)的形參,是為了將此函數(shù)用于生成access token和refresh token虱疏,而兩者的expire_in時間是不同的惹骂。
5. 總結(jié)一下
我們做了一個JWT的認證模塊:
(access token在以下代碼中為'token',refresh token在代碼中為'rftoken')
- 首次認證
client -----用戶名密碼-----------> server
client <------token做瞪、rftoken----- server
- access token存續(xù)期內(nèi)的請求
client ------請求(攜帶token)----> server
client <-----結(jié)果----------------- server
- access token超時
client ------請求(攜帶token)----> server
client <-----msg:token expired--- server
- 重新申請access token
client -請求新token(攜帶rftoken)-> server
client <-----新token-------------- server
- rftoken token超時
client -請求新token(攜帶rftoken)-> server
client <----msg:rftoken expired--- server
如果設(shè)計一個針對此認證的前端对粪,需要:
存儲access token右冻、refresh token
訪問時攜帶access token,自動檢查access token超時著拭,超時則使用refresh token更新access token纱扭;狀態(tài)延期用戶無感知
用戶登出直接拋棄access token與refresh token