在使用FormsAuthentication配置的一个应用程序中,当用户访问没有auth cookie或过时的访问受保护页面时,ASP.NET会发出HTTP 401 Unauthorized,然后FormsAuthentication模块在请求结束前拦截此响应,并且为HTTP 302 Found更改它,设置HTTP标头“Location:/ path / loginurl”以便将用户代理重定向到登录页面,然后浏览器转到该页面并检索未受保护的登录页面,获得HTTP 200 OK。
当AJAX没有被考虑时,确实是一个非常好的主意。
现在我的应用程序中有一个返回JSON数据的URL,它需要用户进行身份验证。一切运作良好,问题是如果auth cookie到期,当我的客户端代码调用服务器时,它将获得HTTP 200 OK和登录页面的html,而不是HTTP 401 Unauthorized(因为之前解释过)。然后我的客户端试图将登录页面html解析为json,并失败。
那么问题是:如何应对来自客户端的过期身份验证?应对这种情况的最优雅解决方案是什么?我需要知道调用何时成功,我想使用HTTP语义进行调用。
是否可以以安全的跨浏览器方式从客户端读取自定义HTTP标头? 如果请求是AJAX请求,有没有办法告诉FormsAuthenticationModule不执行重定向? 有没有办法使用HTTP标头覆盖HTTP状态,就像覆盖HTTP请求方法一样?
我需要Forms身份验证,我想避免重写该模块或编写自己的表单身份验证模块。
问候。
答案 0 :(得分:23)
我遇到了同样的问题,不得不在MVC中使用自定义属性。您可以轻松地将其调整为在Web表单中工作,如果所有页面都从某个基页继承,则可以覆盖页面在页面中的授权(MVC中的全局属性允许相同的事情 - 覆盖所有控制器/操作的OnAuthorization方法应用程序)
这就是属性的样子:
public class AjaxAuthorizationAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest()
&& !filterContext.HttpContext.User.Identity.IsAuthenticated
&& (filterContext.ActionDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0
|| filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0))
{
filterContext.HttpContext.SkipAuthorization = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
filterContext.Result = new HttpUnauthorizedResult("Unauthorized");
filterContext.Result.ExecuteResult(filterContext.Controller.ControllerContext);
filterContext.HttpContext.Response.End();
}
}
}
请注意,您需要调用HttpContext.Response.End();或者您的请求将被重定向到登录(因此我丢失了一些头发)。
在客户端,我使用了jQuery ajaxError方法:
var lastAjaxCall = { settings: null, jqXHR: null };
var loginUrl = "yourloginurl";
//...
//...
$(document).ready(function(){
$(document).ajaxError(function (event, jqxhr, settings) {
if (jqxhr.status == 401) {
if (loginUrl) {
$("body").prepend("<div class='loginoverlay'><div class='full'></div><div class='iframe'><iframe id='login' src='" + loginUrl + "'></iframe></div></div>");
$("div.loginoverlay").show();
lastAjaxCall.jqXHR = jqxhr;
lastAjaxCall.settings = settings;
}
}
}
}
这显示iframe在当前页面上的登录(看起来像用户被重定向但你可以使它不同),当登录成功时,这个弹出窗口被关闭,原始的ajax请求重新发送:
if (lastAjaxCall.settings) {
$.ajax(lastAjaxCall.settings);
lastAjaxCall.settings = null;
}
这允许您的用户在会话过期时登录,而不会丢失他们在上次显示的表单中输入的任何作品或数据。
答案 1 :(得分:5)
我从其他帖子中大量窃取了这个答案,但一个想法可能是在登录页面上实现HttpModule
到intercept the redirect(该链接上的说明)。
如果请求是通过AJAX进行的,如果请求不是通过AJAX进行的,默认行为是正确的,那么您也可以修改该示例HttpModule以仅拦截重定向:
这就是:
class AuthRedirectHandler : IHttpModule
{
#region IHttpModule Members
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.EndRequest+= new EventHandler(context_EndRequest);
}
void context_EndRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if (app.Response.StatusCode == 302
&& app.Request.Headers["X-Requested-With"] == "XMLHttpRequest"
&& context.Response.RedirectLocation.ToUpper().Contains("LOGIN.ASPX"))
{
app.Response.ClearHeaders();
app.Response.ClearContent();
app.Response.StatusCode = 401;
}
}
#endregion
}
如果您的应用中还有其他合法的302重定向,您还可以确保重定向到您的实际登录页面。
然后你只需要添加到你的web.config:
<httpModules>
<add name="AuthRedirectHandler" type="SomeNameSpace.AuthRedirectHandler, SomeNameSpace" />
</httpModules>
总之。再一次,实际的原始思想进入了这个答案,我只是从SO和网络的其他部分把各种各样的东西拉到一起。
答案 2 :(得分:5)
我在实施accepted answer时遇到了问题。主要是,我的错误日志充满Server cannot set status after HTTP headers have been sent
错误。
我尝试实施accepted answer来质疑Server cannot set status after HTTP headers have been sent IIS7.5,但再次没有成功。
谷歌搜索了一下我偶然发现SuppressFormsAuthenticationRedirect
property
如果您的.Net版本是&gt; = 4.5,那么您可以将以下代码添加到自定义HandleUnauthorizedRequest
类的AuthorizeAttribute
方法中。
public sealed class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
base.HandleUnauthorizedRequest(filterContext);
return;
}
base.HandleUnauthorizedRequest(filterContext);
return;
}
}
重要的部分是if
块。如果您使用的话,这是最简单的方法.Net 4.5&amp;已经有自定义授权。