一個(gè)OpenResty里OAuth 2認(rèn)證的輪子(中)

精巧的Lua

在這個(gè)小教程的上篇丸边,我們搭了一個(gè)非常簡單的Docker容器叠必,并把OpenResty跑起來了。在中篇我們來開始探索一下OpenResty的一些用法妹窖。作為一個(gè)Python程序員纬朝,我可能會(huì)不時(shí)地拿出Flask來做對比。當(dāng)然骄呼,first things first共苛,最重要的還是祭出OpenResty官方文檔

首先我們在之前的 nginx.conf 配置里谒麦,看到了這么一句 ngx.say(...)俄讹,這就相關(guān)于往請求方發(fā)信息,有點(diǎn)Flask里的返回一個(gè)Response對象的意思绕德。不過這里并沒有那么多面向?qū)ο笤O(shè)計(jì)的條條框框患膛,一個(gè)請求結(jié)束,甭管是JSON還是HTML耻蛇,都是返回一串東西踪蹬。

我想大家多少都有玩過一些Nginx的基本配置胞此,知道大概怎么寫location block,Apache也是差不多一個(gè)路數(shù)跃捣。我們現(xiàn)在就來寫這個(gè)Demo服務(wù)的登陸接口漱牵。這個(gè)接口的行為就是,在被訪問的時(shí)候疚漆,返回OAuth平臺對應(yīng)的跳轉(zhuǎn)登陸網(wǎng)址酣胀,所以會(huì)是一個(gè)30X的跳轉(zhuǎn)請求。通過查詢文檔娶聘,我們看到對應(yīng)的OpenResty API是 ngx.redirect闻镶。再通過查詢GitHub的登陸說明,取code的API在https://github.com/login/oauth/authorize丸升,必填的參數(shù)為 client_id铆农,redirect_urlscope。假設(shè)在GitHub里注冊App后拿到的Client ID和Client Secret分別叫 client_idclient_secret狡耻,那我們的這個(gè)新location block就可以這么寫:

location /auth/github/login {
  content_by_lua_block {
    local params = ngx.encode_args({
      client_id = client_id,
      redirect_uri = 'http://test.myoauth.com/auth-cb/github/login',
      scope = 'user',
    })
    local url = 'https://github.com/login/oauth/authorize?' .. params
    ngx.log(ngx.DEBUG, url)
    return ngx.redirect(url)
  }
}

稍微解釋一下兩個(gè)地方:ngx.encode_arg 會(huì)把一個(gè)Lua的table進(jìn)行URL路徑轉(zhuǎn)義墩剖,變成請求參數(shù)字符串;ngx.log 經(jīng)過我們一開始的配置夷狰,日志等級為DEBUG岭皂,統(tǒng)一寫到 resty/logs/error.log 這個(gè)文件里。現(xiàn)在請干掉容器進(jìn)程再up一遍沼头,然后訪問 http://localhost/auth/github/login蒲障。

$ curl -I http://localhost/auth/github/login
HTTP/1.1 302 Moved Temporarily
Server: openresty/1.11.2.3
Date: Fri, 30 Jun 2017 06:39:29 GMT
Content-Type: text/html
Content-Length: 167
Connection: keep-alive
Location: https://github.com/login/oauth/authorize?redirect_uri=http%3A%2F%2Ftest.myoauth.com%2Fauth-callback%2Flogin%2Fcallback&scope=user

瀏覽器里打開就會(huì)被跳轉(zhuǎn)到GitHub的登錄頁。登錄以后跳轉(zhuǎn)回來說找不到DNS瘫证?當(dāng)然了揉阎,那個(gè)回調(diào)URL是我亂寫的,去host文件里加成127.0.0.1就會(huì)連到本機(jī)背捌,就會(huì)出一個(gè)404咯毙籽。好啦,接下來的東西就是自己讀文檔啦毡庆!

……

逗你的坑赡,不過應(yīng)該通過這幾個(gè)入門例子大家已經(jīng)知道怎么搜文檔找API了。

現(xiàn)在我們已經(jīng)可以跳轉(zhuǎn)到OAuth平臺么抗,讓用戶在平臺上登陸毅否,然后平臺會(huì)重新把用戶導(dǎo)回來,并帶上一串 &code=blahblahblah蝇刀。這個(gè) code 就是OAuth登陸的第一步螟加,獲取認(rèn)證碼;我們接下來會(huì)用認(rèn)證碼來換取令牌符;最后再用令牌符去獲得用戶信息捆探。

