Restringir acesso a Web Service / Desktop Application

Estou desenvolvendo um sistema para que os representantes de venda da empresa onde faço estágio possam fazer os pedidos.
A aplicação está sendo feita com Java Web Start, ou seja, uma aplicação que roda na máquina do usuário.
O sistema precisa se conectar a uma base de dados única, via internet, já que os representantes estão espalhados por várias partes do país.

Pedindo por ajuda aqui no fórum e pesquisando na internet, vi que seria uma má ideia disponibilizar o banco de dados na internet, que o correto seria utilizar um web service para servir como intermediário na comunicação do banco de dados com a aplicação. Já que o web service rodaria no mesmo “local” que o banco de dados, eu não precisaria disponibilizar o banco de dados na internet.

Como não tenho experiência com web service, resolvi seguir um tutorial na internet, para entender o funcionamento do web service.
A aplicação de teste que fiz, funcionou normalmente, mas analisando a situação, percebi que há um problema grave:
QUALQUER UM que saiba a URL do meu Web Service pode executar os “métodos” do web service e receber/enviar informações ao banco de dados.

Se eu criar um método no web service para fazer um SELECT Nome FROM CLIENTES, por exemplo, qualquer um que saiba a URL do web service pode criar uma aplicação-cliente para que esse método seja usado e TCHARAM!, tem acesso ao nome de todos os clientes.

Visto que o sistema só será utilizado por pessoas previamente cadastradas, e que portanto não há o interesse de o Web Service ser algo público, há como eu restringir o acesso ao web service apenas para os usuários desejados?

Estou confuso.

Vc pode usar autenticação no Web Service, usando a mesma senha dos usuários dos usuários cadastrados.

Qual servidos está usando para prover os serviços?(JBoss, Glassfishm, Spring WS, CXF, etc)

Cara, sou iniciante então minha sugestão não é lá essas coisas, mas uma coisa que você poderia fazer era obrigar o cliente do webService a, quando fizesse uma requisição, passar um token de autenticação. Aí, dentro do método do webservice, vc validaria o token. Essa é uma idéia básica. Tem outras formas muito mais avançadas e seguras para se fazer isso mas entendo pouco. Uma boa dica (pra quem é iniciante no uso de webServices) é o livro da casa do código de SOA. Me ajudou muito aqui no trabalho. Tem a questão de segurança lá tambem mas infelizmente ainda não cheguei lá. Chegando pode ter certeza que eu te passo mais informações.
Abraços.

Foi o que perguntei, tem a especificação WS-Security com algumas possibilidades, sendo que a se encaixa melhor no contexto é a de autenticação usando usuário e senha.

Porém cada um ‘trata da forma como quer’, tipo o JBoss usa tais arquivos, Spring outros, etc.

Se não me engano tenho exemplo usando JBoss e Spring com autenticação via usuário e senha.

[quote=lsjunior]Vc pode usar autenticação no Web Service, usando a mesma senha dos usuários dos usuários cadastrados.

Qual servidos está usando para prover os serviços?(JBoss, Glassfishm, Spring WS, CXF, etc)[/quote]

Glass Fish.

Exatamente o que não mexo…

Bom vc vai precisar fazer a autenticação via UsernameToken, uma consulta no google usando ‘glassfish 3 ws-security usernametoken’ retornou essa página:
http://www.oio.de/public/xml/usernametoken-tutorial-webservice-security-artikel.htm
Da pra se basear nele(tem que pedir para traduzir ao clicar no link).

Acho que na documentação do JavaEE(tutorial) ou no site do Netbeans possa achar algo.

[quote=kbello]Cara, sou iniciante então minha sugestão não é lá essas coisas, mas uma coisa que você poderia fazer era obrigar o cliente do webService a, quando fizesse uma requisição, passar um token de autenticação. Aí, dentro do método do webservice, vc validaria o token. Essa é uma idéia básica. Tem outras formas muito mais avançadas e seguras para se fazer isso mas entendo pouco. Uma boa dica (pra quem é iniciante no uso de webServices) é o livro da casa do código de SOA. Me ajudou muito aqui no trabalho. Tem a questão de segurança lá tambem mas infelizmente ainda não cheguei lá. Chegando pode ter certeza que eu te passo mais informações.
Abraços.[/quote]

Esse “token” que você diz seria um parâmetro passado?

Exemplo:

Na minha aplicação, cliente do meu web service eu faria algo como:
service.getNomes(“123”);

E no web service:

