Use a serialização em Java com segurança!

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (8)  (0)

Veja neste artigo a serialização em Java, mostrando os principais conceitos, além de abordar como solucionar uma falha crítica de segurança.

A serialização em Java é o processo no qual a instância de um objeto é transformada em uma sequência de bytes e é útil quando precisamos enviar objetos pela rede, salvar no disco, ou comunicar de uma JVM com outra. Isso porque o estado atual do objeto é “congelado” e na outra “ponta” nós podemos “descongelar” este objeto sem perder nenhuma informação.

Para habilitar a serialização de terminada classe você deve explicitamente implementar a interface Serializable. Esta interface é apenas de marcação, pois não tem nenhum método a ser implementado, serve apenas para que a JVM saiba que aquela determinada Classe está hábil para ser serializada. Veja um exemplo simples na listagem 1.

Listagem 1: Serialização em Java

import java.io.Serializable;

public class Address implements Serializable{ 
 
	   String street;
	   String country;
 
	   public void setStreet(String street){
		   this.street = street;
	   }
 
	   public void setCountry(String country){
		   this.country = country;
	   }
 
	   public String getStreet(){
		   return this.street;
	   }
 
	   public String getCountry(){
		   return this.country;
	   }
 
	   @Override
	   public String toString() {
    	   return new StringBuffer(" Street : ")
    	   .append(this.street)
    	   .append(" Country : ")
    	   .append(this.country).toString();
	   }
}

Temos acima um bean normal, com seus getters e setters, apenas com uma única diferença: este será serializado.

Transient

Caso você não deseje serializar algum atributo em específico de determinado objeto, basta marcá-lo como transient, assim o objeto serializado não conterá a informação referente ao atributo transient.

Listagem 2: Serialização em Java com atributos transients

import java.io.Serializable;

public class Address implements Serializable{ 
 
	   private transient String street;
	   String country;
 
	   public void setStreet(String street){
		   this.street = street;
	   }
 
	   public void setCountry(String country){
		   this.country = country;
	   }
 
	   public String getStreet(){
		   return this.street;
	   }
 
	   public String getCountry(){
		   return this.country;
	   }
 
	   @Override
	   public String toString() {
    	   return new StringBuffer(" Street : ")
    	   .append(this.street)
    	   .append(" Country : ")
    	   .append(this.country).toString();
	   }
}

O que você pode perceber acima é que o atributo street não será serializado, em outras palavras, nosso objeto serializado não irá conter a rua (street), apenas o pais (country).

Serializando e Deserializando

Precisamos conhecer 2 classes fundamentais para o processo descrito neste tópico: WriteObject e ReadObject.

A classe WriteObject é responsável por serializar determinada instancia, por outro lado a ReadObject faz o processo inverso, a deserialização. Na listagem 3 temos a serialização de um objeto Address toda comentada, para que fique claro o funcionamento desta.

Listagem 3: Serializando com WriteObject

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
 
public class WriteObject{
 
	public static void main (String args[]) {
	
	 /*
	 * Criamos o objeto Address na memória e setamos os valores
	 * de seus atributos
	 * */
	   Address address = new Address();
	   address.setStreet("wall street");
	   address.setCountry("united states");
	   ///////////////////////////////////////////////
 
	   try{
		
		/*
		 * A Classe FileOutputStream é responsável por criar
		 * o arquivo fisicamente no disco, assim poderemos realizar a 
		 * escrita neste. 
		 * */
		FileOutputStream fout = new FileOutputStream("c:\\address.ser");
		
		/*
		 * A Classe ObjectOutputStream escreve os objetos no FileOutputStream
		 * */
		ObjectOutputStream oos = new ObjectOutputStream(fout);   
		
		/*
		 * Veja aqui a mágica ocorrendo: Estamos gravando um objeto 
		 * do tipo Address no arquivo address.ser. Atenção: O nosso 
		 * objeto Address que está sendo gravado, já é gravado de forma 
		 * serializada
		 * */
		oos.writeObject(address);
		
		oos.close();
		System.out.println("Done");
 
	   }catch(Exception ex){
		   ex.printStackTrace();
	   } 
	}
}

Vamos agorar deserializar o objeto Address.

Listagem 4. Deserializando com ReadObject

import java.io.FileInputStream;
import java.io.ObjectInputStream;
 
public class ReadObject{
 
