为什么浏览器在经过validation的XMLHttpRequest之后不重复使用授权标头?
我正在开发使用Angular的单页面应用程序。 后端公开了需要基本身份validation的REST服务。 获取index.html或任何脚本不需要身份validation。
我有一个奇怪的情况,其中我的一个视图有一个<img>
其中src
是需要身份validation的REST API的URL。 <img>
是由浏览器处理的,我没有机会为它所做的GET请求设置授权头。 这会导致浏览器提示input凭据。
我试图通过这样做来解决这个问题:
- 在源代码中将
img
src
留空 - 在“文档准备好”时,使用授权标头对服务(
/api/login
)进行XMLHttpRequest
,以使authentication发生。 - 在完成这个调用后,设置
img src
属性,认为到那时,浏览器将知道在随后的请求中包括授权标题…
…但它不。 对图像的请求不带标题。 如果我input凭据,那么页面上的所有其他图像是正确的。 (我也试过和Angular的ng-src
但是产生了相同的结果)
我有两个问题:
- 为什么浏览器(IE10)在成功的
XMLHttpRequest
之后在所有请求中包含头文件? - 我能做些什么来解决这个问题?
@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
然后Web浏览器提示input凭据。 如果我input他们,我得到这个回应:
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加载图像并将其显示在网站上。 优点是authentication凭证绝不会进入HTML。 他们会抵制在JavaScript方面。
第1步:通过JS加载图像数据
这是基本的AJAXfunction(另请参阅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)://
derivates。
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>
字段中。 这样,用户将无法右键单击图像(canvas所在的区域),而不是在查看图像属性面板时用户将看到长数据URI的<img>
和数据URI 。
谷歌驱动器上传是使用angularjs创build的。 其作者面临类似的问题。 图标托pipe在不同的域上,并将它们作为img src=
违反了CSP。 所以,像你一样,他们必须使用XHR获取图标图像,然后以某种方式设法让他们进入img
标签。
他们描述了他们如何解决它 。 使用XHR获取图像后,将其写入HTML5本地文件系统。 他们使用ng-src
指令将它的URL放在本地文件系统的img
的src
属性中。
$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); ... }
至于为什么,我不知道。 我认为创build会话令牌来检索图像是不可能的? 我希望Cookie标题确实发送? 这是一个跨源的请求? 在这种情况下,你是否设置了withCredentials属性? 也许它是一个P3P的东西?
另一种方法是添加一个结束点到您的网站后端代理图片请求。 所以你的页面可以在没有证书的情况下请求它,而后端将负责authentication。 后端也可以caching图像,如果它没有频繁变化,或者你知道更新的频率。 这在后端很容易做到,使前端简单,并防止凭据发送到浏览器。
如果问题是身份validation,那么链接可能包含为用户生成的单个使用令牌,该令牌已通过身份validation,并且只能从其当前浏览器会话访问。 仅为用户使用的内容授予安全访问权限,并且仅限授权用户访问内容。 但是,这也需要在后端工作。
在我看来,要解决你的问题,你应该改变你的应用程序的devise,而不是试图绕开浏览器实际工作的方式。
对安全URL的请求将始终需要身份validation,关于是由浏览器使用img标记还是使用javascript。
如果您可以在没有用户交互的情况下自动执行授权,则可以在服务器端执行授权,并且无需向客户端发送任何用户+密码即可完成此操作。 如果是这种情况,只有当用户被授权请求时,才可以更改https://myserver/dev30281_WebServices/api/user/picture/2218
后面的代码来执行授权并提供映像,而不进行HTTP身份validation。否则返回403禁止响应( http://en.wikipedia.org/wiki/HTTP_403 )。
另一个可能的解决scheme是将包含应用程序其余部分的安全图像的页面分开。 所以你理论上有两个单页面应用程序。 用户将被要求login访问安全部分。 我不确定,如果在你的情况下这是可能的,因为你没有说明所有的要求。 但更有意义的是,如果您想要提供需要身份validation的安全资源,则应该提示用户input凭据,就像浏览器一样。
我总是parsing
在之前(或第一个login请求)中设置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,你应该parsingPHPSESSID =“一些值”
您需要尝试使用Access-Control-Allow-Credentials: true
标头。 我曾经遇到过一个IE问题,最终归结为这个头文件的使用。 还要在angular度js代码中设置$httpProvider.defaults.headers.get = { 'withCredentials' : 'true' }
。