Ajax-requests 中的 CORS 针对具有 IdentityServer3 授权的 MVC 控制器

CORS in Ajax-requests against an MVC controller with IdentityServer3-authorization

我目前在使用各种 Ajax-requests 来保存、加载和自动完成数据的网站上工作。它是使用 C#、MVC 和 JQuery 构建的。 MVC 控制器上的所有操作都需要用户获得授权,我们使用 IdentityServer3 进行身份验证。使用NuGet安装,当前版本为2.3.0.

当我打开页面并按下按钮时,一切正常。问题似乎发生在某个 session 过期时。如果我闲置一段时间,并尝试使用 Ajax-function,它会生成以下错误:

XMLHttpRequest cannot load https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email+phone+roles [...]. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:12345' is therefore not allowed access.

据我所知Ajax,问题本身很简单。 MVC 站点已失去对当前 session 的跟踪,它正在要求客户端再次进行身份验证。我从 Ajax-request 得到的响应是“302 Found”,其中 Location-header 指向我们的 IdentityServer。 IdentityServer 恰好在另一个域上,虽然在执行常规 HTTP-requests 时它工作正常,但它在 Ajax-requests 时效果不是特别好。 "Same Origin Policy" 直接阻止 Ajax-function 进行身份验证。如果我刷新页面,我将被重定向到 IdentityServer 并正常进行身份验证。几分钟后一切都会恢复正常。

解决方案可能是在来自 IdentityServer 的响应消息中添加一个额外的 header,明确声明允许此服务 cross-origin 请求。

我目前没有从 IdentityServer(在 Fiddler 中检查)得到这个header。

According to the docs,应该默认开启。我已经检查过我们确实以这种方式启用了 CORS:

factory.CorsPolicyService = new Registration<ICorsPolicyService>(new DefaultCorsPolicyService { AllowAll = true });

这是我的一位客户:

new Client
{
    Enabled = true,
    ClientName = "Foo",
    ClientId = "Bar",
    ClientSecrets = new List<Secret>
    {
        new Secret("Cosmic")
    },
    Flow = Flows.Implicit,
    RequireConsent = false,
    AllowRememberConsent = true,
    AccessTokenType = AccessTokenType.Jwt,
    PostLogoutRedirectUris = new List<string>
    {
        "http://localhost:12345/",
        "https://my.domain.com"
    },
    RedirectUris = new List<string>
    {
        "http://localhost:12345/",
        "https://my.domain.com"
    },
    AllowAccessToAllScopes = true
}

这些设置不起作用。我注意到我在此处的 URI 中有一个额外的正斜杠,但如果我删除它们,我会得到默认的 IdentityServer-error,表明客户端未被授权(错误的 URI)。如果我部署站点(而不是 运行 本地主机调试),我使用没有尾部斜杠的域名,并且我得到与调试中完全相同的行为。我确实注意到上面的错误消息中没有尾部斜杠,我认为这可能是问题所在,直到我在站点的部署版本中看到同样的东西。

我也做了自己的policy provider,像这样:

public class MyCorsPolicyService : ICorsPolicyService
{
    public Task<bool> IsOriginAllowedAsync(string origin)
    {
        return Task.FromResult(true);
    }
}

...然后我像这样将它插入到 IdentityServerServiceFactory 中:

factory.CorsPolicyService = new Registration<ICorsPolicyService>(new MyCorsPolicyService());

想法是 return 无论来源如何。这也不起作用;与之前完全相同的结果。

我已经阅读了关于这个特定主题的十几个其他线程,但我一无所获。据我所知,在不同站点的设置方面,我们没有做任何不寻常的事情。都差不多out-of-the-box了。有什么建议吗?

-----更新-----

问题依然存在。我现在尝试了一些新的策略。我在某处读到 cookie 身份验证对 Ajax-requests 不利,我应该改用不记名令牌。我在 Ajax 中这样设置:

$(function () {
    $(document).ajaxSend(function (event, request, settings) {
        console.log("Setting bearer token.");
        request.setRequestHeader("Authorization", "Bearer " + $bearerToken);
    });
});

Chrome 中的控制台和 Fiddler 均确认令牌确实存在并由 JQuery 发送。我使用的令牌来自 access_token-property 声明主体 object 来自 HttpContext.GetOwinContext().Authentication.User.

这没什么用。我仍然从服务器收到 302 响应,并且 Fiddler 显示令牌未在以下 Ajax-request(这是一个 GET-request)上发送到 IdentityServer。

从那里,我读到了这个帖子: Handling CORS Preflight requests to ASP.NET MVC actions 我试图将此代码放入 IdentityServer 的 startup.cs,但似乎没有 "preflight" 请求进入。我在 Fiddler 中看到的是(从一开始):

1 - 从客户端到 MVC 控制器的初始 Ajax-request:

