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

42

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

我遇到了一个奇怪的情况,在我的一个视图中有一张<img>,其中src是需要认证的REST API的URL。浏览器处理<img>,我无法为它发出的GET请求设置授权头,这导致浏览器提示要求输入凭据。

我尝试通过以下方式修复此问题:

  1. 在源代码中将img src留空
  2. 在“文档就绪”时,使用授权头发出XMLHttpRequest请求到一个服务(/api/login),只是为了进行身份验证。
  3. 完成该调用后,设置img src属性,认为此时浏览器会知道在后续请求中包括授权头...

...但它没有。图像的请求被发送而不带头信息。如果我输入凭据,那么页面上的所有其他图像都正确。

我有两个问题:

  1. 为什么浏览器(IE10)不在成功的XMLHttpRequest之后的所有请求中包括头信息?
  2. 我该怎么解决这个问题?

@bergi要求提供请求的详细信息。下面是它们。

请求/api/login

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

请求 /user/picture/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

接着,网络浏览器会提示输入凭据。如果我输入了凭据,就会得到如下响应:

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

1
如果您正在使用HTTP身份验证,那么您是否可以操纵src URL以包括用户名/密码,例如http://user:pass@server/path/to/img.png - quietmint
@user113215:我不想把密码放在源代码中。 - Sylvain
1
你不想把密码放在源代码中。在浏览器中,你把密码存储在哪里(以便包含在XMLHttpRequest中),并且保持“隐藏不可见”? - Khanh TO
1
在HTML源代码中的@Sylvain: <img src="https://user:pass@server/1x1.gif"> - Paul Scheltema
1
+1 @PaulScheltema。作为一种改进,无需在HTML源代码中包含<img src="user:pass@server/1x1.gif">。可以通过脚本动态添加,然后在加载完成后立即删除,甚至更好的是使用visibility: hidden或等效方式。 - matpop
显示剩余24条评论
7个回答

24

基本思路

通过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时,通常会将URI分配给图像元素的src属性。我们可以应用相同的原则,在这里使用数据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。


我尝试在Chrome中测试它,但没有图像出现。我从utf8_to_b64()函数中得到了一个结果,但将其分配给图像并不起作用(而且它是PNG格式的)。 - Sylvain
@PaulScheltema建议采用这种方法:http://jsperf.com/encoding-xhr-image-data/14。我在Chrome中使其工作,但在IE10中无法支持`XMLHttpRequest`的`overrideMimeType`。虽然IE11支持它,但我没有在IE11上尝试过,因为我需要它能够在IE8及以上版本中运行。 - Sylvain
此外,这种方法在IE8上不起作用,window.bota仅支持IE11。 - Sylvain
@Sylvain 请看一下这个问题,特别是它的被接受的答案:https://dev59.com/mnNA5IYBdhLWcg3wF5uO。在服务器端对图像进行base64编码会更容易。在你的环境中是否可能实现? - ComFreek

9
谷歌驱动程序上传工具是使用AngularJS创建的。它的作者们遇到了类似的问题。图标托管在不同的域上,将它们作为放置会违反CSP。所以,像你一样,他们必须使用XHR获取图标图像,然后以某种方式将它们放入标签中。
他们描述了他们如何解决这个问题。在使用XHR获取图像后,他们将其写入HTML5本地文件系统。他们使用ng-src指令将其URL放在本地文件系统中的的属性中。
$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的问题?

HTML5,这在IE上行不通。我必须支持IE8及以上版本。 - Sylvain
抱歉,对于Blob URL,需要使用IE 10及以上版本。 - flup

4
另一种方法是在站点后端添加一个终点来代理图像请求。因此,您的页面可以请求不带凭据的图像,并且后端会处理身份验证。如果图像不经常更改或您知道更新频率,则后端还可以缓存图像。这在后端上相当容易实现,使前端变得简单,并防止凭据被发送到浏览器中。
如果问题在于身份验证,则链接可能包含为已认证用户生成的单次使用令牌,并且仅可从他们当前的浏览器会话访问。为用户提供安全访问所需内容,并且仅限于授权访问的时间。但这也需要在后端进行工作。

我无法这样做,因为每个终端用户的凭据都不同,我需要授权每个请求。一个用户可能没有权限查看另一个用户的图片。 - Sylvain
如果您在代理上创建一个会话,那么cookie头会被发送吗? - flup
你在后端要做的是构建一个HTTP请求,以允许检索所需的图像文件。由于您完全控制该请求,因此可以在其中添加cookie。 - SzabV

2

我总是解析

在上一个(或第一次登录请求)中设置Set-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="任意值"的值。如果服务器使用php,则应解析PHPSESSID="某个值"


嗨,我不想在服务器上使用会话。 - Sylvain

2
在我看来,要解决你的问题,你应该改变你的应用程序设计,而不是试图绕过浏览器实际工作方式进行黑客攻击。
对于安全 URL 的请求,无论是由浏览器使用 img 标签还是在 JavaScript 中完成,都需要进行身份验证。
如果您可以自动执行授权而无需用户交互,则可以在服务器端执行它,而无需向客户端发送任何用户+密码。如果是这种情况,您可以更改 https://myserver/dev30281_WebServices/api/user/picture/2218 后面的代码以执行授权并提供图像,而无需进行 HTTP 身份验证,仅当用户被授权请求时才返回 403 禁止响应(http://en.wikipedia.org/wiki/HTTP_403)。
另一个可能的解决方案是将包含安全图片的页面与应用程序的其余部分分开。因此,理论上您将拥有两个单页面应用程序。用户需要登录才能访问安全部分。但是,如果您没有说明所有要求,则不确定是否可以在您的情况下实现此操作。但是,如果您想提供需要身份验证的安全资源,则更合理的做法是像浏览器一样提示用户输入凭据。

你如何“自动”授权请求? - flup
“自动化”指的是您拥有服务器端代码,可以处理来自身份验证的存储用户信息的授权。它不需要任何用户交互即可完成。 - Jonas

2

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


0
关于原因:我尝试过Chrome和Firefox,只有在直接从浏览器UI中输入凭据时,它们才会记住基本授权。如果凭证来自JavaScript,即使HTTP请求相同,它也不会记住它。我猜这是有意设计的,但我没有在标准中看到它被提到。

1
通常情况下这不是真的,我已经使用当前版本的Firefox(65)和Chromium(72)进行了检查,即使最初使用XMLHttpRequest(通过Javascript发送),两者都会记住域的凭据。 - Ján Lalinský

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接