   public static void main (String args[]) {
	   
	   /*
	    * Apenas criamos o objeto address sem instanciá-lo 
	    * pois pegaremos a instância do arquivo address.ser
	    * */
	   Address address;
 
	   try{
		   
		   /*
		    * Responsável por carregar o arquivo address.ser
		    * */
		   FileInputStream fin = new FileInputStream("c:\\address.ser");
		   
		   /*
		    * Responsável por ler o objeto referente ao arquivo
		    * */
		   ObjectInputStream ois = new ObjectInputStream(fin);
		   
		   /*
		    * Aqui a mágica é feita, onde os bytes presentes no arquivo address.ser
		    * são convertidos em uma instância de Address.
		    * */
		   address = (Address) ois.readObject();
		   ois.close();
 
		   System.out.println(address);
 
	   }catch(Exception ex){
		   ex.printStackTrace(); 
	   } 
   }
}

A utilidade destes métodos são inúmeras. Para ilustrar melhor o uso da serialização vamos imaginar algumas situações comuns:

Vamos pensar em um sistema que exija um cadastro com muitos campos (mais de 40), com informações detalhadas. Imagine um sistema que cadastra projetos para construção de casas em massa, onde cada projeto exige esse montante de informações. O usuário começa a cadastrar as informações, mas como são muitas ele pode optar por salvar o estado atual e continuar somente amanhã.

Como você faria isso ? Uma opção a ser pensada é salvar o cadastro como está no banco de dados. Mas isso pode acarretar em vários problemas: Um cadastro incompleto pode ocasionar inconsistências no sistema por diversas formas que não convém tratar aqui. Além da remota possibilidade de faltar energia ou o computador travar e todo cadastro não salvo ser perdido.

Enfim, uma solução provável para isso é aplicar a serialização. Podemos, por exemplo, salvar o cadastro como um objeto serializado na rede (um local seguro no servidor), e amanhã quando o usuário abrir novamente a tela, todos os seus dados estarão “magicamente” preenchidos.

Imagine a aplicabilidade da serialização em documentos de texto, em editores de texto que possuem este recurso (a maioria possui). Você está digitando um texto super importante com informações que de forma alguma podem ser perdidas. Você, por falta de atenção, esquece de salvar o documento e o computador trava. Você deve pensar: E agora ? Perdi todo o conteúdo ? Magicamente quando você abre o editor de texto novamente ele avisa que há documentos que podem ser recarregados, e magicamente o seu documento (mesmo sem ser salvo) é recarregado. A solução ? Serialização de Objetos. Salvamos o estado do documento de 5 em 5 minutos (apenas um exemplo, não que os editores atuais sejam exatamente assim) e mesmo que ocorra um sinistro, podemos recuperar o documento a 5 minutos atrás.

Serialização não é segura

Temos um problema grave, que até então nem imaginava que isso era possível. Vamos primeiro a uma pergunta simples: Porque colocamos os atributos de um Bean como privados e definimos getters e setters para acessá-lo ? A resposte é simples: Para evitar que seja feito um acesso direto ao atributo, assim podemos tratar a forma que queremos mostrar esse atributo. Imagine um Bean chamado CartaoDeCredito onde temos informações sigilosas como número do cartão e senha, no método get você pode definir wildcards (*) para não retornar o número do cartão por completo e muito menos a senha.

Enfim, o que isso tem a ver com serialização ? Como a serialização é uma sequência de bytes com informações puras, está na cara que é possível ler qualquer informações do objeto serializado, independente se ele é privado ou não. Se alguém simplesmente realizar um dump do conteúdo para o console, já consegue visualizar toda a informação contida no bean, sem nenhuma segurança.

Para evitar isso o ideal é implementar um método writeObject e readObject no Bean que desejamos serializar com segurança, assim podemos implementar algoritmos para serializar o valor e deserializar, como uma chave de segurança. Lembre-se que a serialização não serializa métodos, por isso o writeObject e readObject não será mostrado, apenas os valores de atributos. O Exemplo mostrado na listagem abaixo exemplifica o uso desses “ocultadores” de informações.

Listagem 5: Ocultando informações na serialização

public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a)
{
    this.firstName = fn; this.lastName = ln; this.age = a;
}

public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }

public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }

private void writeObject(java.io.ObjectOutputStream stream)
    throws java.io.IOException
{
    
    age = age << 2;
    stream.defaultWriteObject();
}

private void readObject(java.io.ObjectInputStream stream)
    throws java.io.IOException, ClassNotFoundException
{
    stream.defaultReadObject();

    
    age = age << 2;
}

public String toString()
{
    return "[Person: firstName=" + firstName + 
        " lastName=" + lastName +
        " age=" + age +
        " spouse=" + (spouse!=null ? spouse.getFirstName() : "[null]") +
        "]";
}      

private String firstName;
private String lastName;
private int age;
private Person spouse;
}

CONCLUSÃO

A Serialização é muito útil, e muitas vezes obrigatória, em aplicações que envolvem transferência de dados (objetos) em rede, operações com banco de dados e comunicação entre diferentes JVM. Este artigo mostrou os princípios básicos da serialização e um problema de segurança que passa despercebido em muitas das vezes.

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?