public String getNomes(@WebParam(name = "chave") String chave) { if ("123".equals(chave)) { //código aqui } else { return null; } }

Foi algo assim que você quis dizer?

Se for isso, de fato, isso geraria uma maior segurança do que minha atual situação (na qual há 0 de segurança), mas pelo que eu li a respeito, é possível descompilar o programa. Se alguém descompilasse a minha aplicação, ele teria o valor passado como parâmetro.

[quote=lsjunior]Exatamente o que não mexo…

Bom vc vai precisar fazer a autenticação via UsernameToken, uma consulta no google usando ‘glassfish 3 ws-security usernametoken’ retornou essa página:
http://www.oio.de/public/xml/usernametoken-tutorial-webservice-security-artikel.htm
Da pra se basear nele(tem que pedir para traduzir ao clicar no link).

Acho que na documentação do JavaEE(tutorial) ou no site do Netbeans possa achar algo.[/quote]

Opa, muito obrigado, cara!
Vou dar uma olhada.

[quote=wellington_r][quote=kbello]Cara, sou iniciante então minha sugestão não é lá essas coisas, mas uma coisa que você poderia fazer era obrigar o cliente do webService a, quando fizesse uma requisição, passar um token de autenticação. Aí, dentro do método do webservice, vc validaria o token. Essa é uma idéia básica. Tem outras formas muito mais avançadas e seguras para se fazer isso mas entendo pouco. Uma boa dica (pra quem é iniciante no uso de webServices) é o livro da casa do código de SOA. Me ajudou muito aqui no trabalho. Tem a questão de segurança lá tambem mas infelizmente ainda não cheguei lá. Chegando pode ter certeza que eu te passo mais informações.
Abraços.[/quote]

Esse “token” que você diz seria um parâmetro passado?

Exemplo:

Na minha aplicação, cliente do meu web service eu faria algo como:
service.getNomes(“123”);

E no web service:

public String getNomes(@WebParam(name = "chave") String chave) { if ("123".equals(chave)) { //código aqui } else { return null; } }

Foi algo assim que você quis dizer?

Se for isso, de fato, isso geraria uma maior segurança do que minha atual situação (na qual há 0 de segurança), mas pelo que eu li a respeito, é possível descompilar o programa. Se alguém descompilasse a minha aplicação, ele teria o valor passado como parâmetro. [/quote]

Sério isso? Não sabia disso não… então o jeito q eu falei é melhor deixar de lado…

A validação fica por conta do servidor do WS.

O que ocorre é que quando ativa a segurança, o cliente cria um elemento contendo os dados da segurança. Esses dados podem ser o tal usernametoken, criptografia, assinatura digital, etc. Do lado do servidor será verificado se essas credenciais são válidas. Dessa forma vc poderia, na chamada do cliente, aidicionar a informação da autenticação.

http://en.wikipedia.org/wiki/WS-Security

A usernametoken por si não garante tanta segurança, pois o XML seria enviado em texto plano. Colocando o Web Service rodando em HTTPS evitaria a captura da informação.

[quote=wellington_r]Estou desenvolvendo um sistema para que os representantes de venda da empresa onde faço estágio possam fazer os pedidos.
A aplicação está sendo feita com Java Web Start, ou seja, uma aplicação que roda na máquina do usuário.
O sistema precisa se conectar a uma base de dados única, via internet, já que os representantes estão espalhados por várias partes do país.

Pedindo por ajuda aqui no fórum e pesquisando na internet, vi que seria uma má ideia disponibilizar o banco de dados na internet, que o correto seria utilizar um web service para servir como intermediário na comunicação do banco de dados com a aplicação. Já que o web service rodaria no mesmo “local” que o banco de dados, eu não precisaria disponibilizar o banco de dados na internet.

Como não tenho experiência com web service, resolvi seguir um tutorial na internet, para entender o funcionamento do web service.
A aplicação de teste que fiz, funcionou normalmente, mas analisando a situação, percebi que há um problema grave:
QUALQUER UM que saiba a URL do meu Web Service pode executar os “métodos” do web service e receber/enviar informações ao banco de dados.

Se eu criar um método no web service para fazer um SELECT Nome FROM CLIENTES, por exemplo, qualquer um que saiba a URL do web service pode criar uma aplicação-cliente para que esse método seja usado e TCHARAM!, tem acesso ao nome de todos os clientes.

Visto que o sistema só será utilizado por pessoas previamente cadastradas, e que portanto não há o interesse de o Web Service ser algo público, há como eu restringir o acesso ao web service apenas para os usuários desejados?

Estou confuso.[/quote]

Depois de vários posts explicando como usar webservices, acho que no seu caso é melhor não usar webservices. Não os tradicionais, pelo menos.

O webservice nada mais é que uma aplicação web especial, mas no seu caso ela precisa ser ainda mais especial, porque é na realidade uma parte interna de um sistema e não um serviço publico.
A autenticação não é do usuário e sim da aplicação cliente como um todo.

Bastaria que vc encriptasse com um esquema de chave publica-privada. Assim o cliente tem a chave para encriptar, mas não tem como desincriptar, isso apenas o server sabe.
Como vc usa jws vc pode usar o jnlp para ser gerado dinamicamente por um jsp ou um servlet e gerar na hora uma chave publica-privada. A chave privada o servidor guarda e a publica ele passa no jnlp.
O cliente usa essa chave para encriptar as chamadas e o servidor para desencriptar. As chamadas em si podem ser simples classes no padrão command que são serilizadas, ou apenas os parametros e o nome do método num esquema de lista simples. O que interessa é que o servidor saiba interpretar isso do outro lado depois de desencriptar. Este processo não necessita de intervenção do usuário e funciona como um todo para o sistema que irá usar este mecanismo mesmo antes do usuário se logar.
Se quiser ainda melhor use HTTPS em vez de HTTP. Assim existe uma camada a mais e segurança na própria transmissão.

Agora um detalhes, seu serviço nunca jamais deve receber sql. Ele deve receber os parâmetros de uma função, é o servidor que vai interpretar o nome da função e chamar algum código que no fim poderá ou não usar um SQL. Isto tb é uma outra forma de dar mais segurança.

Outra coisa simples que você poderia fazer seria gerar um hash com o próprio usuário e senha do seu sistema e enviar como um token para o web service e fazer algo como:

public void metodoPublicadoWS(byte[] token, ...) { checkToken(token); ... //faz o que vc tiver que fazer }

Sendo que este checkToken() seria um método seu que validaria o token informado pelo usuário. Caso não valide, lança uma exception (que você irá tratar como quiser)…

Tenho aplicações com o contexto bem parecido com o seu aqui e uso esta forma para garantir a segurança. Uma vantagem é estar por dentro do processo de autenticação (pois foi criado por mim mesmo) e ter a liberdade de a qualquer momento mudar o algoritmo de criação do hash…

[quote=erico_kl]Outra coisa simples que você poderia fazer seria gerar um hash com o próprio usuário e senha do seu sistema e enviar como um token para o web service e fazer algo como:

public void metodoPublicadoWS(byte[] token, ...) { checkToken(token); ... //faz o que vc tiver que fazer }

Sendo que este checkToken() seria um método seu que validaria o token informado pelo usuário. Caso não valide, lança uma exception (que você irá tratar como quiser)…

Tenho aplicações com o contexto bem parecido com o seu aqui e uso esta forma para garantir a segurança. Uma vantagem é estar por dentro do processo de autenticação (pois foi criado por mim mesmo) e ter a liberdade de a qualquer momento mudar o algoritmo de criação do hash…[/quote]

E como o servidor sabe qual é o usuário ?

No primeiro acesso ao servidor o cliente envia o login e a senha, então o servidor verifica a existência do mesmo no banco. Se existe, gera o hash, guarda num Map e retorna para o cliente, que estará participando da “sessão” e enviará este token em cada requisição. Basicamente nas futuras requisições o servidor somente percorre o map esperando encontrar lá o token gerado que o cliente enviou. Se encontrar, realiza a ação, senão nada acontece e uma RemoteException é lançada para o cliente

Esse seu hash é nada mais que um sessionId. O servidor já vai mandar um sessioId do container, então não ha muita diferença (excepto que o servidor manda dentro de um cookie)

Pesquise por Session Hijacking

Com esse modelo um adversário pode pegar seu user e pass e mais tarde usá-los para simular que é vc e usar os mesmos serviços para outros fins. Estas informações deveria está criptografas de alguma forma na primeira comunicação. No minimo a primeira comunicação tem que ser HTTPS. Mesmo depois disso, o adversário pode usar o mesmo hash em outra sessão, o que tb não é bom.

Vc substituiu o envio de dois parametros (nome e senha) pelo envio de um. Mas não aumentou a segurança da aplicação e seria equivalente a enviar o user e senha a cada request.

Mas o usuário e senha não são enviados hardcoded… O algoritmo além de gerar o hash, cria um MD5 do token que é enviado (lembrando que o algoritmo do hash é um código próprio)

O cara só vai ter acesso ao web service se ele tiver um usuário/senha da aplicação…

Não entendi o que quer dizer com isso. eles são encriptados para envio ?

[quote]
O cara só vai ter acesso ao web service se ele tiver um usuário/senha da aplicação…[/quote]

Isso é simples em sistema web. Basta colocar algum sniffer na rede. Porque o pessoa manda os forms com http (plain text) fica fácil de saber. Apenas o HTTPS garante que ha encriptação.
É por isso, que o Google sempre pede sua senha por HTTPS (se ainda nunca reparou) e todos os sites deveriam fazer isso. Senão, não ha realmente segurança alguma. Qualquer pessoa pode pegar um par de usuário-senha. Tudo bem que não é trivial, mas convenhamos que do ponto de vista da segurança da aplicação é furada.

Na verdade é um byte[] md5 do hash do usuário e senha da aplicação

[quote]
Isso é simples em sistema web. Basta colocar algum sniffer na rede. Porque o pessoa manda os forms com http (plain text) fica fácil de saber. Apenas o HTTPS garante que ha encriptação.
É por isso, que o Google sempre pede sua senha por HTTPS (se ainda nunca reparou) e todos os sites deveriam fazer isso. Senão, não ha realmente segurança alguma. Qualquer pessoa pode pegar um par de usuário-senha. Tudo bem que não é trivial, mas convenhamos que do ponto de vista da segurança da aplicação é furada.[/quote]

Não é a forma mais segura que existe, mas dependendo do porte da aplicação e contexto do problema é bem viável… Apenas citei por ser uma solução simples que resolveria o problema de qualquer pessoa poder acessar o web service e consumir os métodos sem restrição nenhuma…

Tentei implementar o projeto do link que o lsjunior passou (http://www.oio.de/public/xml/usernametoken-tutorial-webservice-security-artikel.htm), para entender como funciona o UsernameToken.

Acho que fiz certo, mas não sei por que o programa está imprimindo o que não era esperado por mim.
Depois de fazer a entrada do usuário e senha, recebo:

[quote][color=red]Fev 20, 2013 8:19:38 AM [com.sun.xml.ws.policy.jaxws.PolicyConfigParser] parse
INFO: WSP5018: Loaded WSIT configuration from file: file:/C:/Users/Wellington/Downloads/usernametoken-tutorial-beispiele/UsernameTokenServiceSOClient/build/classes/META-INF/wsit-client.xml.[/color]
—[HTTP request - http://localhost:8080/UsernameTokenService/UsernameTokenServiceService]—
Content-type: text/xml;charset=utf-8
Soapaction: "http://server/UsernameTokenService/sayHelloRequest"
Accept: text/xml, multipart/related

<?xml version='1.0' encoding='UTF-8'?>http://localhost:8080/UsernameTokenService/UsernameTokenServiceServicehttp://server/UsernameTokenService/sayHelloRequest
<Address>http://www.w3.org/2005/08/addressing/anonymous</Address>

(…)
<S:Body wsu:Id="_5006"><xenc:EncryptedData xmlns:ns18=“http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512” xmlns:ns17=“http://www.w3.org/2003/05/soap-envelope” Id="_5007" Type=“http://www.w3.org/2001/04/xmlenc#Content”><xenc:EncryptionMethod Algorithm=“http://www.w3.org/2001/04/xmlenc#aes128-cbc”/><ds:KeyInfo xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance” xsi:type=“KeyInfoType”>wsse:SecurityTokenReference<wsse:KeyIdentifier ValueType=“http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKeySHA1” EncodingType=“http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary”>+4RSjqP2NBeSuTwB+YTnlRwSswU=</wsse:KeyIdentifier></wsse:SecurityTokenReference></ds:KeyInfo>xenc:CipherDataxenc:CipherValuebVNjqtFFp/ft+Ot1rCe1Hcn9Bx4pWPkYuWQwuzoDawjpLV2G+t2Iu+jMcqgrFP1u/bdx2AtNUX4WD/RN0U0+66yelG/r3BLDowPpKgluarAQBwhYPw1h4wt5X2xCcq+Xzq0mwKkrA5bHh8Dl/+2bMDpH3StPqDiBGxH4DSDiLMI=</xenc:CipherValue></xenc:CipherData></xenc:EncryptedData></S:Body></S:Envelope>--------------------
Result = Hello Test[/quote]

(A primeira parte da saída é em vermelho, mesmo)

O único resultado deveria ser “Result = Hello Test” (indica que a autenticação foi feita com sucesso).
Não consigo descobrir de onde vem o restante da saída.

Minha classe Main:

[code]public class Main {

public static void main(String[] args) {
    try {
        System.out.println("Digite o nome do usuário: ");
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        UserPasswordInfo.setUsername(reader.readLine());
        System.out.println("Digite a senha: ");
        UserPasswordInfo.setPassword(reader.readLine());
        reader.close();

        System.setProperty("com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump", "true");
        server.UsernameTokenServiceService service = new server.UsernameTokenServiceService();
        server.UsernameTokenService port = service.getUsernameTokenServicePort();
        BindingProvider prov = (BindingProvider) port;
        List<Handler> handlerChain = prov.getBinding().getHandlerChain();
        handlerChain.add(new UsernameTokenHandler());
        prov.getBinding().setHandlerChain(handlerChain);
        prov.getRequestContext().put(prov.ENDPOINT_ADDRESS_PROPERTY, "http://localhost:8080/UsernameTokenService/UsernameTokenServiceService");
        java.lang.String name = "Test";
        java.lang.String result = port.sayHello(name);
        System.out.println("Result = " + result);
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

}[/code]

Consegui resolver o problema.

Segui o seguinte tutorial:
http://docs.oracle.com/cd/E17802_01/webservices/webservices/reference/tutorials/wsit/doc/WSIT_Security9.html#wp162458

As configurações do web service, no servidor:

Serviço seguro: habilitado
Localização de keystore: keystore.jks, no diretório do Glassfish.
Demais configurações: padrão ou como especificado no tutorial.

As configurações do cliente do web service na aplicação-cliente:
Credenciais de autenticação: dinâmico
Handler de callback de nome de usuário: security.PWCallback (pacote security, classe PWCallback)
Handler de callback de senha: security.PWCallback
Localização de truststore: cacerts.jks (o arquivo deve ser colocado em PastadoProjeto>build>classes>META-INF, manualmente)
Demais configurações: padrão

Código da classe PWCallback:

[code]package security;

import java.io.IOException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

public class PWCallback implements CallbackHandler {

//thread specific username, initialized to null; 
private static ThreadLocal username = new ThreadLocal() {
    @Override
    protected synchronized Object initialValue() {
        return null;
    }
};
//thread specific password, initialized to null; 
private static ThreadLocal password = new ThreadLocal() {
    @Override
    protected synchronized Object initialValue() {
        return null;
    }
};

public static void setThreadIdentity(String user, String passw) {
    password.set(user);
    username.set(passw);
}

@Override
public void handle(Callback[] callbacks) throws IOException,
        UnsupportedCallbackException {
    for (int i = 0; i < callbacks.length; i++) {
        if (callbacks[i] instanceof NameCallback) {
            NameCallback nc = (NameCallback) callbacks[i];
            String user = (String) username.get();
            nc.setName(user);
            username.set(null);
            
        } else if (callbacks[i] instanceof PasswordCallback) {
            PasswordCallback pc = (PasswordCallback) callbacks[i];
            String passCode = (String) password.get();
            pc.setPassword(passCode.toCharArray());
            //also, after each use revert to using default 
            password.set(null);
        }
    }
}

}[/code]

Código da classe Main:

[code]package client;

import java.util.Scanner;
import security.PWCallback;
import security.Pref;

public class Main {

public static void main(String[] args) {
    try {
        Scanner scan = new Scanner(System.in);
        String id = scan.nextLine();
        
        PWCallback.setThreadIdentity("123", "usuario");

        server.CalculatorWS_Service service = new server.CalculatorWS_Service();
        server.CalculatorWS port = service.getCalculatorWSPort();

        java.lang.String resultado = port.hello(id);
        System.out.println("Result = " + resultado);
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

}[/code]

O único problema que ocorreu, foi que como se pode ver na linha

da classe Main, eu tive que inverter os argumentos, para que o código funcionasse. O esperado era setThreadIdentity(String user, String passw), mas quando passei os argumentos na ordem certa, estava dando exception, pois a senha era tida como o nome de usuário. Alguém sabe por que isso ocorre?


Além disso, o único “empecilho” é que implementando o web service dessa maneira, ao criar um usuário novo para o sistema, terei que criar um usuário com mesmo login e senha no glassfish. Mas isso não é um problema tão grande, já que a empresa tem apenas 15 representantes de venda, e eles raramente mudam.

PS: para implementar a classe PWCallBack e entender como utilizá-la utilizei o seguinte link:
http://netbeans-org.1045718.n5.nabble.com/How-to-do-dynamic-authentication-with-default-callback-handler-td3274173.html