一個(gè)很自然的問題就是然爆,接下來的步驟這么復(fù)雜,難道都直接把Lua代碼強(qiáng)行寫在這個(gè)Nginx配置里黍图?有沒有辦法把模塊分出來曾雕?

答案是肯定的,不過我要配置一個(gè)指令助被,在Nginx配置的http block里加上一句 lua_package_path '$prefix/lua/?.lua;;';剖张。$prefix 代表Nginx的啟動(dòng)位置,就是我們的 resty 文件夾揩环。這樣我們就可以再創(chuàng)建一個(gè) resty/lua 文件夾修械,把我們的Lua代碼放在里面,再從Nginx的配置里引用到它們了检盼。

另一個(gè)問題,每次修改代碼和配置翘单,現(xiàn)在都需要重啟一下容器來重啟OpenResty吨枉,有沒有在開發(fā)階段省事一些的方法?答案也是肯定的哄芜。如果把代碼寫到外部的Lua腳本里再引用進(jìn)來的話貌亭,可以同樣在http block里加上這一句 lua_code_cache off;,就可以讓每個(gè)請求獨(dú)立運(yùn)行一遍Lua腳本认臊。默認(rèn)的行為是在一次運(yùn)行之后圃庭,OpenResty會(huì)把Lua腳本Cache住,LuaJIT也會(huì)做對應(yīng)的JIT編譯優(yōu)化失晴,來保證線上服務(wù)的性能剧腻。但是我們在開發(fā)階段需要的是代碼熱重載,就把這個(gè)腳本緩存先關(guān)掉了涂屁。

現(xiàn)在我們有了一個(gè)代碼模塊化的機(jī)制书在,優(yōu)先考慮的當(dāng)然是創(chuàng)建一個(gè)OAuth相關(guān)的模塊:

-- resty/lua/oauth.lua

local cjson = require('cjson.safe')
cjson.encode_empty_table_as_object(false)

local M = {}
local _conf = nil

local function code_url()
  local params = ngx.encode_args({
    client_id = _conf.client_id,
    redirect_uri = _conf.redirect_uri,
    scope = _conf.scope,
  })
  return _conf.code_endpoint .. '?' .. params
end

function M.get_code()
  return ngx.redirect(code_url())
end

