Compartilhando dados dos cookies entre domínios

Publicado por Geison Biazus no dia dev

Cookie Share

Nós que trabalhamos com marketing digital, precisamos estar sempre próximos dos sites dos nossos clientes. Muitas vezes temos que detectar informações como origem do visitante, quais páginas e até mesmo quem acessa esses sites. Uma das formas de armazenarmos esses dados enquanto o visitante está na página é fazendo o uso de cookies do navegador. Porém quando esse visitante sai de um domínio e vai para outro, o navegador acaba gerando outro cookie e perdemos o rastreamento desse visitante.

Infelizmente, por questões de segurança, não é possível compartilhar cookies entre domínios. Porém existe um hack para conseguir compartilhar essas informações. Podemos fazer isso usando um servidor para tal propósito.

A ideia é que ao acessar a página, uma requisição AJAX seja feita para um servidor. Este, por sua vez, criará um cookie em seu próprio domínio. Dessa forma, em um novo acesso, a requisição será refeita enviando esse cookie vinculado ao domínio do servidor onde ele pode devolver esses valores na resposta da requisição.

Cookie Share

Configurando domínios para simulação

Para que seja possível testar nossa implementação vamos montar um ambiente para simular o acesso de domínios diferentes. Essa simulação foi feita em um ambiente Linux e deve funcionar em um sistema operacional OS X normalmente. No Windows você pode fazer isso seguindo as dicas do post: Hosts File in Windows 10 : Locate, Edit and Manage

Para isso, edite o arquivo “/etc/hosts”. Isso pode ser feito com o comando:

1
$ sudo vi /etc/hosts

Adicione as seguintes linhas no arquivo:

1
2
3
127.0.0.1 firstdomain.dev
127.0.0.1 seconddomain.dev
127.0.0.1 server.dev

Dessa forma você pode acessar os servidores locais utilizando os hosts configurados.

Criando o servidor para gerar o cookie

Inicialmente implementaremos o servidor responsável por receber as requisições e gerar o cookie compartilhado “_server.cookie”. Criaremos o server na linguagem “Ruby” utilizando o framework “Sinatra”. Inicialmente crie um diretório para armazenar o código do servidor:

1
$ mkdir server

Dentro desse diretório crie um arquivo chamado “Gemfile” e adicione as dependências:

1
2
3
4
source 'http://rubygems.org'

gem 'sinatra'
gem "sinatra-cross_origin"

Utilize o seguinte comando no diretório do server para instalar essas dependências:

1
$ bundle install

Crie um arquivo chamado “app.rb” no mesmo diretório. É nesse arquivo que implementaremos a lógica do server. Adicione as seguintes linhas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'sinatra'
require 'sinatra/cross_origin'
require 'securerandom'
require 'json'

configure do
  enable :cross_origin
end

COOKIE_NAME = '_server.cookie'

post '/cookie' do
  response.set_cookie(COOKIE_NAME, cookie_data)
  cookie_data
end

def cookie_data
  @cookie ||= request.cookies[COOKIE_NAME] || create_cookie
end

def create_cookie
  JSON.dump({ id: SecureRandom.uuid, creation_time: Time.now.to_s })
end

O processo funciona da seguinte maneira: O server recebe uma requisição “POST” no caminho “/cookie”. Ele então cria um cookie chamado “_server.cookie” com o retorno do método “cookie_data”. Esse método, por sua vez, retorna o valor de um cookie com o mesmo nome ou gera novos valores caso ele não exista. Ou seja, se já existir um cookie chamado “_server.cookie” na requisição, o valor desse cookie é reatribuido para o mesmo cookie. Caso não exista, é gerado um novo valor para ele. Por fim a requisição retorna o próprio valor do cookie para que ele possa ser lido no JavaScript que a invocou.

Utilize o seguinte comando no diretório do server para iniciar:

1
$ ruby app.rb

Implementação do HTML e JavaScript

Implementaremos agora o JavaScript que fará a requisição AJAX para o server. Ele também tratará o retorno criando um cookie local. Inicialmente, crie outro diretório para armazenar o HTML e o JavaScript:

1
$ mkdir client

Nesse diretório crie o arquivo “index.html” e adicione o seguinte código HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <div id="content">
      <h3>Cookie Value</h3>
      <p><strong>ID: </strong><span id="cookie-id"></span></p>
      <p><strong>Creation time: </strong><span id="cookie-creation"></span></p>
    </div>
    <script src="cookie.js" charset="utf-8"></script>
  </body>
</html>

Esse documento HTML contém uma “div” onde serão exibidos os valores do cookie, além da inclusão do JavaScript que fará o tratamento dele.

Agora crie um arquivo chamado “cookie.js” com a seguinte estrutura:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var CookieHandler = (function() {

  var url = 'http://server.dev:4567/cookie';
  var cookieName = '_client.cookie';

  var init = function() {
    makeRequest();
  };

  return {
    init : init
  };

})();

window.addEventListener("load", CookieHandler.init);

Essa é a estrutura básica do nosso JavaScript. Nele utilizamos o pattern Revealing Module como estrutura do nosso script. Você pode aprender mais sobre organização de código JavaScript no post: Proteja seus métodos organizando seu código JavaScript.

Definimos duas variáveis que armazenam a URL do servidor que o POST será feito e o nome do cookie que será criado. Além disso temos tabém uma função chamada “init” que é vinculada ao evento “load” do window, ou seja, essa função será executada após o carregamento da página.

Abaixo da função “init”, insira a função “makeRequest” que realizará o request para o servidor:

