在這個(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_url
和 scope
。假設(shè)在GitHub里注冊App后拿到的Client ID和Client Secret分別叫 client_id
和 client_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è)小封裝骇钦,我們下篇再見宛渐。