I'm developing Single Page App using Angular. The backend exposes REST services that require Basic authentication. Getting index.html or any of the scripts does not require authentication.
I have an odd situation where one of my view has a <img>
where the src
is the url of a REST API that requires authentication. The <img>
is processed by the browser and I have no chance to set the authorization header for GET request it makes. That causes the browser to prompt for credentials.
I attempted to fix this by doing this:
- Leave
img
src
empty in the source - At "document ready", make an
XMLHttpRequest
to a service (/api/login
) with the Authorization header, just to cause the authentication to occur. - Upon completing that call, set the
img src
attribute, thinking that by then, the browser would know to include the Authorization header in subsequent requests...
...but it doesn't. The request for the image goes out without the headers. If I enter the credentials, then all other images on the page are right.
(I've also tried and Angular's ng-src
but that produced the same result)
I have two questions:
- Why didn't the browser (IE10) include the headers in all requests after a successful
XMLHttpRequest
? - What can I do to work around this problem?
@bergi asked for requests' details. Here they are.
Request to /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
Response (/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
Request to /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
And then the web browser prompts for credentials. If I enter them, I get this response:
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
Basic idea
Load the images via JavaScript and display them on the site. The advantage is that the authentication credentials will never find their way into the HTML. They will resist at the JavaScript side.
Step 1: load the image data via JS
That's basic AJAX functionality (see also 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) {
// ...
}
};
Step 2: format the data
Now, how can we display the image data? When using HTML, one would normally assign an URI to the src
attribute of the image element. We can apply the same principle here except for the fact that we use data URIs instead of 'normal' 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
There is also another option which consists in painting the loaded data in a <canvas>
field. This way, the user won't be able to right-click the image (the area where the canvas is positioned) as opposed to the <img>
and data URIs where the user will see a long data URI when viewing the image properties panel.
The google drive uploader is created using angular js. Its authors faced a similar problem. The icons were hosted on a different domain and putting them as img src=
violated the CSP. So, like you, they had to fetch the icon images using XHR and then somehow manage to get them into the img
tags.
They describe how they solved it. After fetching the image using XHR, they write it to the HTML5 local file system. They put its URL on the local file system in the img
's src
attribute using the ng-src
directive.
$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);
...
}
As for the why, I don't know. I assume that creating a session token for retrieving the images is out of the question? I'd expect that Cookie headers do get sent? Is it a cross-origin request? In that case, do you set the withCredentials property? Is it a P3P thing perhaps?
Another approach would be to add an end point to your sites back end that proxied the image request. So your page could request it without credentials and the back end would take care of the authentication. The back end could also cache the image if it didn't change frequently or you knew the frequency with which it was updated. This is fairly easy to do on the back end, makes your front end simple and prevents credentials being sent to the browser.
If the issue is authentication then the links could contain a single use token generated for the user that is authenticated and only accessible from their current browser session. Giving secure access to the content only for the user it was intended for and only for the time they are authorized to access it. This would also require work in the back end, however.
It seems to me that to solve your problem you should change the design of your app, instead of trying to hack your way around how browsers actually work.
A request to a secure URL will always need authentication, regarding of it being done by the browser with an img tag or in javascript.
If you can perform authorization automatically without user interaction, you can do it on the server side and you don't need to send any user+pass to the client to do this. If that is the case, you could change the code behind https://myserver/dev30281_WebServices/api/user/picture/2218
to perform the authorization and serve the image, without HTTP auth, only if the user is authorized to request it, otherwise return a 403 forbidden response (http://en.wikipedia.org/wiki/HTTP_403).
Another possible solution would be separate the pages that include the secure images from the rest of the app. So you would theoretically have two single-page-apps. The user would be required to login to access the secure part. I'm not sure though if this is possible in your case, since you didn't state all requirements. But it makes more sense that if you want to serve secure resources that require authentication, that the user should be prompted for credentials, just as the browser does.
I always parse
Set-Cookie header value in previous (or first login request) and then send it's value in next requests.
Something like this
Response after first request:
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
Any next request:
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;
As you can see I send ASP.NET_SessionId="any value" value in Cookie header. If server uses php you should parse PHPSESSID="some value"
You need to try using the Access-Control-Allow-Credentials: true
header. I once encountered an issue with IE which eventually boiled down to the use of this header. Also set $httpProvider.defaults.headers.get = { 'withCredentials' : 'true' }
in the angular js code.
As for the reason: I tried Chrome and Firefox, and both remember basic authorization only if the credential is entered directly from Browser UI, i.e. the pop-up made by browser. It will not remember it if the credential came from JavaScript, although the HTTP request is the same. I guess this is by design, but I don't see it mentioned in standard.
来源:https://stackoverflow.com/questions/20617720/why-doesnt-the-browser-reuse-the-authorization-headers-after-an-authenticated-x