一個(gè)OpenResty里OAuth 2認(rèn)證的輪子(補(bǔ)遺)

上篇含全系列鏈接:傳送門

如果你有認(rèn)真讀第一篇博客吱晒,你可能也會(huì)注意到阮老師博客里的回復(fù)引颈,有人提到了OAuth標(biāo)準(zhǔn)里有一個(gè)參數(shù),叫 state凯肋,少了它的話網(wǎng)站就有可能受到CSRF攻擊谊惭。這個(gè)參數(shù)到底怎么用呢?為什么少了就有CSRF攻擊的漏洞呢侮东?其實(shí)這個(gè)問題我曾經(jīng)也糾結(jié)過很久圈盔,去年讀到一篇文章,終于明白CSRF攻擊是怎么實(shí)現(xiàn)的了——然而那篇文章也找不到了苗桂。大體思路其實(shí)就是攻擊的網(wǎng)站需要支持多賬號(hào)綁定药磺,然后攻擊人把自己的認(rèn)證碼放到被劫持用戶的回調(diào)中,就可以把自己的賬號(hào)綁定為被劫持用戶煤伟,獲取網(wǎng)站的個(gè)人信息甚至進(jìn)行交易癌佩。為了寫這篇博客木缝,剛剛發(fā)現(xiàn)Spring的文檔里有一篇很不錯(cuò)的說明

……
“不不不我并不會(huì)寫Java围辙∥业”
……
“不不不我一直很敬佩寫Java的人,真心的姚建!”

好了矫俺,大家都理解 state 參數(shù)的重要性以后,問題就來了:我(wa)們(jue)怎(ji)么(ji)加(shu)到(dao)流(di)程(na)里(jia)呢(qiang)掸冤?

其實(shí)思路很簡單厘托,在用戶訪問登錄頁的時(shí)候,除了返回一個(gè)跳轉(zhuǎn)讓用戶去OAuth平臺(tái)認(rèn)證App以外稿湿,還要隨機(jī)生成一個(gè) state 值铅匹,寫到這個(gè)請(qǐng)求的session里。這樣在用戶被跳轉(zhuǎn)回來以后饺藤,發(fā)的請(qǐng)求就有兩個(gè)地方有 state 值——session里和URL請(qǐng)求參數(shù)里包斑。服務(wù)器在獲取用戶信息之前要先檢查URL請(qǐng)求參數(shù)里的 state 和session里的是不是一致,不一致的話基本上就是出現(xiàn)CSRF攻擊了涕俗。

這就是為什么在Dockerfile里我們裝上了lua-resty-session這個(gè)庫罗丰。

好了,又到了貼代碼的時(shí)間再姑!下面是擴(kuò)展后的跳轉(zhuǎn)階段邏輯萌抵,主要就是寫了一個(gè)session。lua-resty-session支持多種session的存儲(chǔ)機(jī)制询刹,我這里偷懶用了最簡單的方法谜嫉,直接放在Cookie里,具體用法大家可以自己讀一下文檔凹联,不是很難沐兰。然后那個(gè) state 的生成表達(dá)式是我網(wǎng)上抄來的,就是Lua里生成隨機(jī)字符串的一個(gè)方法:

local random = require('resty.random')
local str = require('resty.string')
local S = require('resty.session')

function M.get_code(next_page)
  local state = str.to_hex(random.bytes(16))
  local session = S.start()
  if next_page then session.data.next_page = next_page end
  session.data.state = state
  session:save()
  return ngx.redirect(code_url(state))
end

接下來就是檢查 state 是不是相同的邏輯了蔽挠。我們對(duì) oauth.lua 模塊的 M.get_profile 做下面的擴(kuò)展:

local function is_valid_state(state)
  if _conf.csrf_unprotect then
    return true
  else
    local session = S.open()
    local saved_state = session.data.state
    return state == saved_state
  end
end

function M.get_profile(code, state)
  -- 檢查失敗的話就直接扔400
  if not is_valid_state(state) then
    ngx.say('{"msg": "invalid-state"}')
    return ngx.exit(ngx.HTTP_BAD_REQUEST)
  end
  local token = get_token(code)
  local profile = get_profile(token)
  ngx.say(cjson.encode(profile))
end

好了住闯!核心的邏輯就是這樣啦!剩下的就是配置了澳淑。大家有沒有注意到這里有一個(gè) _conf.csrf_unprotect比原,這個(gè)是為了支持那些不靠譜的OAuth提供方做的一個(gè)小配置跳過 state 檢查。然后這里要提一下lua-resty-session在代碼緩存關(guān)掉時(shí)候的一個(gè)小坑杠巡。這里就不展開說明了量窘,大家讀一下文檔,記得在Nginx配置的server block里加上這么一個(gè)變量就好:

