我有一个上游服务器处理我们网站的登录。登录成功后,我想将用户重定向到网站的安全部分。登录失败后,我想将用户重定向到登录表单。
200 OK
如果登录成功,上游服务器将返回,如果401 Unauthorized
登录失败,则返回 。
这是我的配置的相关部分:
{
error_page 401 = @error401
location @error401 {
return 302 /login.html # this page holds the login form
}
location = /login { # this is the POST target of the login form
proxy_pass http://localhost:8080;
proxy_intercept_errors on;
return 302 /secure/; # without this line, failures work. With it failed logins (401 upstream response) still get 302 redirected
}
}
此设置仅在成功登录时有效。客户端将使用 302 重定向。这才不是登录失败时可以正常工作。上游服务器返回 401,我预计error_page
随后会启动。但我仍然得到 302。如果我删除该return 302 /secure/
行,重定向到登录页面即可正常工作。因此,似乎我可以选择其中之一,但不能同时选择两者。
附加问题:我怀疑我处理该命名位置的方式error_page
是否正确。我这样做正确吗?
编辑:事实证明,块return
中有一个location
会使 Nginx 根本不使用proxy_pass
。因此,错误页面不会被访问是合理的。然而,如何做到这一点的问题仍然存在。
答案1
这精确的解决这个问题的方法是使用 Nginx 的 Lua 功能。
在 Ubuntu 16.04 上,你可以使用以下命令安装支持 Lua 的 Nginx 版本:
$ apt install nginx-extra
在其他系统上可能会有所不同。您也可以选择安装 OpenResty。
使用 Lua,您可以完全访问上游响应。请注意,您出现通过变量访问上游状态$upstream_status
。在某种程度上,您可以这样做,但由于 Nginx 中“if”语句的评估方式,您无法$upstream_status
在“if”语句中使用条件。
使用 Lua 你的配置将如下所示:
location = /login { # the POST target of your login form
rewrite_by_lua_block {
ngx.req.read_body()
local res = ngx.location.capture("/login_proxy", {method = ngx.HTTP_POST})
if res.status == 200 then
ngx.header.Set_Cookie = res.header["Set-Cookie"] # pass along the cookie set by the backend
return ngx.redirect("/shows/")
else
return ngx.redirect("/login.html")
end
}
}
location = /login_proxy {
internal;
proxy_pass http://localhost:8080/login;
}
非常简单。唯一的两个怪癖是读取请求主体以传递 POST 参数和在最终响应中设置 cookie 给客户端。
我实际上在社区的大量敦促下,我最终决定在客户端处理上游流响应。这保持上游服务器不变,我的 Nginx 配置也很简单:
location = /login {
proxy_pass http://localhost:8080;
}
发起请求的客户端处理上游响应:
<body>
<form id='login-form' action="/login" method="post">
<input type="text" name="username">
<input type="text" name="password">
<input type="submit">
</form>
<script type='text/javascript'>
const form = document.getElementById('login-form');
form.addEventListener('submit', (event) => {
const data = new FormData(form);
const postRepresentation = new URLSearchParams(); // My upstream auth server can't handle the "multipart/form-data" FormData generates.
postRepresentation.set('username', data.get('username'));
postRepresentation.set('password', data.get('password'));
event.preventDefault();
fetch('/login', {
method: 'POST',
body: postRepresentation,
})
.then((response) => {
if (response.status === 200) {
console.log('200');
} else if (response.status === 401) {
console.log('401');
} else {
console.log('we got an unexpected return');
console.log(response);
}
});
});
</script>
</body>
上述解决方案实现了我的目标,即明确分离关注点。身份验证服务器不了解调用者想要支持的用例。
答案2
虽然我完全同意@michael-hampton的观点,即这个问题应该不是由 nginx 处理,您是否尝试过移入error_page
位置块:
{
location @error401 {
return 302 /login.html # this page holds the login form
}
location = /login { # this is the POST target of the login form
proxy_pass http://localhost:8080;
proxy_intercept_errors on;
error_page 401 = @error401;
return 302 /secure/; # without this line, failures work. With it failed logins (401 upstream response) still get 302 redirected
}
}
答案3
我不完全确定下面的方法是否有效,我同意迈克尔的观点,但你可以尝试使用HTTP 身份验证请求模块,可能是这样的:
location /private/ {
auth_request /auth;
...
}
location = /auth {
proxy_pass ...
proxy_pass_request_body off;
proxy_set_header Content-Length "";
# This is only needed if your auth server uses this information,
# for example if you're hosting different content which is
# only accessible to specific groups, not all of them, so your
# auth server checks "who" and "what"
proxy_set_header X-Original-URI $request_uri;
error_page 401 = @error401;
}
location @error401 {
return 302 /login.html
}
您可以使用这个配置片段不是在执行实际身份验证的服务器上,但在托管受保护内容的服务器上。我现在无法测试这一点,但也许这对你仍然有帮助。
关于您的奖励问题:是的,据我所知,应该这样处理。
答案4
您需要 proxy_intercept_errors on
在服务器级别进行设置以处理代理错误。
server {
server_name mydomain.com;
proxy_intercept_errors on;
location @error401 {
return 302 /login.html # this page holds the login form
}
location / {
proxy_pass http://localhost:8080;
error_page 401 = @error401;
}
}