为什么不't浏览器在经过身份验证的XMLHttpRequest之后重用授权标头

Why doesn't the browser reuse the authorization headers after an authenticated XMLHttpRequest?

本文关键字:之后 XMLHttpRequest 授权 权标 身份验证 浏览器 经过 为什么不      更新时间:2023-09-26

我正在使用Angular开发单页应用程序。后端公开需要基本身份验证的REST服务。获取index.html或任何脚本都不需要身份验证。

我有一个奇怪的情况,其中一个视图有一个<img>,其中src是需要身份验证的REST API的url。<img>由浏览器处理,我没有机会为它发出的GET请求设置授权头。这会导致浏览器提示输入凭据。

我试图通过这样做来解决这个问题:

  1. 在源中保留img src为空
  2. 在"文档准备就绪"时,使用Authorization标头向服务(/api/login)发送XMLHttpRequest,以便进行身份验证
  3. 完成该调用后,设置img src属性,认为到那时,浏览器将知道在后续请求中包含Authorization标头

但事实并非如此。对图像的请求在没有标题的情况下发出。如果我输入凭据,则页面上的所有其他图像都是正确的。(我也尝试过Angular的ng-src,但结果相同)

我有两个问题:

  1. 为什么浏览器(IE10)在成功XMLHttpRequest之后没有在所有请求中包含标头
  2. 我能做些什么来解决这个问题

@bergi询问了请求的详细信息。它们在这儿

请求/api/登录

GET https://myserver/dev30281_WebServices/api/login HTTP/1.1
Accept: */*
Authorization: Basic <header here>
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/6.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 2.0.50727; .NET CLR 3.0.30729)
Connection: Keep-Alive

响应(/api/login)

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 4
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 20 Dec 2013 14:44:52 GMT

请求/用户/图片/2218:

GET https://myserver/dev30281_WebServices/api/user/picture/2218 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/6.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR 2.0.50727; .NET CLR 3.0.30729)
Connection: Keep-Alive

然后web浏览器提示输入凭据。如果我输入它们,我会得到这样的响应:

HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Content-Length: 3119
Content-Type: image/png
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 20 Dec 2013 14:50:17 GMT

基本思想

通过JavaScript加载图像并在网站上显示。其优点是身份验证凭据永远不会进入HTML。他们会在JavaScript方面进行抵抗。

步骤1:通过JS加载图像数据

这是AJAX的基本功能(另请参阅XMLHttpRequest::open(method, uri, async, user, pw)):

var xhr = new XMLHttpRequest();
xhr.open("GET", "your-server-path-to-image", true, "username", "password");
xhr.onload = function(evt) {
  if (this.status == 200) {
    // ...
  }
};

步骤2:格式化数据

现在,我们如何显示图像数据?当使用HTML时,通常会为图像元素的src属性分配一个URI。我们可以在这里应用相同的原理,除了我们使用数据URI而不是"正常"http(s)://衍生物这一事实。

xhr.onload = function(evt) {
  if (this.status == 200) {
    var b64 = utf8_to_b64(this.responseText);
    var dataUri = 'data:image/png;base64,' + b64; // Assuming a PNG image
    myImgElement.src = dataUri;
  }
};
// From MDN:
// https://developer.mozilla.org/en-US/docs/Web/API/window.btoa
function utf8_to_b64( str ) {
    return window.btoa(unescape(encodeURIComponent( str )));
}

画布

还有另一个选项,即在<canvas>字段中绘制加载的数据。这样,用户将无法右键单击图像(画布所在的区域),而<img>和数据URI是用户在查看图像属性面板时看到的长数据URI。

谷歌驱动器上传器是使用angular js创建的。其作者也面临着类似的问题。图标托管在不同的域上,将它们作为img src=违反了CSP。所以,和你一样,他们必须使用XHR获取图标图像,然后以某种方式将它们放入img标签中。

他们描述了他们是如何解决的。在使用XHR获取图像后,他们将其写入HTML5本地文件系统。他们使用ng-src指令将其URL放在imgsrc属性中的本地文件系统上。

$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
  console.log('Fetched icon via XHR');
  blob.name = doc.iconFilename; // Add icon filename to blob.
  writeFile(blob); // Write is async, but that's ok.
  doc.icon = window.URL.createObjectURL(blob);
  ...
}

至于原因,我不知道。我认为创建用于检索图像的会话令牌是不可能的?我希望Cookie标头会被发送?这是跨来源请求吗?在这种情况下,是否设置withCredentials属性?这可能是P3P的事情吗?

另一种方法是在代理图像请求的站点后端添加一个端点。因此,您的页面可以在没有凭据的情况下请求它,后端将负责身份验证。如果图像没有频繁更改,或者您知道更新的频率,后端也可以缓存图像。这在后端很容易做到,使前端变得简单,并防止凭据发送到浏览器。

如果问题是身份验证,那么链接可能包含为用户生成的一次性令牌,该令牌已通过身份验证,并且只能从其当前浏览器会话访问。只为用户提供对内容的安全访问,并且只在他们被授权访问内容的时间内提供。然而,这也需要在后端进行工作。

在我看来,为了解决你的问题,你应该改变应用程序的设计,而不是试图破解浏览器的实际工作方式。

对安全URL的请求总是需要身份验证,无论是由浏览器使用img标记还是javascript完成。

如果您可以在没有用户交互的情况下自动执行授权,那么您可以在服务器端执行授权,并且不需要向客户端发送任何user+通行证。如果是这种情况,您可以更改https://myserver/dev30281_WebServices/api/user/picture/2218后面的代码来执行授权并提供图像,而无需HTTP身份验证,只有在用户有权请求的情况下,否则返回403禁止的响应(http://en.wikipedia.org/wiki/HTTP_403)。

另一个可能的解决方案是将包含安全图像的页面与应用程序的其他部分分开。所以理论上你会有两个单页应用程序。用户需要登录才能访问安全部件。但我不确定这在你的情况下是否可行,因为你没有说明所有的要求。但更有意义的是,如果你想为需要身份验证的安全资源提供服务,应该像浏览器一样提示用户输入凭据。

我总是解析

在上一次(或第一次登录请求)中设置Cookie标头值,然后在下一次请求中发送其值。

像这样的

第一次请求后的响应:

Date:Thu, 26 Dec 2013 16:20:53 GMT
Expires:-1
Pragma:no-cache
Set-Cookie:ASP.NET_SessionId=lb1nbxeyfhl5suii2hfchxpx; domain=.example.com; path=/; secure; HttpOnly
Vary:Accept-Encoding
X-Cdn:Served-By-Akamai
X-Powered-By:ASP.NET

任何下一个请求:

Accept:text/html,application/xhtml+xml
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8,ru;q=0.6
Cache-Control:no-cache
Connection:keep-alive
Cookie:ASP.NET_SessionId=lb1nbxeyfhl5suii2hfchxpx;

如您所见,我在Cookie标头中发送ASP.NET_SessionId="any value"值。如果服务器使用php,您应该解析PHPSSID="some-value"

您需要尝试使用Access-Control-Allow-Credentials: true标头。我曾经遇到过IE的一个问题,最终归结为使用了这个标题。同时在angular js代码中设置$httpProvider.defaults.headers.get = { 'withCredentials' : 'true' }

原因:我尝试了Chrome和Firefox,并且都记住了基本授权,前提是凭证是直接从浏览器UI输入的,即浏览器弹出的。如果凭据来自JavaScript,它将不会记住,尽管HTTP请求是相同的。我想这是故意的,但我看不出标准中提到它。