POST http://localhost:12345/my/url HTTP/1.1
Host: localhost:12345
Connection: keep-alive
Content-Length: pretty long
Authorization: Bearer <insert long token here>
Origin: http://localhost:12345
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Referer: http://localhost:12345/my/url
Accept-Encoding: gzip, deflate
Accept-Language: nb-NO,nb;q=0.8,no;q=0.6,nn;q=0.4,en-US;q=0.2,en;q=0.2
Cookie: OpenIdConnect.nonce.<insert 30 000 lbs of hashed text here>

param=fish&morestuff=salmon&crossDomain=true

2 - 来自 MVC 控制器的重定向响应:

HTTP/1.1 302 Found
Cache-Control: private
Location: https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345%2f&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email [...]
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-AspNet-Version: 4.0.30319
Set-Cookie: OpenIdConnect.nonce.<lots of hashed text>
X-SourceFiles: <more hashed text>
X-Powered-By: ASP.NET
Date: Fri, 15 Jan 2016 12:23:08 GMT
Content-Length: 0

3 - Ajax-request 到 IdentityServer:

GET https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345%2f&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email [...]
Host: identityserver.domain.com
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://localhost:12345
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://localhost:12345/my/url
Accept-Encoding: gzip, deflate, sdch
Accept-Language: nb-NO,nb;q=0.8,no;q=0.6,nn;q=0.4,en-US;q=0.2,en;q=0.2

4 - 来自 IdentityServer3 的响应

HTTP/1.1 302 Found
Content-Length: 0
Location: https://identityserver.domain.com/login?signin=<some hexadecimal id>
Server: Microsoft-IIS/8.5
Set-Cookie: SignInMessage.<many, many, many hashed bytes>; path=/; secure; HttpOnly
X-Powered-By: ASP.NET
Date: Fri, 15 Jan 2016 12:23:11 GMT

5 - Chrome

的崩溃

XMLHttpRequest cannot load https://identityserver.domain.com/connect/authorize?client_id=Bar&blahblahblah. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:12345' is therefore not allowed access.

我最近遇到了这个问题,它是由 header X-Requested-With 与 AJAX 请求一起发送引起的。删除此 header 或拦截它并使用 401 处理它将使您走上正轨。

如果您没有这个 header,问题很可能是由另一个 header 触发 Access-Control-Allow-Origin 响应引起的。

如您所见,您在 Identity Server 中对 CORS 所做的任何操作都无法解决此问题。

事实证明,问题出在 MVC 中的客户端配置上。我缺少 UseTokenLifetime 属性,它应该设置为 false。

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ClientId = "Bar",
        Scope = "openid profile email phone roles",
        UseTokenLifetime = false,
        SignInAsAuthenticationType = "Cookies"
    [...]

出于某种原因,IdentityServer 将所有这些 cookie 设置为在分发后 5 分钟内过期。此特定设置将覆盖 IdentityServer 的微小过期时间,而是使用 aprox。 10 小时,或您的客户端应用程序中的任何默认值。

可以说这足以解决问题了。然而,如果用户决定在网站上闲置 10 个小时,除了 Ajax-buttons.

,什么都不点击,这将不可避免地 return

https://github.com/IdentityServer/IdentityServer3/issues/2424

我也遇到了这个问题,UseTokenLifetime = false 没有解决这个问题,因为你失去了 STS 上的令牌有效性。

当我尝试访问授权的 api 方法时,即使我在 Owin 上有效,我仍然得到 401。

我找到的解决方案是将 UseTokenLifetime = true 保留为默认值,但要编写一个全局 ajax 错误处理程序(或 angular http 拦截器),如下所示:

$.ajaxSetup({
global: true,
error: function(xhr, status, err) {
    if (xhr.status == -1) { 
       alert("You were idle too long, redirecting to STS") //or something like that
       window.location.reload();
    }
}});

触发身份验证工作流程。

我在使用 OWIN Middleware for OpenIDConnect 和不同的身份提供者时遇到了类似的问题。但是,该行为是在 1 小时而不是 5 分钟后发生的。解决方案是检查请求是否是 AJAX 请求,如果是,则将其强制为 return 401 而不是 302。这是执行此操作的代码:

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    ClientId = oktaOAuthClientId,
    Authority = oidcAuthority,
    RedirectUri = oidcRedirectUri,
    ResponseType = oidcResponseType,
    Scope = oauthScopes,
    SignInAsAuthenticationType = "Cookies",
    UseTokenLifetime = true,
    Notifications = new OpenIdConnectAuthenticationNotifications
    {
        AuthorizationCodeReceived = async n =>
        {
            //...
        },
        RedirectToIdentityProvider = n => //token expired!
        {
            if (IsAjaxRequest(n.Request))
            {
                n.Response.StatusCode = 401;//for web api only!
                n.Response.Headers.Remove("Set-Cookie");
                n.State = NotificationResultState.HandledResponse;
            }
            return Task.CompletedTask;
        },
    }
});

然后,我使用Angular拦截器检测到状态码为401,并重定向到认证页面。