在Nginx中增加對(duì)OAuth協(xié)議的支持的教程
我們使用Nginx的Lua中間件建立了OAuth2認(rèn)證和授權(quán)層。如果你也有此打算,閱讀下面的文檔,實(shí)現(xiàn)自動(dòng)化并獲得收益。
SeatGeek在過去幾年中取得了發(fā)展,我們已經(jīng)積累了不少針對(duì)各種任務(wù)的不同管理接口。我們通常為新的展示需求創(chuàng)建新模塊,比如我們自己的博客、圖表等。我們還定期開發(fā)內(nèi)部工具來處理諸如部署、可視化操作及事件處理等事務(wù)。在處理這些事務(wù)中,我們使用了幾個(gè)不同的接口來認(rèn)證:
- Github/Google Oauth
- 我們SeatGeek內(nèi)部的用戶系統(tǒng)
- 基本認(rèn)證
- 硬編碼登錄
顯然,實(shí)際應(yīng)用中很不規(guī)范。多個(gè)認(rèn)證系統(tǒng)使得難以對(duì)用于訪問級(jí)別和通用許可的各種數(shù)據(jù)庫進(jìn)行抽象。
單系統(tǒng)認(rèn)證
我們也做了一些關(guān)于如何設(shè)置將解決我們問題的研究。這促使了Odin的出現(xiàn),它在驗(yàn)證谷歌應(yīng)用的用戶方面工作的很好。不幸的是它需要使用Apache,而我們已和Nginx結(jié)為連理并把它作為我們的后端應(yīng)用的前端。
幸運(yùn)的是,我看了mixlr的博客并引用了他們Lua在Nginx上的應(yīng)用:
- 修改響應(yīng)頭
- 重寫內(nèi)部請(qǐng)求
- 選擇性地基于IP拒絕主機(jī)訪問
最后一條看起來很有趣。它開啟了軟件包管理的地獄之旅。
構(gòu)建支持Lua的Nginx
Lua for Nginx沒有被包含在Nginx的核心中,我們經(jīng)常要為OSX構(gòu)建Nginx用于開發(fā)測(cè)試,為L(zhǎng)inux構(gòu)建用于部署。
為OSX定制Nginx
對(duì)于OSX系統(tǒng),我推薦使用Homebrew進(jìn)行包管理。它初始的Nginx安裝包啟用的模塊不多,這有非常好的理由:
關(guān)鍵在于NGINX有著如此之多的選項(xiàng),如果把它們都加入初始包那一定是瘋了,如果我們只把其中一些加入其中就會(huì)迫使我們把所有都加入,這會(huì)讓我們瘋掉的。
- Charlie Sharpsteen, @sharpie
所以我們需要自己構(gòu)建。合理地構(gòu)建Nginx可以方便我們以后繼續(xù)擴(kuò)展。幸運(yùn)的是,使用Homebrew進(jìn)行包管理十分方便快捷。
我們首先需要一個(gè)工作空間:
mkdir -p src
cd src
之后,我們需要找到初始安裝信息包。你可以通過下面任何一種方式得到它:
- 找到HOMEBREW_PREFIX目錄,通常在/usr/local下,在其中找到nginx.rb文件
- 從下列地址取得https://raw.github.com/mxcl/homebrew/master/Library/Formula/nginx.rb
- 使用如下命令 brew cat nginx > nginx.rb
此時(shí)如果我們執(zhí)行brew install ./nginx.rb命令, 它會(huì)依據(jù)其中的信息安裝Nginx。既然現(xiàn)在我們要完全定制Nginx,我們要重命名信息包,這樣之后通過brew update命令進(jìn)行更新的時(shí)候就不會(huì)覆蓋我們自定義的了:
cat nginx-custom.rb | sed 's/class Nginx/class NginxCustom/' >> tmp
rm nginx-custom.rb
mv tmp nginx-custom.rb
我們現(xiàn)在可以將我們需要的模塊加入安裝信息包中并開始編譯了。這很簡(jiǎn)單,我們只要將所有我們需要的模塊以參數(shù)形式傳給brew install命令,代碼如下:
def collect_modules regex=nil
ARGV.select { |arg| arg.match(regex) != nil }.collect { |arg| arg.gsub(regex, '') }
end
# Get nginx modules that are not compiled in by default specified in ARGV
def nginx_modules; collect_modules(/^--include-module-/); end
# Get nginx modules that are available on github specified in ARGV
def add_from_github; collect_modules(/^--add-github-module=/); end
# Get nginx modules from mdounin's hg repository specified in ARGV
def add_from_mdounin; collect_modules(/^--add-mdounin-module=/); end
# Retrieve a repository from github
def fetch_from_github name
name, repository = name.split('/')
raise "You must specify a repository name for github modules" if repository.nil?
puts "- adding #{repository} from github..."
`git clone -q git://github.com/#{name}/#{repository} modules/#{name}/#{repository}`
path = Dir.pwd + '/modules/' + name + '/' + repository
end
# Retrieve a tar of a package from mdounin
def fetch_from_mdounin name
name, hash = name.split('#')
raise "You must specify a commit sha for mdounin modules" if hash.nil?
puts "- adding #{name} from mdounin..."
`mkdir -p modules/mdounin && cd $_ ; curl -s -O http://mdounin.ru/hg/#{name}/archive/#{hash}.tar.gz; tar -zxf #{hash}.tar.gz`
path = Dir.pwd + '/modules/mdounin/' + name + '-' + hash
end
上面這個(gè)輔助模塊可以讓我們指定想要的模塊并檢索模塊的地址。現(xiàn)在,我們需要修改nginx-custom.rb文件,使之包含這些模塊的名字并在包中檢索它們,在58行附近:
add_from_github.each { |name| args << "--add-module=#{fetch_from_github(name)}" }
add_from_mdounin.each { |name| args << "--add-module=#{fetch_from_mdounin(name)}" }
現(xiàn)在我們可以編譯我們重新定制的nginx了:
--add-github-module=agentzh/chunkin-nginx-module \
--include-module-http_gzip_static_module \
--add-mdounin-module=ngx_http_auth_request_module#a29d74804ff1
你可以方便地在seatgeek/homebrew-formulae找到以上信息包。
為Debian定制Nginx
我們通常都會(huì)部署到Debian的發(fā)行版-通常是Ubuntu上作為我們的產(chǎn)品服務(wù)器。如果是這樣,那將會(huì)非常簡(jiǎn)單,運(yùn)行 dpkg -i nginx-custom 安裝我們的定制包。這步驟如此簡(jiǎn)單你一運(yùn)行它就完成了。
一些在搜索定制debian/ubuntu包時(shí)的筆記:
- 你可以通過 apt-get source PACKAGE_NAME來獲取debian安裝包。
- Debian安裝包受控于一個(gè) rules文件,你需要sed-fu來操作它。
- 你可以通過編輯 control 文件來更新 deb包的依賴。注意這里指定了一些元依賴(meta-dependency)你不要去刪除它,但是這些很容易分辨出來。
- 新的發(fā)布必須要在changelog里注明,否則包有可能不會(huì)被升級(jí)因?yàn)樗赡芤呀?jīng)被安裝過了。你需要在表單里使用 +tag_name來指明哪些是你自己在baseline上新加的改動(dòng)。我會(huì)額外加上一個(gè)數(shù)字-從0開始-指示出包的發(fā)布編號(hào)。
- 大多數(shù)的改動(dòng)可以以某種方式自動(dòng)更改,但是似乎沒有一個(gè)簡(jiǎn)單的命令行工具可以創(chuàng)建定制的發(fā)布包。這也正是我們感興趣的地方,如果你知道什么的話,請(qǐng)給我們給我們提供一些鏈接,工具或方法。
在運(yùn)行這個(gè)偉大過程的同時(shí),我構(gòu)建了一個(gè)小的批處理腳本來自動(dòng)化這個(gè)過程的主要步驟,你可以在gist on github上找到它。
在我意識(shí)到這個(gè)過程可以被腳本化之前僅僅花費(fèi)了90個(gè)nginx包的構(gòu)建時(shí)間。
全部OAuth
現(xiàn)在可以測(cè)試并部署嵌入Nginx的Lua腳本了,讓我們開始Lua編程。
nginx-lua模塊提供了一些輔助功能和變量來訪問Nginx的絕大多數(shù)功能,顯然我們可以通過access_by_lua中該模塊提供的指令來強(qiáng)制打開OAuth認(rèn)證。
當(dāng)使用*_by_lua_file指令后,必須重載nginx來使其起作用。
我用NodeJS為SeatGeek創(chuàng)建了一個(gè)簡(jiǎn)單的OAuth2提供者類。這部分內(nèi)容很簡(jiǎn)單,你也很容易獲得你是通用語言的響應(yīng)版本。
接下來,我們的OAuth API使用JSON來處理令牌(token)、訪問級(jí)別(access level)和重新認(rèn)證響應(yīng)(re-authentication responses)。所以我們需要安裝lua-cjson模塊。
if [ ! -d lua-cjson-2.1.0 ]; then
tar zxf lua-cjson-2.1.0.tar.gz
fi
cd lua-cjson-2.1.0
sed 's/i686/x86_64/' /usr/share/lua/5.1/luarocks/config.lua > /usr/share/lua/5.1/luarocks/config.lua-tmp
rm /usr/share/lua/5.1/luarocks/config.lua
mv /usr/share/lua/5.1/luarocks/config.lua-tmp /usr/share/lua/5.1/luarocks/config.lua
luarocks make
我的OAuth提供者類使用了query-string來發(fā)送認(rèn)證的錯(cuò)誤信息,我們也需要在我們的Lua腳本中為其提供支持:
if args.error and args.error == "access_denied" then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say("{\"status\": 401, \"message\": \""..args.error_description.."\"}")
return ngx.exit(ngx.HTTP_OK)
end
現(xiàn)在我們解決了基本的錯(cuò)誤情況,我們要為訪問令牌設(shè)置cookie。在我的例子中,cookie會(huì)在訪問令牌過期前過期,所以我可以利用cookie來刷新訪問令牌。
if access_token then
ngx.header["Set-Cookie"] = "SGAccessToken="..access_token.."; path=/;Max-Age=3000"
end
現(xiàn)在,我們解決了錯(cuò)誤響應(yīng)的api,并儲(chǔ)存了access_token供后續(xù)訪問。我們現(xiàn)在需要確保OAuth認(rèn)證過程正確啟動(dòng)。下面,我們想要:
- 如果沒有access_token已經(jīng)或?qū)⒁鎯?chǔ),開啟OAuth認(rèn)證
- 如果query string的參數(shù)中有OAuth訪問代碼(access code),使用OAuth API檢索用戶的access_token
- 拒絕使用非法訪問代碼用戶的請(qǐng)求
閱讀nginx-Lua函數(shù)和變量的相關(guān)文檔可以解決一些問題,或許還能告訴你訪問特定請(qǐng)求/響應(yīng)信息的各種方法。
此時(shí),我們需要從我們的api接口獲取一個(gè)TOKEN。nginx-lua提供了ngx.location.capture方法,支持發(fā)起一個(gè)內(nèi)部請(qǐng)求到redis,并接收響應(yīng)。這意味著,我們不能直接調(diào)用類似于http://seatgeek.com/ncaa-football-tickets,但我們可以用proxy_pass把這種外部鏈接包裝成內(nèi)部請(qǐng)求。
我們通常約定給這樣的內(nèi)部請(qǐng)求前面加一個(gè)_(下劃線), 用來阻止外部直接訪問。
if not access_token or args.code then
if args.code then
-- internal-oauth:1337/access_token
local res = ngx.location.capture("/_access_token?client_id="..app_id.."&client_secret="..app_secret.."&code="..args.code)
-- 終止所有非法請(qǐng)求
if res.status ~= 200 then
ngx.status = res.status
ngx.say(res.body)
ngx.exit(ngx.HTTP_OK)
end
-- 解碼 token
local text = res.body
local json = cjson.decode(text)
access_token = json.access_token
end
-- cookie 和 proxy_pass token 請(qǐng)求失敗
if not access_token then
-- 跟蹤用戶訪問,用于透明的重定向
ngx.header["Set-Cookie"] = "SGRedirectBack="..nginx_uri.."; path=/;Max-Age=120"
-- 重定向到 /oauth , 獲取權(quán)限
return ngx.redirect("internal-oauth:1337/oauth?client_id="..app_id.."&scope=all")
end
end
此時(shí)在Lua腳本中,應(yīng)該已經(jīng)有了一個(gè)可用的access_token。我們可以用來獲取任何請(qǐng)求需要的用戶信息。在本文中,返回401表示沒有權(quán)限,403表示token過期,并且授權(quán)信息用簡(jiǎn)單數(shù)字打包成json響應(yīng)。
-- internal-oauth:1337/accessible
local res = ngx.location.capture("/_user", {args = { access_token = access_token } } )
if res.status ~= 200 then
-- 刪除損壞的 token
ngx.header["Set-Cookie"] = "SGAccessToken=deleted; path=/; Expires=Thu, 01-Jan-1970 00:00:01 GMT"
-- 如果 token 損壞 ,重定向 403 forbidden 到 oauth
if res.status == 403 then
return ngx.redirect("https://seatgeek.com/oauth?client_id="..app_id.."&scope=all")
end
-- 沒有權(quán)限
ngx.status = res.status
ngx.say("{"status": 503, "message": "Error accessing api/me for credentials"}")
return ngx.exit(ngx.HTTP_OK)
end
現(xiàn)在,我們已經(jīng)驗(yàn)證了用戶確實(shí)是經(jīng)過身份驗(yàn)證的并且具有某個(gè)級(jí)別的訪問權(quán)限,我們可以檢查他們的訪問級(jí)別,看看是否同我們所定義的任何當(dāng)前端點(diǎn)的訪問級(jí)別有沖突。我個(gè)人在這一步刪除了SGAccessToken,以便用戶擁有使用不同的用戶身份登錄的能力,但這一做法用不用由你決定。
-- Ensure we have the minimum for access_level to this resource
if json.access_level < 255 then
-- Expire their stored token
ngx.header["Set-Cookie"] = "SGAccessToken=deleted; path=/; Expires=Thu, 01-Jan-1970 00:00:01 GMT"
-- Disallow access
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say("{\"status\": 403, \"message\": \"USER_ID"..json.user_id.." has no access to this resource\"}")
return ngx.exit(ngx.HTTP_OK)
end
-- Store the access_token within a cookie
ngx.header["Set-Cookie"] = "SGAccessToken="..access_token.."; path=/;Max-Age=3000"
-- Support redirection back to your request if necessary
local redirect_back = ngx.var.cookie_SGRedirectBack
if redirect_back then
ngx.header["Set-Cookie"] = "SGRedirectBack=deleted; path=/; Expires=Thu, 01-Jan-1970 00:00:01 GMT"
return ngx.redirect(redirect_back)
end
現(xiàn)在我們只需要通過一些請(qǐng)求頭信息告知我們當(dāng)前的應(yīng)用誰登錄了就行了。您可以重用REMOTE_USER,如果你有需求的話,就可以用這個(gè)取代基本的身份驗(yàn)證,而除此之外的任何事情都是公平的游戲。
ngx.req.set_header("X-USER-ACCESS-LEVEL", json.access_level)
ngx.req.set_header("X-USER-EMAIL", json.email)
我現(xiàn)在就可以像任何其它的站點(diǎn)那樣在我的應(yīng)用程序中訪問這些http頭了,而不是用數(shù)百行代碼和大量的時(shí)間來重新實(shí)現(xiàn)身份驗(yàn)證。
Nginx 和 Lua, 放在樹結(jié)構(gòu)里面
在這一點(diǎn)上,我們應(yīng)該有一個(gè)可以用來阻擋/拒絕訪問的LUA腳本。我們可以將這個(gè)腳本放到磁盤上的一個(gè)文件中,然后使用access_by_lua_file配置來將它應(yīng)用在我們的nginx站點(diǎn)中。在SeatGeek中,我們使用Chief來模板化輸出配置文件,雖然你可以使用Puppet,F(xiàn)abric,或者其它任何你喜歡的工具。
下面是你可以用來使所有東西都運(yùn)行起來的最簡(jiǎn)單的nginx的網(wǎng)站。你也可能會(huì)想要檢查下access.lua - 在這里 - 它是上面的lua腳本編譯后的文件。
upstream production-app {
server localhost:8080;
}
# The internal oauth provider
upstream internal-oauth {
server localhost:1337;
}
server {
listen 80;
server_name private.example.com;
root /apps;
charset utf-8;
# This will run for everything but subrequests
access_by_lua_file "/etc/nginx/access.lua";
# Used in a subrequest
location /_access_token { proxy_pass http://internal-oauth/oauth/access_token; }
location /_user { proxy_pass http://internal-oauth/user; }
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_max_temp_file_size 0;
if (!-f $request_filename) {
proxy_pass http://production-app;
break;
}
}
}
進(jìn)一步思考
雖然此設(shè)置運(yùn)行的比較好,但是我想指出一些缺點(diǎn):
- 上面的代碼是我們access_by_lua腳本的簡(jiǎn)化。我們也處理保存POST提交的請(qǐng)求,JS加入到到頁面更新會(huì)話自動(dòng)處理的令牌更新等,你可能不需要這些功能,而事實(shí)上,我不認(rèn)為我需要它們,直到我們開始了我們?cè)趦?nèi)部系統(tǒng)進(jìn)行系統(tǒng)測(cè)試。
- 我們有一些結(jié)點(diǎn),可以通過一定的后臺(tái)任務(wù)基本認(rèn)證。這些被修改,數(shù)據(jù)是從一個(gè)外部存儲(chǔ)中檢索,如S3。注意,這并不總是可能的,所以使用的可能不是你想要的答案。
- Oauth2只是我選擇的標(biāo)準(zhǔn)。在理論上,你可以使用facebook授權(quán)來實(shí)現(xiàn)類似的結(jié)果。你也可以將這種方法限速,或存儲(chǔ)在數(shù)據(jù)庫中的不同的訪問級(jí)別如在你的Lua腳本方便操作和檢索使用。如果你真的很無聊,你可以重新實(shí)現(xiàn)基本認(rèn)證在Lua,這只需要你。
- 有沒有測(cè)試控制系統(tǒng)等。測(cè)試者會(huì)害怕當(dāng)他們意識(shí)到這將是一段時(shí)間的集成測(cè)試。你可以重新運(yùn)行上面的嘲笑為全球范圍內(nèi)注入變量以及執(zhí)行腳本,但它不是理想的設(shè)置。
- 你還需要修改應(yīng)用程序識(shí)別你的新的訪問標(biāo)頭。內(nèi)部工具將是最簡(jiǎn)單的,但你可能需要為供應(yīng)商軟件作出一定的讓步。
相關(guān)文章
Nginx緩存文件與動(dòng)態(tài)文件自動(dòng)均衡的配置腳本
Nginx (engine x) 是一個(gè)高性能的HTTP和反向代理服務(wù),也是一個(gè)IMAP/POP3/SMTP服務(wù)。這篇文章主要介紹了Nignx緩存文件與動(dòng)態(tài)文件自動(dòng)均衡的配置,需要的朋友可以參考下2018-09-09Nginx服務(wù)器設(shè)置網(wǎng)站驗(yàn)證訪問的方法
這篇文章主要介紹了Nginx服務(wù)器設(shè)置網(wǎng)站驗(yàn)證訪問的方法,通過設(shè)置密碼來要求登錄網(wǎng)站目錄的用戶進(jìn)行驗(yàn)證,需要的朋友可以參考下2015-07-07WordPress中開啟多站點(diǎn)支持及Nginx的重寫規(guī)則配置
這篇文章主要介紹了WordPress中開啟多站點(diǎn)支持及Nginx的重寫規(guī)則配置方法,在同一個(gè)WordPress軟件中開啟的多個(gè)站點(diǎn)如果需要綁定不同域名的話也可以使用WordPress MU Domain Mapping插件,需要的朋友可以參考下2016-03-03nginx?rewrite?用法如何使用rewrite去除URL中的特定參數(shù)
日常服務(wù)中經(jīng)常會(huì)用Nginx做一層代理轉(zhuǎn)發(fā),把Nginx當(dāng)做前置機(jī),這篇文章主要介紹了nginx?rewrite?用法如何使用rewrite去除URL中的特定參數(shù),需要的朋友可以參考下2024-02-02Nginx配置80端口訪問8080及項(xiàng)目名地址方法解析
這篇文章主要介紹了Nginx配置80端口訪問8080及項(xiàng)目名地址方法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09Nginx反向代理多域名的HTTP和HTTPS服務(wù)的實(shí)現(xiàn)
這篇文章主要介紹了Nginx反向代理多域名的HTTP和HTTPS服務(wù)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06