nginx+lua实现客户端请求token校验和动态获取转发uri实现rewrite
需求场景:
- 1、客户端请求服务端时通过nginx转发,但是需要完成对请求相关的身份校验,如果客户端携带的token不合法则不允许请求;
- 2、服务是多租户模式的,不同租户的客户端发起请求的uri所携带的标识不一致,且并非服务端接口实际的uri,另外还需要根据客户端发起请求携带的标识将服务请求转发至对应租户的服务端完成请求;
nginx的location代码块:
location ~ /38ad46566ae64a20896bd95769c89199 {
set $req_type "https";
#lua脚本完成对客户端token的校验
access_by_lua_file /usr/local/openresty/nginx/conf/lua/token_filter.lua;
#lua脚本完成对客户端请求的uri的动态转换
set_by_lua_file $router /usr/local/openresty/nginx/conf/lua/router.lua;
rewrite ^(.*) $router break;
proxy_pass https://upstream_38ad46566ae64a20896bd95769c89199;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
解决方案:
- 根据不同租户发起请求时的标识,将请求转发至对应的服务端
location ~ /38ad46566ae64a20896bd95769c89199 { }
可以看到location的匹配规则是通过通配符来匹配的,只要客户端请求的uri中包含
/38ad46566ae64a20896bd95769c89199
uri 作为一个 string,只要 regex 匹配其中的一部分,即可算作匹配成功,就认为请求是需要这个location去转发的,当然这也需要根据客户端实际的请求uri去决定使用哪种location匹配规则,我们服务中的客户端端请求的最后一级是租户的标识,所以使用regex 匹配是最合适的。
- 解决nginx转发的同时校验token方案
首选需要将nginx和lua配合使用,在nginx转发的同时,调用lua脚本完成对客户端请求时所携带token的校验工作,可以看到location代码块中增加了一行代码,引用了一个lua脚本,
#lua脚本完成对客户端token的校验
access_by_lua_file /usr/local/openresty/nginx/conf/lua/token_filter.lua;
这个lua脚本就是用来完成对客户端请求token的校验功能,下面一起来看token_filter.lua脚本的具体实现:
function getHost()
return ngx.var.host
end
function getUri()
return ngx.var.uri
end
function getToken()
--获取客户端请求的token,客户端的toekn存放在请求header的token属性
local reqHeaders = ngx.req.get_headers();
return reqHeaders["token"]
end
function token_filter(httpc, token, host)
local req_para = {}
local cjson = require "cjson"
--/session/validatetma认证接口中设置的token字段为id,所以这里将token放入json的属性是id
req_para["id"] = token
req_body = cjson.encode(req_para)
ngx.log(ngx.ERR,req_body)
//发起对认证服务接口的调用,
local resp,err = httpc:request_uri("https://127.0.0.1:8082/session/validatetma",
{
ssl_verify = false,
method = "POST",
--请求参数,包含了客户端请求token
body = req_body,
headers = {
["Content-Type"] = "application/json",
["Accept"] = "*/*",
["Host"] = host,
["Content-Length"] = #req_body
}
})
ngx.log(ngx.ERR,resp.body)
--请求相应为空,返回false,校验不通过,这种情况可能有多种,调用接口失败或者其他原因需要具体分析
if not resp then
return false
end
--请求接口成功
if resp.status == 200 then
local resp_body = cjson.decode(resp.body)
--校验成功返回true,放行
if resp_body["success"] == true then
return true
else
--校验成功返回false,无权限,拦截
return false
end
end
end
----- main lua被调用后从这里开始执行----
--引入必要的lua库,完成http请求,我这里是lua脚本获取到客户端token后通过调用token认证服务完成token校验的
--这只是其中的一种方案,如果token存在数据库或者redis中也可以引入对应的库lua直接调用数据库或redis完整认证
local http = require 'resty.http'
local httpc = http:new()
ngx.log(ngx.ERR,"-------------------- token_filter---------------")
--获取客户端请求的token,客户端的toekn存放在请求header的token属性
local token = getToken()
--请求的host
local host = getHost()
--如果客户端未携带toekn,直接返回401无权限
if (token == nil) then
httpc:close()
ngx.exit(401)
else
--客户端携带了token,对token进行校验,校验结果为true放行,为false返回401
if (token_filter(httpc, token, host) == false) then
httpc:close()
ngx.exit(401)
end
end
httpc:close()
这段增加了不少注释,详细大家都能够看明白,这只是nginx转发过程中校验客户端token的其中一种方案,如有更好的建议欢迎评论交流。
- 解决动态获取转发uri实现rewrite
由于客户端在请求的时候需要携带租户标识,所以客户端发起请求是的uri并不是服务侧接口实际的uri,需要根据一定的规则处理,并让nginx将请求转发出去,
客户端实际请求的uri:/pwdauth/38ad46566ae64a20896bd95769c89199
密码认证的uri,/pwdauth 这一级是密码认证对应服务端实际接口的别名
/38ad46566ae64a20896bd95769c89199 这一级是租户的唯一标识
如果不做任何处理,ngxin转发的uri还是**/pwdauth/38ad46566ae64a20896bd95769c89199**,但是服务端并没有对应的接口啊,肯定是404,肯定是不行的。
需要客户端的uri,进行转发,并且让nginx转发的时候使用真实的uri进行转发,这种需求同样可以使用nginx+lua相互配合来实现,大家也看到了,location代码块中还引用了另一个lua脚本。
#lua脚本完成对客户端请求的uri的动态转换,并将实际的uri赋值为$router
set_by_lua_file $router /usr/local/openresty/nginx/conf/lua/router.lua;
#重写nginx转发的uri为 $router
rewrite ^(.*) $router break;
这段代码就是来完成对客户端请求的uri转,并将实际的uri赋值为$router,再利用ngixn中的rewrite 将实际的uri进行设置,完整转发时uri的替换。
下面是router.lua脚本的具体代码实现,这个lua脚本实际上很简单,就是一个截取uri别名,根据别名从tables中取到实际服务端uri返回的过程:
-- lua中的tables实际和java的map很类似,<key,value>键值对,需要的是事先在lua脚本中设置好请求uri别名和实际uri的关系,这个是需要规范好的,需要将多有的接口梳理并放入tables
local tables = {
["/pwdauth"] = "/authn/pwdauth",
}
--获取客户端请求的uri
function getUri()
return ngx.var.uri
end
--对客户端请求的uri进行截取,并从tables中获取实际uri
function router(uri,tables)
ngx.log(ngx.ERR,"uri=",uri)
--获取uri字符串的长度
local length = string.len(uri)
--截取字符串,需要截取掉租户的标识,获取uri别名,我们服务站的标识是定长的,所以截取掉33位,剩余的就是uri别名了
local suburi = string.sub(uri, 0, length-33)
--使用uri别名最为key,从tables中取到实际的uri
local router = tables[suburi]
ngx.log(ngx.ERR,"router=", router)
return router
end
----- main lua被调用后从这里开始执行----
local uri = getUri()
ngx.log(ngx.ERR,"--------------------router---------------")
local rr = router(uri,tables)
--如果返回的实际uri为空,说明客户端发起的请求uri不合法,服务端并不存在这个接口,原样返回即可,反正也请求不到
if rr == nil then
return uri
else
-- 不为空,则返回真实的uri
return rr
end
相信根据lua脚本中的注释大家都能看明白,毕竟是在是太简单了这个逻辑,简单的说就是一个截取和从map中取值的过程。
获取到的实际uri需要赋值给ngixn变量$router,下一步就是使用nginx自带的uri替换语法(rewrite )进行uri替换了,将原本客户端发起的请求uri(/pwdauth/38ad46566ae64a20896bd95769c89199)替换成处理后的实际uri(/authn/pwdauth),ngixn只有将请求转发至/authn/pwdauth才能走通真正的业务逻辑,完整密码认证的操作。
总结:
到此nginx+lua实现客户端请求token校验和动态获取转发uri实现rewrite的使用场景就算可以正常使用了,并且目前使用还算稳定,基本没出过异常,不得不说ngxin还是很强大的。
proxy_pass https://upstream_38ad46566ae64a20896bd95769c89199;
大家有没有注意到location代码块中的这行,这行就是ngxin转发的服务端的地址,这里我们使用到了ngixn负载均衡策略upstream,如果服务端是负载的,就可以这样来实现,如果是单台服务那就不需要了,直接将upstream_38ad46566ae64a20896bd95769c89199这个替换成 ip:port即可,nginx的负载均衡配置以及使用方案网上很多,大家可以自行搜索。
不足之处还望指教,一起学习进步。