Nginx本身是一个很单纯的http服务器,其附加功能少的可怜,也正因为单纯,nginx才能如此高效。
我们常常会有一些简单的权限验证的需求,例如一个纯静态文件的wiki站点,需要对不同的人进行简单的权限控制。
Nginx本身可以实现,使用http_auth_basic_module模块可以进行最基本的权限验证。 但缺点是,修改密码必须登录服务器进行修改,十分不方便,而且当团队人多时,基本上不具有管理账号的功能。 稍大一些的团队会使用LDAP来做集中的账号管理中心。
使用auth_ldap模块可以实现基于ldap的authn,但基本没有authz的能力。
什么是authn/authz?常见的权限系统一般有两个任务,Authentication(常简称authn)和Authorization(常简称authz)。 authn的过程是验证一个用户是否是该系统内的合法用户,例如验证你是否拥有A网站的账号。 authz的过程是验证该用户是否有权限访问某项功能,例如你可能拥有账号,但你只能访问A网站的部分内容。
authn和authz有时会一起实现,有时也会分开实现。这两者都可能很简单也可能很复杂,看具体需求。
我们前面提到了使用LDAP做账号验证,通常是使用LDAP做authn。当然LDAP也支持简单的authz的功能, 可并不是任何时候都那么方便。
我在使用auth_ldap模块时碰到了有这些问题:
对每一个请求,都要该模块进行authn,当然该模块具有一定的缓存能力,但是当各种服务较多时,也会对ldap服务器带来较大的负担。
我需要稍微复杂一些authz,但也不需要过于复杂,支持基于uid list/group的验证即可,但创建组要足够方便,也支持嵌套组。
在网上搜了一圈,没有找到称心的解决方案,所以就自己写了一个。放在Github上了。 该项目目前有两个小模块,分别是simpleauthn_cookie和simpleauthz。
simpleauthn_cookie.lua这个模块本身并没有authn的功能,因为它不提供持久化存储账号信息的功能。它仅仅是对其他authn提供一个缓存的方案。
网站登录常见的做法:
用户发送自己的账号、密码
网站验证该账号、密码,如果验证成功,那么常见有两种做法:
在服务器生成一个session_id,以此作为key,账号信息作为value,存放在memcache、redis之类的数据库中,将session_id种到客户端的Cookie中(HTTP响应中带Set-Cookie头即可)。客户端后续访问时带着这个session_id的Cookie,服务器通过查表知道这个session_id对应的账号。
将用户的一些信息以及一些私钥生成一个hash,例如hash = md5(uid+secret),然后将uid、hash种到客户端cookie中。客户端后续访问时,服务器端验证用户提供的hash和uid是否满足 hash == md5(uid+secret),如果验证通过,则该用户是uid。
这种基于Cookie的验证缓存方案,可以大大减轻后端(如存放账号密码信息的数据库)的压力。simpleauthn_cookie.lua 就是这样一个模块。
使用这个模块,首先需要配置一个另外的server或location,用于真正的authn后端,其他路径的访问都用simpleauthn即可。例如:
init_by_lua "err">'simpleauthn "o">= require "s">"simpleauthn_cookie"
simpleauthn "p">.set_secret_key "p">("your-secret" "p">)
simpleauthn "p">.set_max_age "p">()
simpleauthn "p">.set_auth_url_fmt "p">(' "n">https: "c1">//auth.example.com/?%s')
';
server {
server_name "n">auth. "n">example. "n">com;
ssl "n">on;
ssl "n">configurations...
location "o">/ {
auth_ldap "s">"Please login with LDAP account";
"n">other_auth_ldap_configurations "p">...;
# "err">这里使用auth_ldap "err">模块做authn "err">,验证成功后,种一个cookie "err">。并跳转回登录前的页面。
content_by_lua "err">'simpleauthn "p">.set_cookie "p">(ngx "p">.var "p">.remote_user "p">, ".example.com" "p">)';
}
}
server {
server_name "n">app. "n">example. "n">com;
location "o">/ {
# "err">验证只需要一句话就可以了。如果发现没有登陆,会跳转到登陆页面进行登陆。
access_by_lua "err">'simpleauthn "p">.access "p">()';
}
}
simpleauthz.lua
这个模块提供了最基本的基于uid list/group的authz功能:
可以定义若干 group,每个 group 里包含一些 uid,支持嵌套。
可以定义一些规则,每个规则中,可以指定一个账号列表(specified by uid/group list),对这个列表进行allow或者deny的判定。
用法示例:
lua_package_path "err">'/ "n">path/ "n">to/ "n">module/? "p">.lua "p">;;';
init_by_lua "err">'-- "n">init authz
simpleauthz "o">= require "s">"simpleauthz"
simpleauthz "p">.create_group "p">("group1" "p">, "alice" "p">, "bob" "p">, ...)
simpleauthz "p">.create_group "p">("group2" "p">, "tom" "p">, "jerry" "p">, "@group1" "p">, ...)
simpleauthz "p">.create_rule "p">("RULE1" "p">, "allow" "p">, { "s">"@group1", "s">"Obama"}, "p">{})
simpleauthz "p">.create_rule "p">("RULE2" "p">, "allow" "p">, { "s">"@group2", "s">"alice"}, "p">{"jerry"})
simpleauthz "p">.create_rule "p">("RULE3" "p">, "deny" "p">, { "s">"@group1"}, "p">{})
-- "n">init authn
simpleauthn "o">= require "s">"simpleauthn_cookie"
simpleauthn "p">.set_secret_key "p">("your-secret" "p">)
simpleauthn "p">.set_max_age "p">()
simpleauthn "p">.set_auth_url_fmt "p">( "s">"https://auth.example.com/?%s")
';
server {
server_name "n">apps. "n">example. "n">com;
location "o">/app1 "o">/ {
# "err">这里并没有用到authn "err">,仅仅将url "err">中的uid "err">参数当做用户的id "err">。
# "err">这里仅作演示,真实的服务器上是不会有人这么傻的。
# "n">RULE1: "err">允许alice "err">、bob "err">和Obama "err">访问/ "n">app1/。
access_by_lua "err">'simpleauthz "p">.access "p">("RULE1" "p">, ngx "p">.var "p">.arg_uid "p">)';
}
location "o">/app2 "o">/ {
# "err">这条规则,通过authn "p">.get_uid "p">()来获取当前登陆的用户,如果用户没有登陆,
# "err">则直接返回 "err">,当然,这样的用户体验也是很不好的。
# "n">RULE2:允许 "n">tom, "n">alice访问,但不允许 "n">jerry访问。
access_by_lua "err">'simpleauthz "p">.access "p">("RULE2" "p">, simpleauthn "p">.get_uid "p">())';
}
location "o">/app3 "o">/ {
# "err">这个用法,需要注意第二和第三个参数。第二个参数是一个函数,第三个参数是一个值。
# "err">首先调用函数来获取当前登陆的用户,如果没有登陆则跳转到登陆页面。若已登陆,则进行 "n">authz操作。
# "n">RULE3:不允许 "n">bob和 "n">alice访问,但允许所有其他人访问。
access_by_lua "err">'simpleauthz "p">.access_with_authn "p">("RULE3" "p">, simpleauthn "p">.get_uid "p">, simpleauthn "p">.get_auth_url "p">())';
}
有两条预定义的规则
"n">ALLOW_ALL: "err">允许所有已登录的用户
"n">DENY_ALL:禁止所有已登陆的用户(未登陆用户也无法访问)
location "o">/app4 "o">/ { "n">access_by_lua ' "n">simpleauthz. "n">access( "s">"ALLOW_ALL", "n">ngx. "n">var. "n">arg_uid) "err">'; }
location "o">/app5 "o">/ { "n">access_by_lua ' "n">simpleauthz. "n">access( "s">"DENY_ALL", "n">ngx. "n">var. "n">arg_uid) "err">'; }
location "o">/app6 "o">/ {
# "err">这个模块也可以跟nginx "err">的Access "err">模块一起使用,例如下面这个例子中,
# "err">如果客户端在192.168.0.0 "o">/ "err">这个子网中,则即使没有登陆或者 "n">authz失败了,同样可以访问。
satisfy "n">any;
allow "mf">192.168.0.0/;
deny "n">all;
access_by_lua "err">'simpleauthn "p">.access "p">()';
}
}
TODO
实现一个纯lua的ldap authn。由于nginx官方release tarball中并不带auth_ldap模块,因此用户需要自己编译。 如果有一个纯基于lua的ldap authn模块,那么就不需要编译了。