前言:咱們使用Nginx的Lua中間件創建了OAuth2認證和受權層。若是你也有此打算,閱讀下面的文檔,實現自動化並得到收益。
SeatGeek 在過去幾年中取得了發展,咱們已經積累了很多針對各類任務的不一樣管理接口。咱們一般爲新的展現需求建立新模塊,好比咱們本身的博客、圖表等。咱們還按期開發內部工具來處理諸如部署、可視化操做及事件處理等事務。在處理這些事務中,咱們使用了幾個不一樣的接口來認證:前端
顯然,實際應用中很不規範。多個認證系統使得難以對用於訪問級別和通用許可的各類數據庫進行抽象。nginx
咱們也作了一些關於如何設置將解決咱們問題的研究。這促使了Odin的出現,它在驗證谷歌應用的用戶方面工做的很好。不幸的是它須要使用Apache,而咱們已和Nginx結爲連理並把它做爲咱們的後端應用的前端。
幸運的是,我看了mixlr的博客並引用了他們Lua在Nginx上的應用:git
最後一條看起來頗有趣。它開啓了軟件包管理的地獄之旅。github
Lua for Nginx沒有被包含在Nginx的核心中,咱們常常要爲OSX構建Nginx用於開發測試,爲Linux構建用於部署。web
對於OSX系統,我推薦使用Homebrew進行包管理。它初始的Nginx安裝包啓用的模塊很少,這有很是好的理由:redis
關鍵在於NGINX有着如此之多的選項,若是把它們都加入初始包那必定是瘋了,若是咱們只把其中一些加入其中就會迫使咱們把全部都加入,這會讓咱們瘋掉的。
--- Charlie Sharpsteen, @sharpie數據庫
因此咱們須要本身構建。合理地構建Nginx能夠方便咱們之後繼續擴展。幸運的是,使用Homebrew進行包管理十分方便快捷。
咱們首先須要一個工做空間:編程
cd ~ mkdir -p src cd src
以後,咱們須要找到初始安裝信息包。你能夠經過下面任何一種方式獲得它:json
此時若是咱們執行 brew install ./nginx.rb
命令, 它會依據其中的信息安裝Nginx。既然如今咱們要徹底定製Nginx,咱們要重命名信息包,這樣以後經過 brew update
命令進行更新的時候就不會覆蓋咱們自定義的了:ubuntu
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
咱們如今能夠將咱們須要的模塊加入安裝信息包中並開始編譯了。這很簡單,咱們只要將全部咱們須要的模塊以參數形式傳給 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
上面這個輔助模塊可讓咱們指定想要的模塊並檢索模塊的地址。如今,咱們須要修改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)}" }
如今咱們能夠編譯咱們從新定製的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的發行版-一般是Ubuntu上做爲咱們的產品服務器。若是是這樣,那將會很是簡單,運行 dpkg -i nginx-custom
安裝咱們的定製包。這步驟如此簡單你一運行它就完成了。
一些在搜索定製debian/ubuntu包時的筆記:
在運行這個偉大過程的同時,我構建了一個小的批處理腳原本自動化這個過程的主要步驟,你能夠在 gist on github
: gist.github.com/4126937 上找到它。
在我意識到這個過程能夠被腳本化以前僅僅花費了90個nginx包的構建時間。
如今能夠測試並部署嵌入Nginx的Lua腳本了,讓咱們開始Lua編程。
nginx-lua模塊提供了一些輔助功能和變量來訪問Nginx的絕大多數功能,顯然咱們能夠經過access_by_lua中該模塊提供的指令來強制打開OAuth認證。
當使用*_by_lua_file指令後,必須重載nginx來使其起做用。
我用 NodeJS 爲 SeatGeek 建立了一個簡單的 OAuth2 提供者類。這部份內容很簡單,你也很容易得到你是通用語言的響應版本。
接下來,咱們的OAuth API使用JSON來處理令牌(token)、訪問級別(access level)和從新認證響應(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 來發送認證的錯誤信息,咱們也須要在咱們的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
如今咱們解決了基本的錯誤狀況,咱們要爲訪問令牌設置cookie。在個人例子中,cookie會在訪問令牌過時前過時,因此我能夠利用cookie來刷新訪問令牌。
local access_token = ngx.var.cookie_SGAccessToken if access_token then ngx.header["Set-Cookie"] = "SGAccessToken="..access_token.."; path=/;Max-Age=3000" end
如今,咱們解決了錯誤響應的api,並儲存了access_token供後續訪問。咱們如今須要確保OAuth認證過程正確啓動。下面,咱們想要:
閱讀nginx-Lua函數和變量的相關文檔能夠解決一些問題,或許還能告訴你訪問特定請求/響應信息的各類方法。
此時,咱們須要從咱們的api接口獲取一個TOKEN。nginx-lua提供了 ngx.location.capture
方法,支持發起一個內部請求到redis,並接收響應。這意味着,咱們不能直接調用相似於 http://seatgeek.com/ncaa-football-tickets,但咱們能夠用 proxy_pass 把這種外部連接包裝成內部請求。
咱們一般約定給這樣的內部請求前面加一個_(下劃線), 用來阻止外部直接訪問。
-- 第一步,從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) -- 終止全部非法請求 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 , 獲取權限 return ngx.redirect("internal-oauth:1337/oauth?client_id="..app_id.."&scope=all") end end
此時在Lua腳本中,應該已經有了一個可用的 access_token。咱們能夠用來獲取任何請求須要的用戶信息。在本文中,返回401表示沒有權限,403表示token過時,而且受權信息用簡單數字打包成json響應。
-- 確保用戶有訪問web應用的權限 -- 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 -- 沒有權限 ngx.status = res.status ngx.say("{"status": 503, "message": "Error accessing api/me for credentials"}") return ngx.exit(ngx.HTTP_OK) end
如今,咱們已經驗證了用戶確實是通過身份驗證的而且具備某個級別的訪問權限,咱們能夠檢查他們的訪問級別,看看是否同咱們所定義的任何當前端點的訪問級別有衝突。我我的在這一步刪除了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
如今咱們只須要經過一些請求頭信息告知咱們當前的應用誰登陸了就好了。您能夠重用REMOTE_USER,若是你有需求的話,就能夠用這個取代基本的身份驗證,而除此以外的任何事情都是公平的遊戲。
-- 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)
我如今就能夠像任何其它的站點那樣在個人應用程序中訪問這些http頭了,而不是用數百行代碼和大量的時間來從新實現身份驗證。
在這一點上,咱們應該有一個能夠用來阻擋/拒絕訪問的LUA腳本。咱們能夠將這個腳本放到磁盤上的一個文件中,而後使用access_by_lua_file配置來將它應用在咱們的nginx站點中。在SeatGeek中,咱們使用Chief來模板化輸出配置文件,雖然你可使用Puppet,Fabric,或者其它任何你喜歡的工具。
下面是你能夠用來使全部東西都運行起來的最簡單的nginx的網站。你也可能會想要檢查下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; } } }
雖然此設置運行的比較好,可是我想指出一些缺點:
一些博客中的講解及研究實例
連接
另外一些講解閱讀
原文:Yak Shaving: Adding OAuth Support to Nginx via Lua
轉載自:開源中國 - Garfielt, LeoXu, DrZ, BoydWang, 學習者8, FreeZ