使用Lua编写一个Nginx认证模块

开发 前端
过去两天里,我解决了一个非常有趣的问题。我用一个nginx服务器作为代理,需要能够向其中添加一个认证层,使其能够使用外部的认证源(比如某个web应用)来进行验证,如果用户在外部认证源有账号,就可以在代理里认证通过。

过去两天里,我解决了一个非常有趣的问题。我用一个nginx服务器作为代理,需要能够向其中添加一个认证层,使其能够使用外部的认证源(比如某个web应用)来进行验证,如果用户在外部认证源有账号,就可以在代理里认证通过。

需求一览

我考虑了几种解决方案,罗列如下:

  • 用一个简单的Python/Flask模块来做代理和验证。
  • 一个使用subrequests做验证的nginx模块(nginx目前可以做到这一点)
  • 使用Lua编写一个nginxren认证模块

很显然,给整个系统添加额外请求将执行的不是很好,因为这将会增加延迟(特别是给每一个页面文件都增加一个请求是很让人烦恼的).这就意味着我们把 subrequest模块排除在外了。Python/Flash解决方案好像对nginx支持的也并不好,所以咱也把它排除了。就剩Lua了,当然 nginx对原生化支持得不错的。

因为我不想再扩展的服务器上对每一个请求都做认证,所以我决定生成一些令牌,这样人们就可以将它保存起来,并把它呈现给服务器,然后服务器就让请求通过。然而,因为Lua模块没有一种保持状态的方式(我已经发现),所以我们不能将令牌随处存储。当你没有更多的内存时,怎样来验证用户所说的话呢?

解决问题

加密签名的方式可是咱的救星!我们可以拿用户的用户名和过期时间数据来给用户添加签名的cookies,这样就能很容易的验证每个用户是谁了,同时我们就不用令牌了。

在nginx中,我们要做的就是直接在指定位置配置access_by_lua_file /our/file.lua,这样这个指定位置就可以保护我们的脚本了。现在,让我们一起来写代码:

  1. -- Some variable declarations.  
  2. local cookie = ngx.var.cookie_MyToken  
  3. local hmac = ""  
  4. local timestamp = ""  
  5. local timestamp_time = 0 
  6.  
  7. -- Check that the cookie exists.  
  8. if cookie == nil or cookie:find(":") == nil then  
  9.     -- Internally rewrite the URL so that we serve  
  10.     -- /auth/ if there's no cookie.  
  11.     ngx.exec("/auth/")  
  12. else 
  13.     -- If there's a cookie, split off the HMAC signature  
  14.     -- and timestamp.  
  15.     local divider = cookie:find(":")  
  16.     hmac = cookie:sub(divider+1)  
  17.     timestamp = cookie:sub(0, divider-1)  
  18. end  
  19.  
  20. -- Verify that the signature is valid.  
  21. if hmac_sha1("some very secret string", timestamp) ~= hmac or tonumber(timestamp) < os.time() then  
  22.     -- If invalid, send to /auth/ again.  
  23.     ngx.exec("/auth/")  
  24. end 

上面的代码可以直接运行。我们用一些明文来签名(这种情况下用的是一个时间戳,当然你可以用任何你想用的),之后我们用密文生成HMAC(哈希信息认证码),然后一个签名就生成了,这样用户就不能篡改为无效信息了。

当用户试图载入一个资源的时候,我们会检查cookie里面的签名是否有效,如果是,就通过他的请求。反之,我们会把他们重定向到一个发行口令的服务器,这个服务器会验证并且在没有的情况下给予他们一个签名的口令。

明锐的你可能会发现,上面的代码存在时间上的漏洞。如果你没有发现,别难过。嗯,也许会有点难过。

