quinta-feira, 8 de abril de 2010

Dupla submissão - double submit JSF - Java Server Faces

Um problema comum em web em sites de compra:
Usuário escolhe um produto e finaliza seu pedido.
Servidor processa a requisição e redireciona uma página para o usuário dizendo que a ação está concluída.
Usuário vê a página de conclusão do seu pedido e recebe um email da compra.
Como as requisição pode demorar a chegar, nosso querido usário aperta F5/botão de reload/Crtl+F5... ignorando as seguintes mensagens do navegador.

Do Internet Explorer
drf_ie7
Do Firefox
drf_firefox
Do Chrome
drf_chrome

Claro que o usuário não vai ler isso… e aí que acontece seu problemas
Nesse momento seu chefe chega para vc e diz... "Houston, we have a problem..." , chegaram dois débitos no cartão de crédito do "usuário F5"....
Não se desespere... we have a solution :) from Hell
.
A solução que emprego é o uso de "Tokens" gerado por MD5, na geração do chave, incluo
o id da sessão do usuário e a data atual com precissão de milissegundos.
Segue exemplo(método da classe TokenProcessor):

public String generateToken(HttpServletRequest request) {
HttpSession session = request.getSession();
try {
byte id[] = session.getId().getBytes();
byte now[] = new Long(System.currentTimeMillis()).toString().getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(id);
md.update(now);
return this.toHex(md.digest());
} catch (IllegalStateException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
}
}
O método abaixo é usado para converter de hex para string:


public String toHex(byte buffer[]) {
StringBuffer sb = new StringBuffer();
String s = null;
for (int i = 0; i < buffer.length; i++) {
s = Integer.toHexString((int) buffer[i] & 0xff);
if (s.length() < 2) {
sb.append('0');
}
sb.append(s);
}
return sb.toString();
}

Antes da chamada da tela(xhtml), crio um token que é duplicado, um vai para a sessão, outro vai para um input hidden qualquer, no meu caso criei get/set token no meu bean:


TokenProcessor tp = TokenProcessor.getInstance();
tp.saveToken(request, this);
// o this é o objeto que tem o get/set token no, ControleCompraBean.java

Método saveToken:

public synchronized void saveToken(HttpServletRequest request, Object objectToken) {
HttpSession session = request.getSession();
String tokenValue = generateToken(request);
if (tokenValue != null) {
session.setAttribute("TRANSACTION_TOKEN_KEY", tokenValue);
try {
// atribui um novo token ao bean
Method tokenMethod = objectToken.getClass().getMethod("setToken", String.class);
tokenMethod.invoke(objectToken, tokenValue);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Exemplo de funcionamento

Na sessão “TRANSACTION_TOKEN_KEY” temos o token gerado de exemplo: 84ea7546089654f71008b828daf04dea

Na página renderizada também temos o mesmo token: 84ea7546089654f71008b828daf04dea

   1:  <h:inputHidden id="token" value="#{controleCompraBean.token}" /> 

Em sua primeira requisição o usuário submete seu pedido, você confere se a requisição é válida, exemplo:

Se “84ea7546089654f71008b828daf04dea” do TRANSACTION_TOKEN_KEY é igual a
“84ea7546089654f71008b828daf04dea” do campo hidden da tela.

TokenProcessor tp = TokenProcessor.getInstance();

// this tem o input hidden do xhtml, o get/set token.
if(!tp.isTokenValid(request, this)){
// Se o token não é válido, isto é não são iguais, enviar msg de erro
ControleUtils.addInfoMessage(ConstantesUtils.MENSAGEM_VALIDACAO_DUPLA_SUBMISSAO );
showErrorMessage = Boolean.TRUE;
return null;
}

No método do isTokenValid, ele verifica se o token é válido e retira-o da sessão, se o usuário mandar os mesmo dados novamente(F5), o token não será válido, pois sua requisição de comprar já foi efetuada, nesse caso você poderia exibir os dados da compra efetuada.

public synchronized boolean isTokenValid(HttpServletRequest request, Object objectToken) {
HttpSession session = request.getSession();
if (session == null) {
return false;
}
// Recuperar o token da sessão
String saved = (String) session.getAttribute("TRANSACTION_TOKEN_KEY");
if (saved == null) {
return false;
}
this.resetToken(request);
String token = null;
try {
// recuperar o atributo token do bean
Method tokenMethod = objectToken.getClass().getMethod("getToken");
token = (String)tokenMethod.invoke(objectToken, new Object[0]);
} catch (Exception e) {
e.printStackTrace();
}

if (token == null) {
return false;
}
return saved.equals(token);
}

public synchronized void resetToken(HttpServletRequest request) {
HttpSession session = request.getSession();
if (session == null) {
return;
}
session.removeAttribute("TRANSACTION_TOKEN_KEY");
}

Neste artigo, não inventei a roda, o antigo strtus 1.x, já tinha uma classe de controle de requisições, aproveitei alguns métodos da mesma.

O nome da classe original é org.apache.struts.util.TokenProcessor.
Claro que você pode implementar um listener diretamente no form, não utilizando o set/get token do bean ;)

2 comentários:

  1. Em primeiro lugar, parabéns pelo blog! Muito bacana tua abordagem bem humorada. Este post sobre o Token ficou ótimo! Já usei o Token várias vezes, a maioria delas enfrentando a resistência dos meus colegas que queriam apenas "desabilitar o botão após o clique..."

    Em segundo, obrigado pelos comentários no meu blog! Fique à vontade para enviar o link da e-T.I.queta para os palestrantes enroladores que você citou. Pessoas como eu e você já estamos cansados de tanta baboseira - e é nossa responsabilidade arrumar a casa. Conto com você!

    Grande abraço,

    Marcone

    ResponderExcluir
  2. oi vc poderia seguir meu blog, é sobre filmes de horror da uma olhadinha lá, falou.

    ResponderExcluir