一个OpenResty里OAuth 2认证的轮子(补遗)

上篇含全系列链接:传送门

如果你有认真读第一篇博客,你可能也会注意到阮老师博客里的回复,有人提到了OAuth标准里有一个参数,叫 state,少了它的话网站就有可能受到CSRF攻击。这个参数到底怎么用呢?为什么少了就有CSRF攻击的漏洞呢?其实这个问题我曾经也纠结过很久,去年读到一篇文章,终于明白CSRF攻击是怎么实现的了——然而那篇文章也找不到了。大体思路其实就是攻击的网站需要支持多账号绑定,然后攻击人把自己的认证码放到被劫持用户的回调中,就可以把自己的账号绑定为被劫持用户,获取网站的个人信息甚至进行交易。为了写这篇博客,刚刚发现Spring的文档里有一篇很不错的说明

……
“不不不我并不会写Java。”
……
“不不不我一直很敬佩写Java的人,真心的!”

好了,大家都理解 state 参数的重要性以后,问题就来了:我(wa)们(jue)怎(ji)么(ji)加(shu)到(dao)流(di)程(na)里(jia)呢(qiang)?

其实思路很简单,在用户访问登录页的时候,除了返回一个跳转让用户去OAuth平台认证App以外,还要随机生成一个 state 值,写到这个请求的session里。这样在用户被跳转回来以后,发的请求就有两个地方有 state 值——session里和URL请求参数里。服务器在获取用户信息之前要先检查URL请求参数里的 state 和session里的是不是一致,不一致的话基本上就是出现CSRF攻击了。

这就是为什么在Dockerfile里我们装上了lua-resty-session这个库。

好了,又到了贴代码的时间!下面是扩展后的跳转阶段逻辑,主要就是写了一个session。lua-resty-session支持多种session的存储机制,我这里偷懒用了最简单的方法,直接放在Cookie里,具体用法大家可以自己读一下文档,不是很难。然后那个 state 的生成表达式是我网上抄来的,就是Lua里生成随机字符串的一个方法:

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 是不是相同的逻辑了。我们对 oauth.lua 模块的 M.get_profile 做下面的扩展:

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

好了!核心的逻辑就是这样啦!剩下的就是配置了。大家有没有注意到这里有一个 _conf.csrf_unprotect,这个是为了支持那些不靠谱的OAuth提供方做的一个小配置跳过 state 检查。然后这里要提一下lua-resty-session在代码缓存关掉时候的一个小坑。这里就不展开说明了,大家读一下文档,记得在Nginx配置的server block里加上这么一个变量就好:

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';
  ...

这样我们整个OAuth的认证流程就非常完整了!

简直好棒棒!

以下为扩展补充材料,看和不看差不了多少,主要涉及到的方法都来自GitHub上大牛们的慷慨相助

其实还有一个小事情,虽然问题不大,却让我纠结了非常非常久……LuaJIT在工程上有一个很麻烦的因素,就是我至今没有找到很好的方案来在大项目中做JIT检查。

LuaJIT有一份文档,记录了哪些调用不能被JIT编译。这个东西实在太细碎、太依赖程序员自身的细心程度和工程经验了。在有一个很好的Linter之前,我觉得会是一个工程推广上蛮大的阻碍。

如果在NYI页面里搜Closure的话,会发现闭包会产生FNEW这个字节码调用,还有可能会有UCLO,这两个调用都不能被JIT编译。那我们的 requests.lua 里用的柯里化会不会有问题呢?这里其实不会,因为在生产环境下我们会把代码缓存打开,然后LuaJIT在把闭包函数赋值给 M 上的字段以后,对这个字段的调用就不会再动态生成新的函数了。下面贴上LuaJIT的 v模块dump模块对两种调用的dump。

我们有三个文件:

$ 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,就会发现动态生成的闭包函数是没办法JIT编译的,而如果把动态生成后的函数赋值给一个变量再反复调用它,就不会有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

好了,这下我知道的、能勉强算点干货的东西,就全写完了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容