使用python實現(xiàn)后臺系統(tǒng)的JWT認證

今天的文章介紹一種適用于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]

檢查和判定的機制如下:

  1. 使用加密的類昼弟,再用來解密(用上之前的密鑰和鹽值),得到結(jié)果存入data奕筐;
  1. 如果捕獲到SignatureExpired異常舱痘,則代表根據(jù)token中的expired設(shè)置,token已經(jīng)超時失效离赫,返回‘token expired’芭逝;
  2. 如果是其他BadSignature異常,又要分為:
    3.1 如果payload還完整渊胸,則解析payload旬盯,如果捕獲BadData異常,則代表token已經(jīng)被篡改,返回‘token tampered’胖翰;
    3.2 如果payload不完整频丘,直接返回‘badSignature of token’;
  3. 如果以上異常都不對泡态,那只能返回未知異陈‘wrong token with unknown reason’;
  4. 最后某弦,如果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)容:

  1. JWT是一次性認證完畢加載信息到token里的逮光,token的信息內(nèi)含過期信息。過期時間過長則被重放攻擊的風(fēng)險太大墩划,而過期時間太短則請求端體驗太差(動不動就要重新登錄)
  2. 把token存進庫里涕刚,很自然能想到的是把每個token存庫,設(shè)置一個valid字段乙帮,一旦注銷了就valid=0杜漠;設(shè)置有效期字段,想要延期就增加有效期時間蚣旱。openstack keystone就是這么做的碑幅。這個做法雖方便,但對數(shù)據(jù)庫的壓力較大塞绿,甚至在訪問量較大沟涨,簽發(fā)token較多的情況下,是對數(shù)據(jù)庫的一個挑戰(zhàn)异吻。況且這也有悖于JWT的初衷喜庞。
  3. 為了使用戶不需要經(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市儡遮,隨后出現(xiàn)的幾起案子乳蛾,更是在濱河造成了極大的恐慌,老刑警劉巖鄙币,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肃叶,死亡現(xiàn)場離奇詭異,居然都是意外死亡爱榔,警方通過查閱死者的電腦和手機被环,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來详幽,“玉大人筛欢,你說我怎么就攤上這事〈狡福” “怎么了版姑?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長迟郎。 經(jīng)常有香客問我剥险,道長,這世上最難降的妖魔是什么宪肖? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任表制,我火速辦了婚禮,結(jié)果婚禮上控乾,老公的妹妹穿的比我還像新娘么介。我一直安慰自己,他們只是感情好蜕衡,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布壤短。 她就那樣靜靜地躺著,像睡著了一般慨仿。 火紅的嫁衣襯著肌膚如雪久脯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天镰吆,我揣著相機與錄音帘撰,去河邊找鬼。 笑死鼎姊,一個胖子當(dāng)著我的面吹牛骡和,可吹牛的內(nèi)容都是我干的相赁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼慰于,長吁一口氣:“原來是場噩夢啊……” “哼钮科!你這毒婦竟也來了诅福?” 一聲冷哼從身側(cè)響起询筏,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎跨新,沒想到半個月后休里,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛆挫,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年妙黍,在試婚紗的時候發(fā)現(xiàn)自己被綠了悴侵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拭嫁,死狀恐怖可免,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情做粤,我是刑警寧澤浇借,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站怕品,受9級特大地震影響妇垢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肉康,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一闯估、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吼和,春花似錦睬愤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砂豌。三九已至厢岂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阳距,已是汗流浹背塔粒。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留筐摘,地道東北人卒茬。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓船老,卻偏偏與公主長得像,于是被迫代替她去往敵國和親圃酵。 傳聞我的和親對象是個殘疾皇子柳畔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容