function M.get_profile(code)
  ngx.say(cjson.encode({ code = code })
end

function M.init(conf)
  _conf = conf
end

return M

用一個(gè) M 元表來裝住所有暴露出去的接口是Lua常用的一種模塊定義方式。cjson 是OpenResty提供的一個(gè)JSON工具庫拆又。這里把OAuth平臺跳轉(zhuǎn)回來的回調(diào)函數(shù)也定義好了儒旬,雖然什么都沒做,只是把code原樣返回√澹現(xiàn)在我們要在Nginx配置里找一個(gè)合適的時(shí)候初始化這個(gè)模塊栈源,在對應(yīng)的請求到來的時(shí)候調(diào)用它們:

http {
  include /usr/local/openresty/nginx/conf/mime.types;
  lua_package_path '$prefix/lua/?.lua;;';
  lua_code_cache off;

  # 就是這里了,init 的時(shí)候也 init 我們的模塊
  init_by_lua_block {
    require('oauth').init({
      code_endpoint = 'https://github.com/login/oauth/authorize',
      token_endpoint = 'https://github.com/login/oauth/access_token',
      profile_endpoint = 'https://api.github.com/user',
      client_id = 'client_id',
      client_secret = 'client_secret',
      redirect_uri = 'http://test.myoauth.com/auth-cb/github/login',
      scope = 'user',
    })
  }

  server {
    listen 80;

    location / {
      content_by_lua_block {
        ngx.say('hello world')
      }
    }

    location /auth/github/login {
      content_by_lua_block {
        return require('oauth').get_code()
      }
    }

    location /auth-cb/github/login {
      content_by_lua_block {
        return require('oauth').get_profile(ngx.var.arg_code)
      }
    }
  }
}

接下來我們要面對的問題就比較復(fù)雜了:拿到臨時(shí)的誰碼以后竖般,我們需要在服務(wù)器里發(fā)兩個(gè)請求甚垦,一個(gè)用來換取令牌符,一個(gè)用來查詢用戶信息。這時(shí)我們就會(huì)需要之前Dockerfile里最下面那行用OPM安裝的resty-http庫來發(fā)起服務(wù)器端的網(wǎng)絡(luò)請求制轰。OpenResty的庫文檔和就是代碼倉庫里的README前计,大家可以直接去項(xiàng)目主頁圍觀。不過我習(xí)慣對這個(gè)庫做一個(gè)淺封裝來統(tǒng)一我的錯(cuò)誤處理:

-- resty/lua/requests.lua

local http = require('resty.http')
local cjson = require('cjson.safe')
cjson.encode_empty_table_as_object(false)

local errors = {
  UNAVAILABLE = 'upstream-unavailable',
  QUERY_ERROR = 'query-failed'
}

local M = { errors = errors }

local function request(method)
  return function(url, payload, headers)
    headers = headers or {}
    headers['Content-Type'] = 'application/json'
    local httpc = http.new()
    local params = { headers = headers, method = method }
    if method == 'GET' then params.query = payload
    else params.body = payload end
    local res, err = httpc:request_uri(url, params)
    if err then
      ngx.log(ngx.ERR, table.concat(
        {method .. ' fail', url, payload}, '|'
      ))
      return nil, nil, errors.UNAVAILABLE
    else
      if res.status >= 400 then
        ngx.log(ngx.ERR, table.concat({
          method .. ' fail code', url, res.status, res.body,
        }, '|'))
        return res.status, res.body, errors.QUERY_ERROR
      else
        return res.status, res.body, nil
      end
    end
  end
end

M.jget = request('GET')
M.jput = request('PUT')
M.jpost = request('POST')

return M

上面這段腳本用了一個(gè)函數(shù)式編程里很常用的模式叫柯里化垃杖。Lua的精巧就在于它用類C的語法實(shí)現(xiàn)了Scheme里最核心的函數(shù)式思想男杈,和ES5一樣,提供了一個(gè)很好的語言核心调俘;但勝過ES5的地方又在于它很早就支持了協(xié)程伶棒,讓它能如絲般順滑地被集成到Nginx的異步事件循環(huán)中……

啊扯遠(yuǎn)了,我把這個(gè)封裝過的工具庫叫 requests.lua(簡直不要臉——用Python的童鞋輕噴)彩库。它也是我們在下篇里會(huì)繼續(xù)擴(kuò)展的 oauth.lua 里要調(diào)用的一個(gè)基礎(chǔ)庫肤无。大家可以先嘗試著讀一讀lua-resty-http的文檔,玩一玩這個(gè)小封裝骇钦,我們下篇再見宛渐。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市眯搭,隨后出現(xiàn)的幾起案子窥翩,更是在濱河造成了極大的恐慌,老刑警劉巖鳞仙,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寇蚊,死亡現(xiàn)場離奇詭異,居然都是意外死亡棍好,警方通過查閱死者的電腦和手機(jī)仗岸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來借笙,“玉大人扒怖,你說我怎么就攤上這事∫导冢” “怎么了姚垃?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盼忌。 經(jīng)常有香客問我积糯,道長,這世上最難降的妖魔是什么谦纱? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任看成,我火速辦了婚禮,結(jié)果婚禮上跨嘉,老公的妹妹穿的比我還像新娘川慌。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布梦重。 她就那樣靜靜地躺著兑燥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琴拧。 梳的紋絲不亂的頭發(fā)上降瞳,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音蚓胸,去河邊找鬼挣饥。 笑死,一個(gè)胖子當(dāng)著我的面吹牛沛膳,可吹牛的內(nèi)容都是我干的扔枫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼锹安,長吁一口氣:“原來是場噩夢啊……” “哼短荐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起叹哭,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤忍宋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后话速,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡芯侥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年泊交,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柱查。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡廓俭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唉工,到底是詐尸還是另有隱情研乒,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布淋硝,位于F島的核電站雹熬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谣膳。R本人自食惡果不足惜竿报,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望继谚。 院中可真熱鬧烈菌,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至济瓢,卻和暖如春荠割,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背葬荷。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工涨共, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宠漩。 一個(gè)月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓举反,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扒吁。 傳聞我的和親對象是個(gè)殘疾皇子火鼻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

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