In the last two days, I’ve had to solve a rather interesting problem. I have an nginx instance proxying various servers, and I need to be able to add an authentication layer that will authenticate people with an external source (such as a web app) and allow them to pass through the proxy if they have an account on the authentication source (the web app, in this example).
Exploring the requirements
I considered various solutions for this, and I will list a few alternatives:
- A simple Python/Flask module that would do the actual proxying and authentication.
- An nginx module that would authenticate using subrequests (nginx can now do that).
- Using nginx’s Lua module to write some authentication code.
译者信息
过去两天里,我解决了一个非常有趣的问题。我用一个nginx服务器作为代理,需要能够向其中添加一个认证层,使其能够使用外部的认证源(比如某个web应用)来进行验证,如果用户在外部认证源有账号,就可以在代理里认证通过。
需求一览
我考虑了几种解决方案,罗列如下:
- 用一个简单的Python/Flask模块来做代理和验证。
- 一个使用subrequests做验证的nginx模块(nginx目前可以做到这一点)
- 使用Lua编写一个nginxren认证模块
It became clear early on that adding another request to the whole system wouldn’t work very well, because of the added latency (it would be annoying to do this on every single request for every file on a page). This means that the subrequest module is out. The Python/Flask solution also feels like it’s bypassing a lot of nginx, so it’s also out. This leaves Lua, which nginx supports natively.
Since I don’t want to authenticate with the external server on every request, I decided to generate tokens which people can store and just present to the server so they are let through. However, since the Lua module doesn’t have any way (that I have found) to keep state, we can’t actually store these tokens anywhere. How do you verify that the user is who they say they are when you have no memory?
译者信息
很显然,给整个系统添加额外请求将执行的不是很好,因为这将会增加延迟(特别是给每一个页面文件都增加一个请求是很让人烦恼的).这就意味着我们把subrequest模块排除在外了。Python/Flash解决方案好像对nginx支持的也并不好,所以咱也把它排除了。就剩Lua了,当然nginx对原生化支持得不错的。
因为我不想再扩展的服务器上对每一个请求都做认证,所以我决定生成一些令牌,这样人们就可以将它保存起来,并把它呈现给服务器,然后服务器就让请求通过。然而,因为Lua模块没有一种保持状态的方式(我已经发现),所以我们不能将令牌随处存储。当你没有更多的内存时,怎样来验证用户所说的话呢?
Solving the problem
Cryptographic signing to the rescue! We can give our users signed cookies with a username and expiration date, and very easily verify that they actually are who they say they are, while easily being able to expire the tokens as well.
With nginx, all we need to do is specify theaccess_by_lua_file /our/file.luadirective in the location config, and that location will be protected with our script. Now, to write the actual checking code:
-- Some variable declarations. local cookie = ngx.var.cookie_MyToken local hmac = "" local timestamp = "" local timestamp_time = 0 -- Check that the cookie exists. if cookie == nil or cookie:find(":") == nil then -- Internally rewrite the URL so that we serve -- /auth/ if there's no cookie. ngx.exec("/auth/") else -- If there's a cookie, split off the HMAC signature -- and timestamp. local divider = cookie:find(":") hmac = cookie:sub(divider+1) timestamp = cookie:sub(0, divider-1) end -- Verify that the signature is valid. if hmac_sha1("some very secret string", timestamp) ~= hmac or tonumber(timestamp) < os.time() then -- If invalid, send to /auth/ again. ngx.exec("/auth/") end
The code above should be pretty straightforward. The signing happens by taking some plaintext (in this case, a timestamp, but it can be anything you want), and HMACing it with a secret key, producing a signature which only we can generate, and which the user cannot tamper with without invalidating it.
译者信息
解决问题
加密签名的方式可是咱的救星!我们可以拿用户的用户名和过期时间数据来给用户添加签名的cookies,这样就能很容易的验证每个用户是谁了,同时我们就不用令牌了。
在nginx中,我们要做的就是直接在指定位置配置access_by_lua_file /our/file.lua,这样这个指定位置就可以保护我们的脚本了。现在,让我们一起来写代码:
-- Some variable declarations. local cookie = ngx.var.cookie_MyToken local hmac = "" local timestamp = "" local timestamp_time = 0 -- Check that the cookie exists. if cookie == nil or cookie:find(":") == nil then -- Internally rewrite the URL so that we serve -- /auth/ if there's no cookie. ngx.exec("/auth/") else -- If there's a cookie, split off the HMAC signature -- and timestamp. local divider = cookie:find(":") hmac = cookie:sub(divider+1) timestamp = cookie:sub(0, divider-1) end -- Verify that the signature is valid. if hmac_sha1("some very secret string", timestamp) ~= hmac or tonumber(timestamp) < os.time() then -- If invalid, send to /auth/ again. ngx.exec("/auth/") end
上面的代码可以直接运行。我们用一些明文来签名(这种情况下用的是一个时间戳,当然你可以用任何你想用的),之后我们用密文生成HMAC(哈希信息认证码),然后一个签名就生成了,这样用户就不能篡改为无效信息了。
When the user tries to load a resource, we check that the signature in the cookie is valid, and let them pass if it is. Otherwise, we redirect them to a token issuing server, which will authenticate and give them a signed token if not.
The more astute of you will have noticed that the code above contains a timing vulnerability. If you didn’t notice it, don’t feel bad. Well, maybe just a little bit.
Here’s a piece of Lua code that should compare two strings for equality and be constant-time (thus foiling any timing attack), unless I’ve overlooked something, which is highly probable:
function compare_strings(str1, str2) -- Constant-time string comparison function. local same = true for i = 1, #str1 do -- If the two strings' lengths are different, sub() -- will just return nil for the remaining length. c1 = str1:sub(i,i) c2 = str2:sub(i,i) if c1 ~= c2 then same = false end end return same end
I have timed this function and, as far as I can tell, it is constant-time with equal-length strings. Strings with different lengths will change the timings a bit, perhaps becausesub()takes a different branch. Also, theif c1 ~= c2branch is obviously not constant-time, but in practice it’s close enough that it won’t matter for our case. I’d prefer XORing the two strings together and seeing if the result is 0, but Lua doesn’t seem to include a bitwise XOR operator. If I’m wrong on this, I would appreciate a correction.
译者信息
当用户试图载入一个资源的时候,我们会检查cookie里面的签名是否有效,如果是,就通过他的请求。反之,我们会把他们重定向到一个发行口令的服务器,这个服务器会验证并且在没有的情况下给予他们一个签名的口令。
明锐的你可能会发现,上面的代码存在时间上的漏洞。如果你没有发现,别难过。嗯,也许会有点难过。
这里是一段Lua的代码,用来比较两个字符串在恒定时间上的等值关系(因而能够阻止任何时间上的攻击,除非我忽视了什么,这极为可能):
function compare_strings(str1, str2) -- Constant-time string comparison function. local same = true for i = 1, #str1 do -- If the two strings' lengths are different, sub() -- will just return nil for the remaining length. c1 = str1:sub(i,i) c2 = str2:sub(i,i) if c1 ~= c2 then same = false end end return same end
我已经在函数上应用了时间来区分,如我所知,这是一个在恒定时间下的等值字符串。不同长度的字符串会稍稍改变时间,也许是因为子过程sub应用了一个不同的分支而导致的。而且,c1~=c2分支显然不是恒定时间的,但是在实际中,它相当接近恒定,所以于我们的例子不会有影响。我更倾向于使用XOR操作,从而确定两个字符串的XOR结果是否为0, 不过Lua似乎不包括二进制位的XOR操作。如果我在这个判断上有误,对于任何纠正我都很感激。
The token issuing server
Now that we have some amazing token-checking code written, all we need to do is write a server to actually issue these tokens. I could have written this in Python with Flask, but I wanted to give Go a shot because I’m a language hipster and Go seems cool. It would probably take a bit less time to do it in Python, but I enjoy it.
This server will pop up an HTTP basic auth form, check the credentials you enter and, if they are correct, it will give give you a signed token which is good for one (1) hour of proxy access. That way, you only have to authenticate with the external service once, and subsequent authentication checks are done at the nginx layer and are pretty fast.
译者信息
口令发行服务器
现在,我们已经写了一些很棒的口令检查代码,所有需要做的,只是写一个服务器来真正的发行这些口令。我本可以用Python以及Flask来写这个服务器,不过我还是想用Go做一个尝试,因为我是一个计算机语言潮人而且Go看上去“酷”。使用Python大概会快一些,不过我乐意用Go。
这个服务器会弹出一个HTTP基础验证的表单,检查你输入的帐户,如果正确,它会给你一个签名的口令,适合于一个小时的代理服务器访问。这样,你只需要验证外部服务一次,而随后的身份验证的检查将在nginx层面,而且会相当的快。
The request handler
Writing a handler that would pop up a basic auth box wasn’t very hard, but Go isn’t terribly well documented, so I had to hunt around a bit. It was pretty simple, in the end, and here it is, HTTP basic authentication in Go:
func handler(w http.ResponseWriter, r *http.Request) { if username := checkAuth(r); username == "" { w.Header().Set("WWW-Authenticate", `Basic realm="The kingdom of Stavros"`) w.WriteHeader(401) w.Write([]byte("401 Unauthorized\n")) } else { fmt.Printf("Authenticated user %v.\n", username) token := getToken() setTokenCookie(w, token) fmt.Fprintf(w, "<html><head><script>location.reload()</script></head></html>") } }
译者信息
请求处理器
写一个处理器,来弹出一个基本的验证窗体不是很难,但是Go没有完美的文档,所以我必须自己一点点寻猎。其实非常简单,最终,这里就是HTTP基本验证的Go代码:
func handler(w http.ResponseWriter, r *http.Request) { if username := checkAuth(r); username == "" { w.Header().Set("WWW-Authenticate", `Basic realm="The kingdom of Stavros"`) w.WriteHeader(401) w.Write([]byte("401 Unauthorized\n")) } else { fmt.Printf("Authenticated user %v.\n", username) token := getToken() setTokenCookie(w, token) fmt.Fprintf(w, "<html><head><script>location.reload()</script></head></html>") } }
Setting the token and cookie
Once we’ve authenticated the user, we need to set a cookie with their token. We just do the same thing we did in Lua, above, only much more easily because Go includes actual crypto packages in the standard library. The code for this is equally straightforward, if underdocumented:
func getToken() string { expiration := int(time.Now().Unix()) + 3600 mac := hmac.New(sha1.New, []byte("some very secret string")) mac.Write([]byte(fmt.Sprintf("%v", expiration))) expectedMAC := fmt.Sprintf("%x", mac.Sum(nil)) return fmt.Sprintf("%v:%s", expiration, expectedMAC) } func setTokenCookie(w http.ResponseWriter, token string) { rawCookie := fmt.Sprintf("MyToken=%s", token) expire := time.Now().Add(time.Hour) cookie := http.Cookie{"MyToken", token, "/", ".example.com", expire, expire.Format(time.UnixDate), 3600, false, true, rawCookie, []string{rawCookie}} http.SetCookie(w, &cookie) }
译者信息
设置口令和cookie
一旦我们验证了一个用户之后,我们需要给他们的口令设置一个cookie。我门只需要做我们用Lua做过的同样的事情,如上,只是更加简单,因为Go在标准库里面就包括一个真加密包。这个代码一样很直接明了,即使没有完全文档化:
func getToken() string { expiration := int(time.Now().Unix()) + 3600 mac := hmac.New(sha1.New, []byte("some very secret string")) mac.Write([]byte(fmt.Sprintf("%v", expiration))) expectedMAC := fmt.Sprintf("%x", mac.Sum(nil)) return fmt.Sprintf("%v:%s", expiration, expectedMAC) } func setTokenCookie(w http.ResponseWriter, token string) { rawCookie := fmt.Sprintf("MyToken=%s", token) expire := time.Now().Add(time.Hour) cookie := http.Cookie{"MyToken", token, "/", ".example.com", expire, expire.Format(time.UnixDate), 3600, false, true, rawCookie, []string{rawCookie}} http.SetCookie(w, &cookie) }
Tying it all together
To finish our big bundle of amazingness, we just need a function that checks the authentication provided by the user, and we’re done! Here’s what I lifted off some library, it currently only checks for a specific username/password combination, so integrating with third-party services is left as an exercise for the reader:
func checkAuth(r *http.Request) string { s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) if len(s) != 2 || s[0] != "Basic" { return "" } b, err := base64.StdEncoding.DecodeString(s[1]) if err != nil { return "" } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { return "" } if pair[0] != "username" || pair[1] != "password" { return "" } return pair[0] }
译者信息
尝试把他们放在一起
来完成我们这一大段美妙的组合,我们只需要一个函数,用来检查由用户提供的验证信息,而且我们做到了!这里是我从一些库里面汲取出来的代码,当前它只是检查一个特定的用户名/密码的组合,所以和第三方的服务的集成就做为留给读者的作业吧:
func checkAuth(r *http.Request) string { s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) if len(s) != 2 || s[0] != "Basic" { return "" } b, err := base64.StdEncoding.DecodeString(s[1]) if err != nil { return "" } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { return "" } if pair[0] != "username" || pair[1] != "password" { return "" } return pair[0] }
Conclusion
I have taken quite a liking to nginx’s Lua module. It allows you to perform simple operations right in the web server’s request/response cycle, and it makes a lot of sense for some things like authentication checks to proxied web servers. This sort of thing would have been very hard to do without a programmable web server, as we would pretty much have to write our own HTTP proxy.
The code above is pretty short and rather elegant, so I’m very happy with it overall. I’m not sure how much time it adds to the response, but, given that authentication is necessary, I think it will be worth it (and fast enough not to be a problem anyway).
Another good thing about it is that you can enable this with just a single directive in an nginxlocationblock, so there’s no long configuration to keep track of. I find it a very elegant solution overall, and am very glad to know nginx lets me do something like this if I need it in the future.
If you have any recommendations or feedback, please leave a comment (especially if I’ve screwed something up).
译者信息
结论
我到目前对于nginx的Lua模块还是有着相当的喜欢。它允许你在web服务器的请求/响应周期里面做一些简单的操作,而且对于某些操作,比如为代理服务器做验证的检查,是很有意义的。这些事情对于一个不可编程的web服务器,一直很难,因此我们极可能需要写自己的HTTP代理服务。
上面的代码相当的简短,而且优雅,所以我对于上面的所有都感到高兴。我不能确定,这对于响应添加了多少额外的时间,不过,做一个验证是有好处的,我想这将值得去做(而且应该足够快,所以不是一个问题)。
另一个好处就是,你可以仅使用一个在nginxlocationblock里面的单独的directive来开启它,所以没有需要跟踪的配置项。我发现,总体而言,这是一个非常优雅的解决方案,而且我很高兴的了解到nginx可以让我去做这样的事情,可能是将来我需要去做的。
如果你有任何建言或者是反馈,请留下你的评语(特别是如果我把某些地方给弄错了)。
相关推荐
在本文中,我们将探讨如何使用Lua语言编写Nginx服务器的认证模块,以实现类似社交应用接入...使用Lua编写自定义的Nginx模块,能够灵活地定制认证逻辑,适应不同的应用场景,如社交应用接入,为用户提供无缝的登录体验。
二、Nginx模块化设计 Nginx的模块化设计是其灵活性的关键。主要分为核心模块、HTTP模块、邮件协议模块等,开发者可以根据需求选择或编写特定的模块。Nginx+Lua开发中,我们主要关注的是HTTP模块,特别是 ngx_lua ...
Nginx模块开发OpenResty简单使用笔记整理 ### Nginx简介 Nginx是当前最流行的HTTP Server之一,根据W3Techs的统计,目前世界排名(根据Alexa)前100万的网站中。与Apache相比。 同时,大量的第三方扩展模块也令...
例如,我们可以编写一个`lua`脚本来从请求头中提取`token`,然后使用`redis`进行验证。`lua`连接`redis`的库,如`lua-resty-redis`,可以帮助我们与`redis`进行通信,查询或设置键值。 以下是`lua`连接`redis`的一...
lua模块,并且将Nginx核心、LuaJIT、ngx_lua模块、许多有用的Lua库和常用的第三方Nginx模块组合在一起成为OpenResty,这样开发人员就可以安装OpenResty,使用Lua编写脚本,然后部署到Nginx Web容器中运行。...
Lua-Nginx-Module,简称lua-nginx-module,是Nginx服务器的一个重要扩展模块,它将强大的Lua脚本语言集成到Nginx中,允许用户在Nginx配置文件中直接编写Lua代码,极大地增强了Nginx的功能性和灵活性。版本0.10.13是...
lua-nginx模块是Nginx服务器的一个扩展,它为Nginx引入了对Lua脚本语言的支持。这个插件允许开发者在Nginx配置文件中嵌入Lua代码,实现更复杂的逻辑处理和功能增强,从而超越了传统静态配置文件的能力。版本0.9.14是...
本教程将详细解析如何使用Nginx、upload模块以及lua来搭建一个简单的文件上传系统。 首先,我们需要了解Nginx的ngx_http_upload_module模块,这是一个用于处理HTTP POST请求中的文件上传的第三方模块。它允许我们在...
1. **Nginx模块 ngx_lua**:Nginx与Lua的桥梁是ngx_lua模块,由OpenResty公司开发,它允许我们在Nginx配置文件中直接嵌入Lua脚本,实现动态处理请求。 2. **性能优势**:由于LuaJIT(Just-In-Time编译器)的存在,...
Lua-Nginx 模块是将 Lua 脚本语言集成到 Nginx Web 服务器中的一个强大插件。在标题 "lua-nginx-16" 中提到的是该模块的一个特定版本,即 0.9.16。这个版本可能包含了针对性能优化、功能增强或者错误修复的更新。...
- **OpenResty**:是一个基于Nginx与Lua的高性能Web平台,它极大地简化了在Nginx上编写复杂的Web应用的过程。OpenResty提供了强大的Lua API,可以用来处理HTTP请求和响应,实现复杂的功能逻辑。 #### 需求分析 本...
Lua模块是Nginx的一个扩展,允许在Nginx配置文件中嵌入Lua脚本,提供了更灵活的服务器配置和动态处理能力。在开发过程中,结合Nginx和Lua可以实现高效、轻量级的Web服务。 2. **安装Nginx** 安装Nginx需要先确保...
我们可以在 nginx.conf 文件中添加一个 Lua 模块,然后使用 Lua 语言编写脚本来处理业务逻辑。 在本文中,我们将使用 Lua 语言编写一个脚本来处理业务逻辑,该脚本将从 Redis 数据库中读取数据,然后将其转换为 ...
例如,一个典型的流程可能是:客户端发送请求到Nginx,Nginx通过lua-redis-pconnector插件将请求转发到Redis集群,Redis处理请求并返回结果,Nginx再将结果回传给客户端。同时,Lua脚本可以用来处理登录验证,通过...
ngx_devel_kit(简称ndk)是一个用于开发Nginx模块的工具集,它提供了很多用于编写C语言扩展的便利函数。在0.10.9rc7这个版本中,可能需要特定的ndk版本才能确保所有功能正常运行。如果版本不匹配,可能会遇到如时间...
在本项目中,需要使用OpenResty版本的Nginx,OpenResty是一个集成LuaJIT的Nginx发行版,允许开发者使用Lua语言直接在Nginx中编写高性能的网络应用。结合lua版WAF,Nginx可以解析HTTP请求,然后将这些请求传递给Lua...
Lua-nginx-module,又称为ngx_lua,是一个极其重要的开源项目,它将强大的Lua脚本语言引入到Nginx服务器中,使得Nginx具备了更丰富的功能扩展能力。通过这个模块,我们可以利用Lua的简洁语法和高效性能来处理HTTP...
在这个location块内,可以使用`content_by_lua_file`指令来指定一个Lua脚本文件,比如`thumbnail.lua`或`config.lua`,这样Nginx将在接收到图片请求时运行这个脚本。 3. **Lua脚本编写**:`thumbnail.lua`可能是...
这是一个非官方的Nginx模块,它提供了处理HTTP POST请求中文件上传的功能。模块的核心功能包括接收上传的文件、将文件临时存储、以及支持文件名重命名和保存到特定目录等。通过配置Nginx的配置文件(如`yup.conf`)...
《Lua-Nginx-Module完全指南》是一份深入解析如何使用Lua在Nginx中编写脚本的文档。本文档涵盖了指令、API接口以及相关的使用方法,为开发者提供了全面的指导。Nginx-Lua模块使得在Nginx配置文件中嵌入Lua脚本成为...