针对具有 IdentityServer3 授权的 MVC 控制器的 Ajax 请求中的 CORS

Posted

技术标签:

【中文标题】针对具有 IdentityServer3 授权的 MVC 控制器的 Ajax 请求中的 CORS【英文标题】:CORS in Ajax-requests against an MVC controller with IdentityServer3-authorization 【发布时间】:2016-04-18 12:43:31 【问题描述】:

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

当我打开页面并按下按钮时,一切正常。当某个会话到期时,似乎会出现该问题。如果我闲置一段时间,并尝试使用 Ajax 函数,则会生成以下错误:

XMLHttpRequest 无法加载 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 [...]。请求的资源上不存在“Access-Control-Allow-Origin”标头。 Origin 'http://localhost:12345' 因此不允许访问。

根据我对 Ajax 的了解,问题本身非常简单。 MVC 站点丢失了对当前会话的跟踪,它正在要求客户端再次进行身份验证。我从 Ajax 请求得到的响应是“302 Found”,带有指向我们的 IdentityServer 的 Location-header。 IdentityServer 恰好在另一个域上,虽然当您执行常规 HTTP 请求时它工作得很好,但它对于 Ajax 请求并不特别好。 “同源策略”直接阻止 Ajax 功能进行身份验证。如果我刷新页面,我将被重定向到 IdentityServer 并正常进行身份验证。几分钟后一切都会恢复正常。

解决方案可能是在来自 IdentityServer 的响应消息中添加一个额外的标头,明确指出此服务允许跨域请求。

我目前没有从 IdentityServer 获取此标头(在 Fiddler 中检查)。

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)。如果我部署站点(而不是运行 localhost 调试),我会使用不带斜杠的域名,我会得到与调试时完全相同的行为。我确实注意到上面的错误消息中没有斜杠,我认为这可能是问题所在,直到我在站点的已部署版本中看到相同的内容。

我还创建了自己的策略提供程序,如下所示:

public class MyCorsPolicyService : ICorsPolicyService

    public Task<bool> IsOriginAllowedAsync(string origin)
    
        return Task.FromResult(true);
    

...然后我将它插入 IdentityServerServiceFactory,如下所示:

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

这个想法是让它返回 true 而不管来源如何。这也不起作用;结果和之前一模一样。

我已经阅读了关于这个特定主题的十几个其他主题,但我无处可去。据我所知,在设置不同站点时,我们没有做任何不寻常的事情。这一切都是开箱即用的。有什么建议吗?

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

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

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

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

这并没有多大作用。我仍然从服务器收到 302 响应,Fiddler 显示令牌没有通过以下 Ajax 请求(这是一个 GET 请求)发送到 IdentityServer。

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

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

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 - 对 IdentityServer 的 Ajax 请求:

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 无法加载 https://identityserver.domain.com/connect/authorize?client_id=Bar&blahblahblah。请求的资源上不存在“Access-Control-Allow-Origin”标头。因此,不允许访问 Origin 'http://localhost:12345'。

【问题讨论】:

【参考方案1】:

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

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

我找到的解决方案是将 UseTokenLifetime = true 保留为默认值,但要编写一个全局 ajax 错误处理程序(或角度 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();
    
);

触发身份验证工作流程。

【讨论】:

【参考方案2】:

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

如果您没有此标头,则问题很可能是由触发 Access-Control-Allow-Origin 响应的不同标头引起的。

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

【讨论】:

我已经对此进行了调查,并且确实取得了一些进展。更改 X-Requested-With 和/或 Referer-headers 什么也没做。然而,我突然注意到,我的常规、有效的 HTTP 请求和我的 Ajax 请求之间的一个显着区别是缺少 cookie。我对我的 Ajax 做了这个: $.ajaxSetup( xhrFields: withCredentials: true );突然间,我得到了与普通 HTTP 请求相同的服务器响应。我得到“提交此表格”页面。即便如此,同样的错误仍然出现在我的 Javascript 控制台中。完全相同的行为。【参考方案3】:

我在使用 OWIN Middleware for OpenIDConnect 和不同的身份提供者时遇到了类似的问题。但是,该行为发生在 1 小时而不是 5 分钟之后。解决方案是检查请求是否是 AJAX 请求,如果是,则强制它返回 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 拦截器检测到 statusCode 为 401,并重定向到身份验证页面。

【讨论】:

【参考方案4】:

事实证明,问题出在 MVC 中的客户端配置中。我错过了应该设置为 false 的 UseTokenLifetime 属性。

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

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

可以说这足以解决问题。然而,如果用户决定在网站上闲置 10 个小时,只点击 Ajax 按钮,它将不可避免地返回。

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

【讨论】:

请注意,这是一个创可贴,而不是修复。即使 UseTokenLifeTime = true,请求仍应有效。如果您闲置足够长的时间,您仍然会看到同样的问题。我得出的结论是,这是一个具体的问题。

以上是关于针对具有 IdentityServer3 授权的 MVC 控制器的 Ajax 请求中的 CORS的主要内容,如果未能解决你的问题,请参考以下文章

一步一步学习IdentityServer3

我可以在没有客户端密码的情况下使用带有 PKCE 的 IdentityServer3 授权代码流吗?

一步一步学习IdentityServer3

使用 IdentityServer4.AccessTokenValidation 包向 IdentityServer3 授权 .NET 5 Web API 引用令牌时遇到问题

一步一步学习IdentityServer3

如何使用IdentityServer3进行登录吗