1
2
3
4
5
6
7
8
  var makeRequest = function() {
    httpRequest = new XMLHttpRequest();
    httpRequest.onreadystatechange = onRequestComplete;
    httpRequest.open('POST', url, true);
    httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
    httpRequest.withCredentials = true;
    httpRequest.send();
  };

Essa função é chamada dentro da função “init”. Neste exemplo utilizei um objeto do tipo “XMLHttpRequest” para realizar a requisição. Esse objeto é nativo do browser, porém esse processo funciona normalmente realizando o POST via jQuery ou outro framework.

A função “makeRequest” instancia um “XMLHttpRequest” e atribui uma função ao atributo “onreadystatechange”. Essa função será executada a cada mudança de estado da requisição. Depois é identificado que a requisição será um POST para a URL que armazenamos anteriormente. O terceiro parâmetro indica que a requisição será assíncrona. Em seguida é configurado o cabeçalho da requisição e depois é atribuido o valor “true” para o atributo “withCredentials”. Esse atributo é muito importante, pois sem ele os cookies não são enviados na requisição AJAX. Como necessitamos deles, esse atributo é obrigatório. Por fim a requisição é realizada na função “send”.

Implemente então a função “onRequestComplete”. Adicione as seguintes linhas:

1
2
3
4
5
6
7
8
9
  var onRequestComplete = function() {
    var COMPLETE = 4
    var OK = 200;

    if (this.readyState === COMPLETE && this.status == OK) {
      createCookie(this.response);
      showCookieValues();
    }
  };

Essa função é executada no final de cada mudança de estado da requisição. Como nesse momento só nos importa quando o request está completo, é necessário verificar se o atributo “readyState” é igual a 4. Quando a requisição estiver completa, pegamos a resposta e criamos um cookie no domínio da página. Depois lemos esse cookie recém criado e atualizamos as informações do HTML.

Insira no arquivo a implementação das funções “createCookie” e “showCookieValues”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  var createCookie = function(data) {
    document.cookie = "_client.cookie=" + data;
  };

  var showCookieValues = function() {
    var cookie = getCookie();
    document.getElementById('cookie-id').innerHTML = cookie.id;
    document.getElementById('cookie-creation').innerHTML = cookie.creation_time;
  };

  var getCookie = function() {
    var re = new RegExp(cookieName + "=([^;]+)");
    var value = re.exec(document.cookie);
    return (value != null) ? JSON.parse(unescape(value[1])) : null;
  }

A função “create_cookie” apenas cria um novo cookie com o retorno do request, que por sua vez é o valor do cookie “_server.cookie” serializado em JSON. Já a função “showCookieValues” lê esse coookie recém criado e atruibui os valores no HTML.

Testando

Vá para o diretório “server” e inicie o servidor:

1
$ ruby app.rb

Acesse agora o diretório “client” e inicie um server local nesse diretório. Isso pode ser feito com o comando:

1
$ ruby -run -e httpd . -p 8000

Abra um navegador e acesse o endereço do primeiro domínio configurado anteriormente, “http://firstdomain.dev:8000”. Voce deverá ver o seguinte retorno:

Acesso a partir do primeiro domínio

Abra uma nova aba e acesse o endereço do segundo domínio, “http://seconddomain.dev:8000”. Você deverá ver os mesmos valores do acesso com o domínio anterior:

Acesso a partir do primeiro domínio

Se os valores exibidos na tela forem os mesmos nos dois acessos, é sinal que tudo funcionou corretamente.

Mas o que aconteceu aí?

WTF

Note que durante a implementação criamos dois cookies: um chamado “_server.cookie” e outro “_client.cookie”.

O primeiro, “_server.cookie”, é gerado pelo servidor na resposta da requisição e vinculado ao domínio do servidor. Quando o server recebe a requisição e atribui um cookie, esse é devolvido para o navegador em um response header chamado “Set-Cookie”. Quem controla a criação/envio de cookies é o próprio navegador. Por isso ao completar a requisição ele cria esse cookie vinculado ao domínio so servidor (“server.dev”).

Em uma próxima requisição o navegador envia para o servidor todos os cookies atrelados ao domínio “server.dev” no request header “Cookie”, onde o servidor pega o valor e apenas devolve novamente no response header “Set-Cookie”. Independente de qual domínio a requisição é realizada, desde que seja feita para um mesmo endereço de servidor, os cookies serão compartilhados.

No entanto, os navegadores bloqueiam o acesso de cookies de domínios diferentes da página atual através do JavaScript. É por isso que criamos o segundo cookie “_client.cookie”. Ele é criado com o valor do cookie que está vinculado ao domínio “server.dev”, que é lido no servidor e retornado no corpo da requisição. Com esse valor em mãos podemos criar um novo cookie no domínio da página (“firstdomain.dev”) e usar esse valor como quisermos.

Cool Story

Conclusão

Fazendo uso dessa alternativa, podemos recuperar várias informações. Podemos trocar informações como horário do primeiro acesso, origem e quantidade de acessos, antes mesmo do visitante realizar um cadastro. Também é possível ter uma real conexão entre todos os sites que seu script estiver incluído e coletar estatísticas mais precisas e confiáveis. As únicas limitações são que os dados não podem ser compartilhados entre navegadores e, caso os usuários limpem os cookies do navegador, o processo é iniciado novamente gerando um novo rastreamento.

O código fonte do servidor e JavaScript mostrados se encontra no GitHub.

Geison Biazus

Geison Biazus

Full Stack Developer

Comentários