web-dev-qa-db-fra.com

Pourquoi le navigateur ne réutilise-t-il pas les en-têtes d'autorisation après un XMLHttpRequest authentifié?

Je développe Single Page App en utilisant Angular. Le backend expose les services REST nécessitant une authentification de base. Obtenir index.html ou l'un des scripts ne nécessite pas d'authentification. 

J'ai une situation étrange où l'un de mes points de vue a un <img>src est l'URL d'une API REST qui nécessite une authentification. Le <img> est traité par le navigateur et je n’ai aucune chance de définir l’en-tête d’autorisation pour la requête GET qu’il émet. Cela provoque le navigateur à demander des informations d'identification.

J'ai essayé de résoudre ce problème en faisant ceci:

  1. Laissez imgsrc vide dans le source 
  2. Dans "document prêt", définissez une XMLHttpRequest sur un service (/api/login) avec l'en-tête Authorization afin que l'authentification se produise.
  3. À la fin de cet appel, définissez l'attribut img src, en pensant que le navigateur saurait alors inclure l'en-tête Authorization dans les requêtes suivantes ...

... mais ça ne marche pas. La demande pour l'image s'éteint sans les en-têtes. Si j'entre les informations d'identification, toutes les autres images de la page sont exactes . (J'ai également essayé et le code ng-src d'Angular, mais cela a donné le même résultat)

J'ai deux questions:

  1. Pourquoi le navigateur (IE10) n’a-t-il pas inclus les en-têtes dans toutes les demandes après une XMLHttpRequest réussie? 
  2. Que puis-je faire pour contourner ce problème?

@bergi a demandé des détails sur les demandes. Les voici.

Demande à/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

Réponse (/ 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

Demande à/utilisateur/image/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

Et puis le navigateur Web demande des informations d'identification. Si je les saisis, j'obtiens cette réponse:

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
39
Sylvain

Idée basique

Chargez les images via JavaScript et affichez-les sur le site. L'avantage est que les informations d'authentification ne parviendront jamais dans le code HTML. Ils vont résister du côté JavaScript.

Étape 1: charger les données d'image via JS

C'est la fonctionnalité de base AJAX (voir aussi 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) {
    // ...
  }
};

Étape 2: formater les données

Maintenant, comment pouvons-nous afficher les données d'image? Lors de l'utilisation de HTML, on attribue normalement un URI à l'attribut src de l'élément image. Nous pouvons appliquer le même principe ici, à l'exception du fait que nous utilisons des URI de données au lieu de dérivés 'normaux' 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 )));
}

Toile

Il existe également une autre option qui consiste à peindre les données chargées dans un champ <canvas>. De cette manière, l'utilisateur ne pourra pas cliquer avec le bouton droit de la souris sur l'image (la zone dans laquelle le canevas est positionné), par opposition à l'URI <img> et aux données où l'utilisateur verra un long URI lorsqu'il affichera le panneau de propriétés de l'image.

24
ComFreek

Le programme de téléchargement de Google Drive est créé à l'aide de js angulaire. Ses auteurs étaient confrontés à un problème similaire. Les icônes étaient hébergées sur un domaine différent et les placer comme img src= constituait une violation du CSP. Ainsi, comme vous, ils ont dû récupérer les images d'icônes à l'aide de XHR, puis réussir à les insérer dans les balises img

Ils décrivent comment ils l’ont résolu . Après avoir récupéré l'image à l'aide de XHR, ils l'écrivent dans le système de fichiers local HTML5. Ils placent son URL sur le système de fichiers local dans l'attribut img de src à l'aide de la directive ng-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);
  ...
}

Quant au pourquoi, je ne sais pas. Je suppose que la création d'un jeton de session pour récupérer les images est hors de question? Je m'attendrais à ce que les en-têtes de cookie soient envoyés? Est-ce une demande d'origine croisée? Dans ce cas, définissez-vous la propriété withCredentials ? Est-ce une chose P3P peut-être?

8
flup

Une autre approche consisterait à ajouter un point final à l’arrière-plan de votre site qui a proxy la demande d’image. Ainsi, votre page pourrait le demander sans informations d'identification et le back-end se chargerait de l'authentification. Le système dorsal peut également mettre en cache l’image si elle ne change pas souvent ou si vous connaissez la fréquence à laquelle elle est mise à jour. Ceci est assez facile à faire sur le back-end, simplifie votre front-end et évite l'envoi d'informations d'identification au navigateur. 

Si le problème est l'authentification, les liens peuvent contenir un jeton à usage unique généré pour l'utilisateur authentifié et accessible uniquement à partir de la session de navigateur en cours. Donner un accès sécurisé au contenu uniquement à l'utilisateur auquel il était destiné et uniquement au moment où il est autorisé à y accéder. Cela nécessiterait également un travail dans le back-end, cependant. 

4
SzabV

Vous devez essayer d'utiliser l'en-tête Access-Control-Allow-Credentials: true. Une fois, j’ai rencontré un problème avec IE qui a finalement abouti à l’utilisation de cet en-tête. Définissez également $httpProvider.defaults.headers.get = { 'withCredentials' : 'true' } dans le code js angulaire.

2
Aziz Shaikh

Je toujours analyser

Set-Cookie en-tête dans la précédente (ou la première demande de connexion), puis envoie sa valeur dans les prochaines requêtes.

Quelque chose comme ça

Réponse après la première demande:

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

Toute demande suivante:

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;

Comme vous pouvez le voir, j'envoie ASP.NET_SessionId = "toute valeur" dans Cookie header. Si le serveur utilise php, vous devez analyser PHPSESSID = "une valeur"

2
Sergey Pekar

Il me semble que pour résoudre votre problème, vous devez modifier la conception de votre application, au lieu d'essayer de modifier le fonctionnement des navigateurs.

Une requête vers une URL sécurisée nécessitera toujours une authentification, que le navigateur l’envoie avec une balise img ou en javascript.

Si vous pouvez effectuer l'autorisation automatiquement sans interaction de l'utilisateur, vous pouvez le faire côté serveur et vous n'avez pas besoin d'envoyer d'utilisateur + passe au client pour le faire. Si tel est le cas, vous pouvez modifier le code derrière https://myserver/dev30281_WebServices/api/user/picture/2218 pour exécuter l'autorisation et diffuser l'image, sans autorisation HTTP, uniquement si l'utilisateur est autorisé à la demander. Sinon, renvoyez une réponse 403 interdite ( http: // en .wikipedia.org/wiki/HTTP_403 ).

Une autre solution possible serait de séparer les pages contenant les images sécurisées du reste de l'application. Donc, théoriquement, vous auriez deux applications d'une seule page. L'utilisateur devrait se connecter pour accéder à la partie sécurisée. Je ne sais pas si cela est possible dans votre cas, puisque vous n'avez pas indiqué toutes les exigences. Mais il est plus logique que si vous souhaitez desservir des ressources sécurisées nécessitant une authentification, vous devez demander à l'utilisateur des informations d'identification, exactement comme le navigateur.

2
Jonas

Pour ce qui est de la raison: j’ai essayé Chrome et Firefox, et je me souviens de l’autorisation de base seulement si les informations d’identité sont saisies directement à partir de l’UI du navigateur , c’est-à-dire la fenêtre contextuelle créée par le navigateur. Il ne s'en souviendra pas si les informations d'identification proviennent de JavaScript, bien que la demande HTTP soit la même. Je suppose que cela est voulu par la conception, mais je ne le vois pas mentionné dans la norme.

0
Franklin Yu