在Nginx中增加對OAuth協(xié)議的支持的教程
我們使用Nginx的Lua中間件建立了OAuth2認證和授權(quán)層。如果你也有此打算,閱讀下面的文檔,實現(xiàn)自動化并獲得收益。
SeatGeek在過去幾年中取得了發(fā)展,我們已經(jīng)積累了不少針對各種任務(wù)的不同管理接口。我們通常為新的展示需求創(chuàng)建新模塊,比如我們自己的博客、圖表等。我們還定期開發(fā)內(nèi)部工具來處理諸如部署、可視化操作及事件處理等事務(wù)。在處理這些事務(wù)中,我們使用了幾個不同的接口來認證:
- Github/Google Oauth
- 我們SeatGeek內(nèi)部的用戶系統(tǒng)
- 基本認證
- 硬編碼登錄
顯然,實際應(yīng)用中很不規(guī)范。多個認證系統(tǒng)使得難以對用于訪問級別和通用許可的各種數(shù)據(jù)庫進行抽象。
單系統(tǒng)認證
我們也做了一些關(guān)于如何設(shè)置將解決我們問題的研究。這促使了Odin的出現(xiàn),它在驗證谷歌應(yīng)用的用戶方面工作的很好。不幸的是它需要使用Apache,而我們已和Nginx結(jié)為連理并把它作為我們的后端應(yīng)用的前端。
幸運的是,我看了mixlr的博客并引用了他們Lua在Nginx上的應(yīng)用:
- 修改響應(yīng)頭
- 重寫內(nèi)部請求
- 選擇性地基于IP拒絕主機訪問
最后一條看起來很有趣。它開啟了軟件包管理的地獄之旅。
構(gòu)建支持Lua的Nginx
Lua for Nginx沒有被包含在Nginx的核心中,我們經(jīng)常要為OSX構(gòu)建Nginx用于開發(fā)測試,為Linux構(gòu)建用于部署。
為OSX定制Nginx
對于OSX系統(tǒng),我推薦使用Homebrew進行包管理。它初始的Nginx安裝包啟用的模塊不多,這有非常好的理由:
關(guān)鍵在于NGINX有著如此之多的選項,如果把它們都加入初始包那一定是瘋了,如果我們只把其中一些加入其中就會迫使我們把所有都加入,這會讓我們瘋掉的。
- Charlie Sharpsteen, @sharpie
所以我們需要自己構(gòu)建。合理地構(gòu)建Nginx可以方便我們以后繼續(xù)擴展。幸運的是,使用Homebrew進行包管理十分方便快捷。
我們首先需要一個工作空間:
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
此時如果我們執(zhí)行brew install ./nginx.rb命令, 它會依據(jù)其中的信息安裝Nginx。既然現(xiàn)在我們要完全定制Nginx,我們要重命名信息包,這樣之后通過brew update命令進行更新的時候就不會覆蓋我們自定義的了:
cat nginx-custom.rb | sed 's/class Nginx/class NginxCustom/' >> tmp
rm nginx-custom.rb
mv tmp nginx-custom.rb
我們現(xià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
上面這個輔助模塊可以讓我們指定想要的模塊并檢索模塊的地址。現(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
我們通常都會部署到Debian的發(fā)行版-通常是Ubuntu上作為我們的產(chǎn)品服務(wù)器。如果是這樣,那將會非常簡單,運行 dpkg -i nginx-custom 安裝我們的定制包。這步驟如此簡單你一運行它就完成了。
一些在搜索定制debian/ubuntu包時的筆記:
- 你可以通過 apt-get source PACKAGE_NAME來獲取debian安裝包。
- Debian安裝包受控于一個 rules文件,你需要sed-fu來操作它。
- 你可以通過編輯 control 文件來更新 deb包的依賴。注意這里指定了一些元依賴(meta-dependency)你不要去刪除它,但是這些很容易分辨出來。
- 新的發(fā)布必須要在changelog里注明,否則包有可能不會被升級因為它可能已經(jīng)被安裝過了。你需要在表單里使用 +tag_name來指明哪些是你自己在baseline上新加的改動。我會額外加上一個數(shù)字-從0開始-指示出包的發(fā)布編號。
- 大多數(shù)的改動可以以某種方式自動更改,但是似乎沒有一個簡單的命令行工具可以創(chuàng)建定制的發(fā)布包。這也正是我們感興趣的地方,如果你知道什么的話,請給我們給我們提供一些鏈接,工具或方法。
在運行這個偉大過程的同時,我構(gòu)建了一個小的批處理腳本來自動化這個過程的主要步驟,你可以在gist on github上找到它。
在我意識到這個過程可以被腳本化之前僅僅花費了90個nginx包的構(gòu)建時間。
全部OAuth
現(xiàn)在可以測試并部署嵌入Nginx的Lua腳本了,讓我們開始Lua編程。
nginx-lua模塊提供了一些輔助功能和變量來訪問Nginx的絕大多數(shù)功能,顯然我們可以通過access_by_lua中該模塊提供的指令來強制打開OAuth認證。
當使用*_by_lua_file指令后,必須重載nginx來使其起作用。
我用NodeJS為SeatGeek創(chuàng)建了一個簡單的OAuth2提供者類。這部分內(nèi)容很簡單,你也很容易獲得你是通用語言的響應(yīng)版本。
接下來,我們的OAuth API使用JSON來處理令牌(token)、訪問級別(access level)和重新認證響應(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ā)送認證的錯誤信息,我們也需要在我們的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)在我們解決了基本的錯誤情況,我們要為訪問令牌設(shè)置cookie。在我的例子中,cookie會在訪問令牌過期前過期,所以我可以利用cookie來刷新訪問令牌。
if access_token then
ngx.header["Set-Cookie"] = "SGAccessToken="..access_token.."; path=/;Max-Age=3000"
end
現(xiàn)在,我們解決了錯誤響應(yīng)的api,并儲存了access_token供后續(xù)訪問。我們現(xiàn)在需要確保OAuth認證過程正確啟動。下面,我們想要:
- 如果沒有access_token已經(jīng)或?qū)⒁鎯Γ_啟OAuth認證
- 如果query string的參數(shù)中有OAuth訪問代碼(access code),使用OAuth API檢索用戶的access_token
- 拒絕使用非法訪問代碼用戶的請求
閱讀nginx-Lua函數(shù)和變量的相關(guān)文檔可以解決一些問題,或許還能告訴你訪問特定請求/響應(yīng)信息的各種方法。
此時,我們需要從我們的api接口獲取一個TOKEN。nginx-lua提供了ngx.location.capture方法,支持發(fā)起一個內(nèi)部請求到redis,并接收響應(yīng)。這意味著,我們不能直接調(diào)用類似于http://seatgeek.com/ncaa-football-tickets,但我們可以用proxy_pass把這種外部鏈接包裝成內(nèi)部請求。
我們通常約定給這樣的內(nèi)部請求前面加一個_(下劃線), 用來阻止外部直接訪問。
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)
-- 終止所有非法請求
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 請求失敗
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
此時在Lua腳本中,應(yīng)該已經(jīng)有了一個可用的access_token。我們可以用來獲取任何請求需要的用戶信息。在本文中,返回401表示沒有權(quán)限,403表示token過期,并且授權(quá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)驗證了用戶確實是經(jīng)過身份驗證的并且具有某個級別的訪問權(quán)限,我們可以檢查他們的訪問級別,看看是否同我們所定義的任何當前端點的訪問級別有沖突。我個人在這一步刪除了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)在我們只需要通過一些請求頭信息告知我們當前的應(yīng)用誰登錄了就行了。您可以重用REMOTE_USER,如果你有需求的話,就可以用這個取代基本的身份驗證,而除此之外的任何事情都是公平的游戲。
ngx.req.set_header("X-USER-ACCESS-LEVEL", json.access_level)
ngx.req.set_header("X-USER-EMAIL", json.email)
我現(xiàn)在就可以像任何其它的站點那樣在我的應(yīng)用程序中訪問這些http頭了,而不是用數(shù)百行代碼和大量的時間來重新實現(xiàn)身份驗證。
Nginx 和 Lua, 放在樹結(jié)構(gòu)里面
在這一點上,我們應(yīng)該有一個可以用來阻擋/拒絕訪問的LUA腳本。我們可以將這個腳本放到磁盤上的一個文件中,然后使用access_by_lua_file配置來將它應(yīng)用在我們的nginx站點中。在SeatGeek中,我們使用Chief來模板化輸出配置文件,雖然你可以使用Puppet,F(xiàn)abric,或者其它任何你喜歡的工具。
下面是你可以用來使所有東西都運行起來的最簡單的nginx的網(wǎng)站。你也可能會想要檢查下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;
}
}
}
進一步思考
雖然此設(shè)置運行的比較好,但是我想指出一些缺點:
- 上面的代碼是我們access_by_lua腳本的簡化。我們也處理保存POST提交的請求,JS加入到到頁面更新會話自動處理的令牌更新等,你可能不需要這些功能,而事實上,我不認為我需要它們,直到我們開始了我們在內(nèi)部系統(tǒng)進行系統(tǒng)測試。
- 我們有一些結(jié)點,可以通過一定的后臺任務(wù)基本認證。這些被修改,數(shù)據(jù)是從一個外部存儲中檢索,如S3。注意,這并不總是可能的,所以使用的可能不是你想要的答案。
- Oauth2只是我選擇的標準。在理論上,你可以使用facebook授權(quán)來實現(xiàn)類似的結(jié)果。你也可以將這種方法限速,或存儲在數(shù)據(jù)庫中的不同的訪問級別如在你的Lua腳本方便操作和檢索使用。如果你真的很無聊,你可以重新實現(xiàn)基本認證在Lua,這只需要你。
- 有沒有測試控制系統(tǒng)等。測試者會害怕當他們意識到這將是一段時間的集成測試。你可以重新運行上面的嘲笑為全球范圍內(nèi)注入變量以及執(zhí)行腳本,但它不是理想的設(shè)置。
- 你還需要修改應(yīng)用程序識別你的新的訪問標頭。內(nèi)部工具將是最簡單的,但你可能需要為供應(yīng)商軟件作出一定的讓步。
相關(guān)文章
Nginx服務(wù)器設(shè)置網(wǎng)站驗證訪問的方法
這篇文章主要介紹了Nginx服務(wù)器設(shè)置網(wǎng)站驗證訪問的方法,通過設(shè)置密碼來要求登錄網(wǎng)站目錄的用戶進行驗證,需要的朋友可以參考下2015-07-07WordPress中開啟多站點支持及Nginx的重寫規(guī)則配置
這篇文章主要介紹了WordPress中開啟多站點支持及Nginx的重寫規(guī)則配置方法,在同一個WordPress軟件中開啟的多個站點如果需要綁定不同域名的話也可以使用WordPress MU Domain Mapping插件,需要的朋友可以參考下2016-03-03nginx?rewrite?用法如何使用rewrite去除URL中的特定參數(shù)
日常服務(wù)中經(jīng)常會用Nginx做一層代理轉(zhuǎn)發(fā),把Nginx當做前置機,這篇文章主要介紹了nginx?rewrite?用法如何使用rewrite去除URL中的特定參數(shù),需要的朋友可以參考下2024-02-02Nginx反向代理多域名的HTTP和HTTPS服務(wù)的實現(xiàn)
這篇文章主要介紹了Nginx反向代理多域名的HTTP和HTTPS服務(wù)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06