这里是一段Lua的代码,用来比较两个字符串在恒定时间上的等值关系(因而能够阻止任何时间上的攻击,除非我忽视了什么,这极为可能

  1. function compare_strings(str1, str2)  
  2.     -- Constant-time string comparison function.  
  3.     local same = true  
  4.     for i = 1#str1 do  
  5.         -- If the two strings' lengths are different, sub()  
  6.         -- will just return nil for the remaining length.  
  7.         c1 = str1:sub(i,i)  
  8.         c2 = str2:sub(i,i)  
  9.         if c1 ~= c2 then  
  10.             same = false  
  11.         end  
  12.     end  
  13.     return same  
  14. end 

我已经在函数上应用了时间来区分,如我所知,这是一个在恒定时间下的等值字符串。不同长度的字符串会稍稍改变时间,也许是因为子过程sub应用了一个不同的分支而导致的。而且,c1~=c2分支显然不是恒定时间的,但是在实际中,它相当接近恒定,所以于我们的例子不会有影响。我更倾向于使用XOR操作,从而确定两个字符串的XOR结果是否为0, 不过Lua似乎不包括二进制位的XOR操作。如果我在这个判断上有误,对于任何纠正我都很感激。

口令发行服务器

现在,我们已经写了一些很棒的口令检查代码,所有需要做的,只是写一个服务器来真正的发行这些口令。我本可以用Python以及Flask来写这个服务器,不过我还是想用Go做一个尝试,因为我是一个计算机语言潮人而且Go看上去“酷”。使用Python大概会快一些,不过我乐意用Go。

这个服务器会弹出一个HTTP基础验证的表单,检查你输入的帐户,如果正确,它会给你一个签名的口令,适合于一个小时的代理服务器访问。这样,你只需要验证外部服务一次,而随后的身份验证的检查将在nginx层面,而且会相当的快。

请求处理器

写一个处理器,来弹出一个基本的验证窗体不是很难,但是Go没有完美的文档,所以我必须自己一点点寻猎。其实非常简单,最终,这里就是HTTP基本验证的Go代码:

  1. func handler(w http.ResponseWriter, r *http.Request) {  
  2.     if username := checkAuth(r); username == "" {  
  3.         w.Header().Set("WWW-Authenticate", `Basic realm="The kingdom of Stavros"`)  
  4.         w.WriteHeader(401)  
  5.         w.Write([]byte("401 Unauthorized\n"))  
  6.     } else {  
  7.         fmt.Printf("Authenticated user %v.\n", username)  
  8.         token := getToken()  
  9.         setTokenCookie(w, token)  
  10.         fmt.Fprintf(w, "<html><head><script>location.reload()</script></head></html>")  
  11.     }  

设置口令和cookie

一旦我们验证了一个用户之后,我们需要给他们的口令设置一个cookie。我门只需要做我们用Lua做过的同样的事情,如上,只是更加简单,因为Go在标准库里面就包括一个真加密包。这个代码一样很直接明了,即使没有完全文档化:

  1. func getToken() string {  
  2.     expiration := int(time.Now().Unix()) + 3600 
  3.     mac := hmac.New(sha1.New, []byte("some very secret string"))  
  4.     mac.Write([]byte(fmt.Sprintf("%v", expiration)))  
  5.     expectedMAC := fmt.Sprintf("%x", mac.Sum(nil))  
  6.  
  7.     return fmt.Sprintf("%v:%s", expiration, expectedMAC)  
  8. }  
  9.  
  10. func setTokenCookie(w http.ResponseWriter, token string) {  
  11.     rawCookie := fmt.Sprintf("MyToken=%s", token)  
  12.     expire := time.Now().Add(time.Hour)  
  13.     cookie := http.Cookie{"MyToken",  
  14.         token,  
  15.         "/",  
  16.         ".example.com",  
  17.         expire,  
  18.         expire.Format(time.UnixDate),  
  19.         3600,  
  20.         false,  
  21.         true,  
  22.         rawCookie,  
  23.         []string{rawCookie}}  
  24.     http.SetCookie(w, &cookie)  

尝试把他们放在一起

来完成我们这一大段美妙的组合,我们只需要一个函数,用来检查由用户提供的验证信息,而且我们做到了!这里是我从一些库里面汲取出来的代码,当前它只是检查一个特定的用户名/密码的组合,所以和第三方的服务的集成就做为留给读者的作业吧:

  1. func checkAuth(r *http.Request) string {  
  2.     s := strings.SplitN(r.Header.Get("Authorization"), " "2)  
  3.     if len(s) != 2 || s[0] != "Basic" {  
  4.         return ""  
  5.     }  
  6.  
  7.     b, err := base64.StdEncoding.DecodeString(s[1])  
  8.     if err != nil {  
  9.         return ""  
  10.     }  
  11.     pair := strings.SplitN(string(b), ":"2)  
  12.     if len(pair) != 2 {  
  13.         return ""  
  14.     }  
  15.     if pair[0] != "username" || pair[1] != "password" {  
  16.         return ""  
  17.     }  
  18.     return pair[0]  

结论

我到目前对于nginx的Lua模块还是有着相当的喜欢。它允许你在web服务器的请求/响应周期里面做一些简单的操作,而且对于某些操作,比如为代理服务器做验证的检查,是很有意义的。这些事情对于一个不可编程的web服务器,一直很难,因此我们极可能需要写自己的HTTP代理服务。

上面的代码相当的简短,而且优雅,所以我对于上面的所有都感到高兴。我不能确定,这对于响应添加了多少额外的时间,不过,做一个验证是有好处的,我想这将值得去做(而且应该足够快,所以不是一个问题)。

另一个好处就是,你可以仅使用一个在nginxlocationblock里面的单独的directive来开启它,所以没有需要跟踪的配置项。我发现,总体而言,这是一个非常优雅的解决方案,而且我很高兴的了解到nginx可以让我去做这样的事情,可能是将来我需要去做的。

如果你有任何建言或者是反馈,请留下你的评语(特别是如果我把某些地方给弄错了)。

英文原文:Writing an nginx authentication module in Lua

译文链接:http://www.oschina.net/translate/writing-an-nginx-authentication-module-in-lua

责任编辑:林师授 来源: OSCHINA编译
相关推荐

2021-04-13 06:35:13

Elixir语言编程语言软件开发

2021-07-06 14:36:05

RustLinux内核模块

2014-07-24 14:35:26

Linux内核模块

2012-07-30 09:40:52

Lua

2017-01-12 22:36:30

2024-04-16 08:09:36

JavapulsarAPI

2011-08-23 18:04:40

LuaWiresharkDissector

2022-10-21 13:14:41

lua插件neovim

2021-04-07 13:38:27

Django项目视图

2020-10-12 10:58:15

IDEA插件监听

2023-12-11 08:16:51

工具命令行参数

2009-07-29 09:58:38

民工通过CCNACCNA

2023-09-26 16:44:14

光模块

2016-01-06 09:57:23

编写PHP扩展

2023-12-12 08:08:17

插件PRPulsar

2015-08-19 09:29:35

Git协议编写

2011-08-23 17:42:42

Lua脚本

2021-02-05 16:03:48

JavaScript游戏学习前端

2021-01-01 19:30:21

Python编程语言

2022-03-22 06:33:49

Python内置模块函数
点赞
收藏

51CTO技术栈公众号