摘要:我的提供者類使用了來發(fā)送認(rèn)證的錯(cuò)誤信息,我們也需要在我們的腳本中為其提供支持現(xiàn)在我們解決了基本的錯(cuò)誤情況,我們要為訪問令牌設(shè)置?,F(xiàn)在我們只需要通過一些請(qǐng)求頭信息告知我們當(dāng)前的應(yīng)用誰登錄了就行了。
前言:我們使用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的NginxLua for Nginx沒有被包含在Nginx的核心中,我們經(jīng)常要為OSX構(gòu)建Nginx用于開發(fā)測(cè)試,為Linux構(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è)工作空間:
cd ~ 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ì)覆蓋我們自定義的了:
mv nginx.rb nginx-custom.rb 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 命令,代碼如下:
# Collects arguments from ARGV 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行附近:
nginx_modules.each { |name| args << "--with-#{name}"; puts "- adding #{name} module" } 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了:
brew install ./nginx-custom.rb --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: gist.github.com/4126937 上找到它。
在我意識(shí)到這個(gè)過程可以被腳本化之前僅僅花費(fèi)了90個(gè)nginx包的構(gòu)建時(shí)間。
現(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模塊。
# install 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腳本中為其提供支持:
local args = ngx.req.get_uri_args() 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來刷新訪問令牌。
local access_token = ngx.var.cookie_SGAccessToken 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è)_(下劃線), 用來阻止外部直接訪問。
-- 第一步,從api獲取獲取token 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)。
-- 確保用戶有訪問web應(yīng)用的權(quán)限 -- 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,以便用戶擁有使用不同的用戶身份登錄的能力,但這一做法用不用由你決定。
local json = cjson.decode(res.body) -- 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)證,而除此之外的任何事情都是公平的游戲。
-- Set some headers for use within the protected endpoint 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 - 在這里:gist.github.com/4196901 - 它是上面的lua腳本編譯后的文件。
# The app we are proxying to 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)商軟件作出一定的讓步。
鏈接一些博客中的講解及研究實(shí)例
鏈接
SeatGeek Homebrew Formulae with customizable nginx
nginx_release.sh for building nginx debs
access.lua and nginx-site
另一些講解閱讀
HttpLuaModule
proxy_pass, like mod_proxy, but for nginx
Lua usage at Mixlr
OAuth for Apache
Homebrew OS X Package Manager
原文:Yak Shaving: Adding OAuth Support to Nginx via Lua
轉(zhuǎn)載自:開源中國 - Garfielt, LeoXu, DrZ, BoydWang, 學(xué)習(xí)者8, FreeZ
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/39051.html
摘要:最近為了分析公司的一個(gè)的應(yīng)用性能,正好需要用到春神的那套的分析腳本,因此就立馬去搭建下環(huán)境依賴項(xiàng)依賴于注上面安裝的時(shí)候,的版本不能太高,不然會(huì)報(bào)錯(cuò),安裝失敗,目前使用的版本是該文的前提是你已經(jīng)編譯安裝好了支持的版本,以及你已經(jīng) 最近為了分析公司的一個(gè) nginx + lua 的應(yīng)用性能,正好需要用到春神的那套 nginx-lua 的分析腳本,因此就立馬去搭建下 環(huán)境: CentO...
摘要:最近為了分析公司的一個(gè)的應(yīng)用性能,正好需要用到春神的那套的分析腳本,因此就立馬去搭建下環(huán)境依賴項(xiàng)依賴于注上面安裝的時(shí)候,的版本不能太高,不然會(huì)報(bào)錯(cuò),安裝失敗,目前使用的版本是該文的前提是你已經(jīng)編譯安裝好了支持的版本,以及你已經(jīng) 最近為了分析公司的一個(gè) nginx + lua 的應(yīng)用性能,正好需要用到春神的那套 nginx-lua 的分析腳本,因此就立馬去搭建下 環(huán)境: CentO...
摘要:用于方便地搭建能夠處理超高并發(fā)擴(kuò)展性極高的動(dòng)態(tài)應(yīng)用服務(wù)和動(dòng)態(tài)網(wǎng)關(guān)。安裝安裝依賴庫下載及安裝激活組件被用于構(gòu)建。大部組件默認(rèn)是激活的,也有部件不是。您需要通過以下選項(xiàng)在編譯的時(shí)候?qū)⑺鼈兏髯约せ?,和? OpenResty簡(jiǎn)介 OpenResty 是一個(gè)基于 Nginx 與 Lua 的高性能 Web 平臺(tái),其內(nèi)部集成了大量精良的 Lua 庫、第三方模塊以及大多數(shù)的依賴項(xiàng)。用于方便地搭建能夠處...
閱讀 2305·2021-09-30 09:47
閱讀 2224·2021-09-26 09:55
閱讀 2954·2021-09-24 10:27
閱讀 1543·2019-08-27 10:54
閱讀 971·2019-08-26 13:40
閱讀 2500·2019-08-26 13:24
閱讀 2423·2019-08-26 13:22
閱讀 1735·2019-08-23 18:38