server {
  listen 80;
  lua_ssl_verify_depth 10;
  lua_ssl_trusted_certificate '/etc/ssl/certs/ca-certificates.crt';
  # Remember to add this line!!!
  set $session_secret 'a-highly-secretive-string';
  ...

這樣我們整個(gè)OAuth的認(rèn)證流程就非常完整了氢拥!

簡直好棒棒蚌铜!

以下為擴(kuò)展補(bǔ)充材料锨侯,看和不看差不了多少,主要涉及到的方法都來自GitHub上大牛們的慷慨相助

其實(shí)還有一個(gè)小事情冬殃,雖然問題不大囚痴,卻讓我糾結(jié)了非常非常久……LuaJIT在工程上有一個(gè)很麻煩的因素,就是我至今沒有找到很好的方案來在大項(xiàng)目中做JIT檢查审葬。

LuaJIT有一份文檔深滚,記錄了哪些調(diào)用不能被JIT編譯。這個(gè)東西實(shí)在太細(xì)碎涣觉、太依賴程序員自身的細(xì)心程度和工程經(jīng)驗(yàn)了痴荐。在有一個(gè)很好的Linter之前,我覺得會(huì)是一個(gè)工程推廣上蠻大的阻礙旨枯。

如果在NYI頁面里搜Closure的話蹬昌,會(huì)發(fā)現(xiàn)閉包會(huì)產(chǎn)生FNEW這個(gè)字節(jié)碼調(diào)用,還有可能會(huì)有UCLO攀隔,這兩個(gè)調(diào)用都不能被JIT編譯。那我們的 requests.lua 里用的柯里化會(huì)不會(huì)有問題呢栖榨?這里其實(shí)不會(huì)昆汹,因?yàn)樵谏a(chǎn)環(huán)境下我們會(huì)把代碼緩存打開,然后LuaJIT在把閉包函數(shù)賦值給 M 上的字段以后婴栽,對(duì)這個(gè)字段的調(diào)用就不會(huì)再動(dòng)態(tài)生成新的函數(shù)了满粗。下面貼上LuaJIT的 v模塊dump模塊對(duì)兩種調(diào)用的dump。

我們有三個(gè)文件:

$ cat mylib.lua
local function closure()
    return function () end
end

local M = {}
M.dynamic_call = closure
M.closure_free = closure()
return M

$ cat dynamic.lua
local mymod = require('mylib')
for i = 1, 1000000 do mymod.dynamic_call()() end

$ cat fixed.lua
local mymod = require('mylib')
for i = 1, 1000000 do mymod.closure_free() end

如果用LuaJIT的v模塊來看JIT trace愚争,就會(huì)發(fā)現(xiàn)動(dòng)態(tài)生成的閉包函數(shù)是沒辦法JIT編譯的映皆,而如果把動(dòng)態(tài)生成后的函數(shù)賦值給一個(gè)變量再反復(fù)調(diào)用它,就不會(huì)有JIT abort:

$ luajit -jv dynamic.lua
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE   1 mylib.lua:4 return]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- dynamic.lua:2 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]
[TRACE --- mylib.lua:3 -- NYI: bytecode 51 at mylib.lua:4]

$ luajit -jv fixed.lua
[TRACE   1 fixed.lua:2 loop]

用dump來看的話就更明顯了:

$ luajit -jdump dynamic.lua
---- TRACE 1 start mylib.lua:3
0001  FNEW     0   0      ; mylib.lua:4
---- TRACE 1 abort mylib.lua:4 -- NYI: bytecode 51

---- TRACE 1 start dynamic.lua:2
0008  TGETS    5   0   2  ; "dynamic_call"
0009  CALL     5   2   1
0000  . FUNCF    1          ; mylib.lua:3
0001  . FNEW     0   0      ; mylib.lua:4
---- TRACE 1 abort mylib.lua:4 -- NYI: bytecode 51

---- TRACE 1 start mylib.lua:3
0001  FNEW     0   0      ; mylib.lua:4
---- TRACE 1 abort mylib.lua:4 -- NYI: bytecode 51
...

---- TRACE 1 start mylib.lua:4
0001  RET0     0   1
---- TRACE 1 IR
---- TRACE 1 mcode 29
10f66ffdc  mov dword [0x00042410], 0x1
10f66ffe7  xor eax, eax
10f66ffe9  mov ebx, 0x00054acc
10f66ffee  mov r14d, 0x00042fa8
10f66fff4  jmp 0x100005ce9
---- TRACE 1 stop -> return

