`

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

阅读更多

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模块开发OpenResty简单使用笔记整理.zip

    Nginx模块开发OpenResty简单使用笔记整理 ### Nginx简介 Nginx是当前最流行的HTTP Server之一,根据W3Techs的统计,目前世界排名(根据Alexa)前100万的网站中。与Apache相比。 同时,大量的第三方扩展模块也令...

    nginx+lua简要说明

    lua模块,并且将Nginx核心、LuaJIT、ngx_lua模块、许多有用的Lua库和常用的第三方Nginx模块组合在一起成为OpenResty,这样开发人员就可以安装OpenResty,使用Lua编写脚本,然后部署到Nginx Web容器中运行。...

    lua-nginx-module完全指南.docx

    使用Lua编写Nginx脚本的基本构建块是指令。指令用于指定何时运行用户Lua代码以及如何使用结果。 在nginx.conf文件中各种*_by_lua,*_by_lua_block和*_by_lua_file配置指令内的用来配置的网关的Lua API。只能在这些...

    Nginx安装lua-nginx-module模块的方法步骤

    ngx_lua_module 是一个nginx http模块,它把 lua 解析器内嵌到 nginx,用来解析并执行lua 语言编写的网页后台脚本 特性很牛叉,可自行百度查看,这里主要是示范一下,如何在Nginx下安装lua-nginx-module模块 当然,...

    Nginx模块开发指南:使用C++11和Boost程序库.mobi

    Nginx[ 1] 是由 俄罗斯 工程师 Igor Sysoev 开发 的 一个 高性能 Web 服务器, 各方 面的 表现 均 远 超 传统 的 ... Nginx模块开发指南:使用C++11和Boost程序库 (Kindle 位置 348-357). 电子工业出版社. Kindle 版本.

    lightpath-nginx:使用Openresty和Redis用Lua编写的CDN

    LightPath CDN Nginx模块版本:1.0.0-beta描述CDN,内容交付网络,使用Openresty(Nginx)用Lua编写。 网站配置(后端,缓存规则,边缘规则等)存储在Redis中。 如果有兴趣,我会在以后添加适当的文档。 该项目之...

    ngx-http-cas-client-lua:使用CAS集成支持构建Nginx

    这是一个完全使用nginx的lua模块编写的CAS客户端。 这个想法是,您将通过CAS身份验证来保护Nginx位置。 通过提供一个CAS端点(目前在nginx.conf中必须具有一个相应的条目,请参阅“限制”部分),您将能够将访问...

    lua-nginx-redis:Redis,Lua,Nginx,OpenResty笔记和资料

    Nginx与Lua编写脚本的基本内置块是指令执行顺序的图 Nginx教程 基础 案例 模块 好文 流媒体 其他 Lua教程 Redis教程 Openresty教程 Linux教程 壳牌 微信公众号

    ua-lua:LUA用户代理检测器

    ua-lua是一个用LUA编写的小库,提供设备类型检测。 ua-lua正在基于nginx守护程序内的用户代理检测移动或平板设备。 也许不如WURFL准确,但它的处理速度快且小。 无需运行复杂的后端脚本,您可以通过将客户端...

    毕业设计论文范文源码-nginx-lua-module-zh-wiki:https://github.com/openresty/lua-ng

    编写的 Lua 扩展模块的搜寻路径(也可以用 ';;'): lua_package_cpath '/bar/baz/?.so;/blah/blah/?.so;;'; server { location /lua_content { # 通过 default_type 设置默认的 MIME 类型: default_type 'text/plain'...

    令人敬畏的:优质的OpenResty库和资源列表

    OpenResty是一个成熟的Web平台,它集成了标准的Nginx核心,LuaJIT,许多精心编写的Lua库,许多高质量的第三方Nginx模块以及它们的大多数外部依赖项。 它旨在帮助开发人员轻松构建可伸缩的Web应用程序,Web服务和...

    nuax:Lua 中的微框架

    Nuax 是一个基于 OpenResty 的微框架,用于编写简单的 JSON Web 服务。 例子: # nginx.conf http { server { listen 80; location / { content_by_lua ' local app = require "nuax" app:get("/hello/:name...

    淘宝开源的Web服务器 Tengine.zip

    加入一个模块不再需要重新编译整个Tengine;更多负载均衡算法支持。如会话保持,一致性hash等;输入过滤器机制支持。通过使用这种机制Web应用防火墙的编写更为方便;动态脚本语言Lua支持。扩展功能非常高效简单;...

    awesome-resty, 质量OpenResty库的列表和资源.zip

    awesome-resty, 质量OpenResty库的列表和资源 的restyopenresty/Nginx 模块,Lua库和相关...什么是 OpenResty OpenResty是一个完整的web平台,集成了标准的Nginx 核心。LuaJIT 。许多精心编写的Lua库。许多高质量的3

    Jitsimeet.apk

    能达到更低的延迟,更高的质量,并且如果你运行你自己的服务,这将是一个非常便于扩展和廉价的解决方案 Jitsi完全兼容webRTC这个开放的web通信标准 Jitsi支持高级的视频路由功能,比如同步广播、带宽检测、可扩展...

Global site tag (gtag.js) - Google Analytics