---- TRACE 2 start mylib.lua:3
0001  FNEW     0   0      ; mylib.lua:4
---- TRACE 2 abort mylib.lua:4 -- NYI: bytecode 51

---- TRACE 2 start dynamic.lua:2
0008  TGETS    5   0   2  ; "dynamic_call"
0009  CALL     5   2   1
0000  . FUNCF    1          ; mylib.lua:3
0001  . FNEW     0   0      ; mylib.lua:4
---- TRACE 2 abort mylib.lua:4 -- NYI: bytecode 51
...


$ luajit -jdump fixed.lua
---- TRACE 1 start fixed.lua:2
0008  TGETS    5   0   2  ; "closure_free"
0009  CALL     5   1   1
0000  . FUNCF    1          ; mylib.lua:4
0001  . RET0     0   1
0010  FORL     1 => 0008
---- TRACE 1 IR
0001    int SLOAD  #2    CI
0002 >  tab SLOAD  #1    T
0003    int FLOAD  0002  tab.hmask
0004 >  int EQ     0003  +1
0005    p32 FLOAD  0002  tab.node
0006 >  p32 HREFK  0005  "closure_free" @0
0007 >  fun HLOAD  0006
0008 >  fun EQ     0007  mylib.lua:4
0009  + int ADD    0001  +1
0010 >  int LE     0009  +1000000
0011 ------ LOOP ------------
0012  + int ADD    0009  +1
0013 >  int LE     0012  +1000000
0014    int PHI    0009  0012
---- TRACE 1 mcode 114
f125ff8e  mov dword [0x00042410], 0x1
f125ff99  cvttsd2si ebp, [rdx+0x8]
f125ff9e  cmp dword [rdx+0x4], -0x0c
f125ffa2  jnz 0xf1250010    ->0
f125ffa8  mov ecx, [rdx]
f125ffaa  cmp dword [rcx+0x1c], +0x01
f125ffae  jnz 0xf1250010    ->0
f125ffb4  mov eax, [rcx+0x14]
f125ffb7  mov rdi, 0xfffffffb000521a8
f125ffc1  cmp rdi, [rax+0x8]
f125ffc5  jnz 0xf1250010    ->0
f125ffcb  cmp dword [rax+0x4], -0x09
f125ffcf  jnz 0xf1250010    ->0
f125ffd5  cmp dword [rax], 0x00062160
f125ffdb  jnz 0xf1250010    ->0
f125ffe1  add ebp, +0x01
f125ffe4  cmp ebp, 0x000f4240
f125ffea  jg 0xf1250014 ->1
->LOOP:
f125fff0  add ebp, +0x01
f125fff3  cmp ebp, 0x000f4240
f125fff9  jle 0xf125fff0    ->LOOP
f125fffb  jmp 0xf125001c    ->3
---- TRACE 1 stop -> loop

好了轰枝,這下我知道的捅彻、能勉強(qiáng)算點(diǎn)干貨的東西,就全寫完了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鞍陨,一起剝皮案震驚了整個(gè)濱河市步淹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诚撵,老刑警劉巖缭裆,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寿烟,居然都是意外死亡澈驼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門筛武,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缝其,“玉大人购桑,你說我怎么就攤上這事∈鲜纾” “怎么了勃蜘?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長假残。 經(jīng)常有香客問我缭贡,道長,這世上最難降的妖魔是什么辉懒? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任阳惹,我火速辦了婚禮,結(jié)果婚禮上眶俩,老公的妹妹穿的比我還像新娘莹汤。我一直安慰自己,他們只是感情好颠印,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布纲岭。 她就那樣靜靜地躺著,像睡著了一般线罕。 火紅的嫁衣襯著肌膚如雪止潮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天钞楼,我揣著相機(jī)與錄音喇闸,去河邊找鬼。 笑死询件,一個(gè)胖子當(dāng)著我的面吹牛燃乍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宛琅,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼刻蟹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了夯秃?” 一聲冷哼從身側(cè)響起座咆,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仓洼,沒想到半個(gè)月后介陶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡色建,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年哺呜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箕戳。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡某残,死狀恐怖国撵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情玻墅,我是刑警寧澤介牙,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站澳厢,受9級(jí)特大地震影響环础,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剩拢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一线得、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧徐伐,春花似錦贯钩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摸屠,卻和暖如春谓罗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背季二。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揭措,地道東北人胯舷。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像绊含,于是被迫代替她去往敵國和親桑嘶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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