20 de abril de 2011

Java EE 6: Segurança com Spring Security 3.0.5

Quando se fala em sistemas online, logo deve-se pensar em como tratar a parte da segurança dos dados, de acesso... nessa postagens vamos trabalhar com a segurança de acesso ao sistema, ou seja, o login.
Para fazer esse controle de login vou utilizar o framework Spring Security na versão 3.0.5, estarei disponibilizando os jar's usados, pois não aconselho baixar do próprio site deles, andei fazendo uns testes e o que está disponível agora nessa mesma versão não bate com o que eu baixei a um tempo atrás, então para fazer este projeto o mesmo pode ser baixado por aqui.
O Spring Security trata a autenticação de usuários da seguinte forma: o bloqueio de acesso pode ser feito a diretórios ou a arquivos, ou seja o usuário deve ter a permissão de acesso para o determinado diretório ou arquivo, simplificando... suponhamos que eu tenha um diretório admin, com o Spring Security eu posso definir que apenas usuários que tenham permissão de administrador possa ter acesso ao conteúdo desse diretório. Existem duas formas de fazer a autenticação de acesso, com usuários pré-definidos no xml de configuração ou fazendo a validação de usuário em um banco de dados. Nessa postagem vamos tratar essas duas formas.
Inicialmente vamos criar o nosso projeto de base que vai funcionar para os exemplos com ou sem banco de dados.
Estrutura


Obs: veja que temos um diretório admin e dentro dele um xhtml com o nome de index, então nesse projeto temos 2 index, o da raiz poderá ser acessado sem fazer login, já o que se encontra dentro da do diretório precisará de permissão para ser acessado. Vamos começar com a codificação:
/index.xhtml
<h:body>
        Usuario não precisa estar logado...
</h:body>

/negado.xhtml
<h:body>
        Você não tem permissão de acessar este diretorio.
        <a href="index.jsf">Voltar </a>
</h:body>

/falha.xhtml
<h:body>
        Login ou senha incorretos...
        <a href="index.jsf">Voltar</a>
</h:body>

/admin/index.xhtml
<h:body>
        Usuario logado com sucesso!
        <a href="../logout">Sair</a>
</h:body>
O /logout eu vou explicar mais pra frente... é coisa da config do Spring Security.
Depois de criar as páginas para fazer nosso teste, vamos colocar as bibliotecas no projeto, você pode criar uma biblioteca no Netbeans (Ferramentas/Bibliotecas/Nova biblioteca...), ou adicionar diretamente os jar's ao projeto (no projeto, botão direito em Bibliotecas e em add jar).
Os jar's são:



Outra coisa em comum nos exemplos que vamos fazer é a configuração do Spring Security no WEB-INF/web.xml, dentro dele adicione a seguinte configuração (pode ser logo abaixo da tag web-app):

<!-- Spring security -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

<!-- Fim spring security -->

Se você executar ele neste ponto, dará erro, pois com essa configuração ele exige que você tenha um /WEB-INF/applicationContext.xml, então para começar crie um arquivo xml com o nome de applicationContext dentro do WEB-INF...

Segurança de acesso com usuários pré-definidos (sem BD)
Toda a configuração a partir de agora é feita dentro do arquivo applicationContext.xml citado acima.
Abaixo vou postar como ele fica e depois explico as tags:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <http auto-config="true" access-denied-page="/negado.jsf">
        <intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
        <logout invalidate-session="true" logout-success-url="/index.jsf" logout-url="/logout"/>
    </http>

    <authentication-manager>
        <authentication-provider>
            <user-service>
                <user name="adm" password="123" authorities="ROLE_ADMIN" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

</beans:beans>
beans: é a raiz do nosso xml, nela é definido os namespace do Spring Security.

http: caso não tenha percebido, nesse projeto nós não fizemos uma página de login, isso porque ao setar a propriedade auto-config como true, o próprio Spring Security cria uma página de login (ao final da postagem ensino como personalizar sua página de login), e a propriedade access-denied-page se refere à pagina que deve ser mostrada quando um usuário for logado mas não tem acesso a um diretório ou arquivo (esse arquivo negado.jsf se refere à nossa página criada lá na estrutura do projeto).
intercept-url: esta tag é a responsável por definir quais as permissões que o usuário deve ter para acessar determinados diretórios ou arquivos, no exemplo veja que estamos definindo que o usuário deve ter a permissão ROLE_ADMIN para ter acesso aos arquivos do diretório admin.
logout: esta tag é a responsável por chamar a servlet que desloga o usuário, a propriedade logout-sucess-url se refere a página pra onde o usuário deve ser enviado ao ser deslogado, e o logout-url seria o nome da servlet... lembra que na página /admin/index.jsf nós temos "../logout", pois bem, é a esta configuração que ele está se referindo.

authentication-manager: gerencia a autenticação.
authentication-provider: provedor de autenticação.
user-service: é onde deve-se definir os usuários.
user: é o usuário propriamente dito, a propriedade name é o login do usuário, password é a senha e authorities são as permissões do usuário. Um usuário pode ter mais de uma permissão, estas devem ser separadas por vírgula na propriedade authorities, ficando por exemplo assim:
authorities="ROLE_ADMIN,ROLE_USER".

Bom, assim nosso login com usuário pré-definido já está funcionando, basta testar...

Segurança de acesso autenticando o usuário no banco de dados (com BD)
Para este exemplo vamos utilizar o MySQL, precisamos ter uma tabela com a seguinte estrutura:


Para fazer este projeto, aconselho apenas fazer uma cópia do projeto acima, pois só precisaremos alterar o applicationContext.xml, o mesmo deve ficar assim (logo abaixo do código vou explicar as novas tags):
<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <http auto-config="true" access-denied-page="/negado.jsf">
        <intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
        <logout invalidate-session="true" logout-success-url="/index.jsf" logout-url="/logout"/>
    </http>

    <beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" >
        <beans:property name="url" value="jdbc:mysql://localhost:3306/5tads" />
        <beans:property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <beans:property name="username" value="root" />
        <beans:property name="password" value="admin" />
    </beans:bean>
    
    <authentication-manager>
        <authentication-provider>
            <jdbc-user-service data-source-ref="dataSource"
                users-by-username-query="SELECT username, password, 'true' as enable FROM users WHERE username=?"
                authorities-by-username-query="SELECT username, authority FROM users WHERE username=?"
            />
        </authentication-provider>
    </authentication-manager>

</beans:beans>
Algumas tags são padrão tanto para usar com ou sem banco, por exemplo a http. Agora vamos ver quais as tags que temos de diferente do exemplo anterior (sem BD).

bean: entre várias possibilidades de utilização desta, nesse exemplo estamos usando ela para definir o dataSource que fará a conexão com o banco de dados MySQL, a propriedade id é nome que identifica esse bean... já a propriedade class se refere a classe do Spring Security que é utilizada para fazer a conexão.
property: serve para setar uma propriedade, neste exemplo é usada para setar propriedades diferentes referentes a conexão ao banco de dados. Vamos ver as propriedades pelos name...
url: é a url de acesso ao banco de dados, 5tads é a database que eu estou usando
driverClassName: é o nome do driver do MySQL
username: é o usuário de acesso ao banco de dados
password: é a senha de acesso ao banco de dados.

Veja que o nosso authentication-provider mudou... vamos ver o que ele mudou:
Quando utilizamos o banco de dados para fazer a validação dos usuários, não temos o porque de definir os usuários manualmente no xml, por isso o nosso user-service foi substituído por jdbc-user-service.
jdbc-user-service: a propriedade data-source-ref utiliza como referencia o nosso bean de conexão acima com o id de dataSource, já a users-by-username-query é a query responsável por selecionar o usuário pelo username... quanto ao "'true' as enable" na query, seria por exemplo uma coluna booleana na nossa tabela users dizendo se o usuário está ativo ou não. A outra propriedade é a authorities-by-username-query que tem uma query que é responsável por pegar as permissões do usuário pelo username.

Feito isso... o nosso controle de login com BD está pronto, e é só testar! 


Página de login
Como já falei anteriormente, o Spring Security já tem sua página de login que é chamada pela servlet : /spring_security_login ... A página é a seguinte:


Vou criar a minha página de login... dentro de páginas web crie uma nova página JSF, com o nome de login.xhtml, ela será a responsável por substituir a página do Spring Security, o código dela é o seguinte:
<h:body>
        <a href="index.jsf">Retornar para a Página Inicial</a>
        <form action="j_spring_security_check" method="post">
            <h:panelGrid columns="2">
                <h:outputText value="Usuario" />
                <h:inputText id="j_username" />
                <h:outputText value="Senha" />
                <h:inputSecret id="j_password" />
            </h:panelGrid>
            <input type="submit" value="Efetuar Login" />
        </form>
</h:body>
Mas agora você deve estar se perguntando... porque não usar o form do próprio JSF e o commandButton no lugar do input? Então, pelos testes que eu fiz, não é possível usar o form e o commandButton do JSF, ele acaba não funcionando direito, e olha que não foi por falta de testes...
Mas vamos entender algumas coisinhas.. como já falei, precisamos usar o form e o input do próprio HTML, outras coisas são de extrema importância para o funcionamento, a action do form precisa ser a servlet j_spring_security_check, assim como os id's dos campos precisam ser j_username e j_passsword ... se eu não me engano quando usa-se o Spring Security de forma personalizada (daria uma longa postagem) tem como usar outros id's, mas não é o nosso foco hoje.
Agora temos que informar ao applicationContext.xml que temos uma página de login, para isso, dentro da tag http, basta informar a seguinte linha:
<form-login login-page="/login.jsf" authentication-failure-url="/falha.jsf"/>
A propriedade login-page é a responsável por dizer qual é a nossa página de login, e a authentication-failure-url é a página que informa que o login e a senha estão incorretos (fizemos essa página lá no inicio da postagem).

Pra quem precisar de algo mais complexo como um login personalizado, deixo a seguinte referencia: Spring Security 3 - MVC Integration: Using A Custom Authentication Manager me ajudou muito quando precisei implementar um assim!

Referências: 

32 comentários:

  1. Muito bom o artigo.
    Imagine que um usuário tenha mais de um perfil para auntenticar-se, role_admin, role_x, role_y, etc...
    Como fazer para que na tela de login ele escolha qual perfil utilizar para o login?

    ResponderExcluir
  2. Olá Junior! Pelo o que eu fiz de testes com o SP, não vai ser você que escolhe qual perfil usar, o próprio SP que vai ver quais as permissões que o usuário tem, e assim liberar o acesso de acordo com os perfis, ou seja, ele não leva em consideração apenas um perfil do usuário, mas todos de uma vez só.
    Bom, não sou expert em SP, mas foi isso que eu entendi ao usar ele e ao ler a doc dele.

    ResponderExcluir
  3. Veja nessa parte: 'authorities-by-username-query="SELECT username, authority FROM users WHERE username=?"' ... ele faz um select para pegar todas as authorities do usuário, ou seja, usa todas de uma vez.

    ResponderExcluir
  4. Tens muita razão mano (a), ele considera todos os privilégios possíveis para cada usuário.
    Trabalhar com esse framework e muito seguro mesmo.

    ResponderExcluir
  5. Olá Vilas, realmente com o Spring Security não tem falha, só acessa se souber a senha do usuário.

    ResponderExcluir
  6. Gostei muito do artigo, mas uma duvida não relacionado ao framework. Alguem sabe dizer como faço para chamar um metodo quando a pagina for carregada ? Exemplo de uma pagina for carregada digitando a url dela, eu valido nesse metedo se o acesso é permitido.

    ResponderExcluir
  7. Não entendo pq queira fazer um metodo para validar se a pagina é permitida ou não, o proprio Spring já faz exatamente isto! Veja pela configuração do xml

    ResponderExcluir
  8. Me deparei com a seguinte situacao: entro com um usuario, e sem deslogar acesso a pagina de login, e entro com outro usuario. Ao invés do spring logar com o segundo usuario, ele continua com o login e os dados do primeiro. Alguém ja se deparou com isso? Isso pode ser um problema, pois se um usuario esquecer de deslogar em uma maquina e vim outro e logar, ele entra com os dados do primeiro, assim podendo ter acesso aos dados do outro.

    ResponderExcluir
  9. Andii tenho um menu (primefaces) soh q queria renderizar soh alguns intens desse menu de acordo com suas ROLE, tem alguma função para q eu pegue se ele tem permissão algo do tipo: renderer=#{isUserInRole} ??

    ResponderExcluir
  10. è descobri..é soh fazer isso -
    rendered="#{facesContext.externalContext.userInRole('ROLE_USER')}"
    Mas a questao de Felipe he interessante! to no aguardo da respota tmb.

    ResponderExcluir
    Respostas
    1. Aqui tive que usar assim:
      rendered="#{facesContext.externalContext.isUserInRole('ROLE_ADM')}"
      Com o "is" no início. Mesmo que ao utilizar a sugestão do IDE, aparece o método sem o "is" (igual ao seu), mas se utilizar a sugestão ocorre o seguinte erro:
      javax.servlet.ServletException: /menu/Menu.xhtml @36,102 rendered="#{facesContext.externalContext.userInRole('ROLE_ADM')}": Unable to find method [userInRole] with [1] parameters

      Excluir
    2. Estranho Junior, está usando a mesma versão? 3.0.5?
      Mas de qualquer forma, menos mau se funcionou! :)

      Excluir
  11. ;) Quanto a questão do Felipe, assim que eu arrumar um tempinho vou atrás de descobrir sobre isso... mas talvez uma solução seria: ao carregar a página de login (onLoad) chamar um método que deslogue o usuário atual, não testei isso, mas acredito que funcionaria sim!

    ResponderExcluir
  12. Jonathas eu nao consigo acessar essa opcao de verificar pelo faceContext se o usuario possui tal permissao. Voce sabe me dizer o pq? ela nem chega a aparecer o auto-complete do eclipse. to usando jsf 2.0 + primefaces 2.1

    ResponderExcluir
  13. Se tento acessar a página /admin/index.jsf ele me pede senha

    mas quando coloco num botão

    ele entra na página sem pedir senha nenhuma

    o que fiz de errado?

    ResponderExcluir
    Respostas
    1. Carlos, vc não está fazendo nada de errado, bem depois que eu fiz a postagem eu percebi isso também, acredito que seja aquela questão de URL do JSF, ainda estou atrás de uma solução exata para isso, mas por enquanto eu evito colocar botões para chamar outra página, ou quando se trata de permissão de acesso em menus, eu deixo o menu desabilitado se a pessoa não tem permissão de acessar determinada parte do sistema.
      Não testei novas versões do Spring Security, talvez tenham arrumado uma solução.

      Excluir
  14. Ola Andi!
    segui o seu tutorial mas quando eu informo o login e senha
    ele me retorna "Reason: Bad credentials", você teria alguma ideia de onde eu possa estar errando?
    Obrigada!!

    ResponderExcluir
    Respostas
    1. Mylla, geralmente esse erro é quando digita login e senha incorreto! veja se as vezes vc está usando o MD5 no applicationContext e não está com a senha criptografada no banco.

      Excluir
    2. Olá andii!!
      Então consegui resolver reescrevendo meu applicationContext, tinha feito alguma coisa errada que não consegui identificar. Comecei a aprender java a pouco tempo e vivo cometendo esses erros :). Seu blog tem me ajudado muito, já fiz bastante coisa seguindo seus post.
      Obrigada.

      Excluir
  15. Ola,

    Otimo artigo, tentei implementar em uma aplicação financeira que estou fazendo e começou a dar erro: Error listenerStart

    Então iniciei um projeto do zero pelo netbeans 7.1.1 usando o tomcat 7.0.22 e fui fazendo o exemplo sem BD e veja so o erro que aconteceu: GRAVE: Error listenerStart
    09/06/2012 01:29:26 org.apache.catalina.core.StandardContext startInternal
    GRAVE: Context [/SpringSecuritySemBD] startup failed due to previous errors

    Será que estou fazendo algo de errado? Agradeço muito se poder me ajudar pois já a alguns dias que tento colocar o spring security pra funcionar se sucesso!

    forte abraço.

    ResponderExcluir
    Respostas
    1. Cléber, em nenhum momento eu cheguei a testar com o Tomcat, se foi seguido o exemplo acima, não tem erro com o GlassFish. Tentou pesquisar alguma coisa sobre Spring Security e Tomcat?

      Excluir
  16. Boa tarde andii, tudo bem?

    Estou achando seu blog excelente, muito bem explicado, segui todos os passos e está funcionando perfeitamente. Agora preciso de uma ajuda: qual seria a melhor prática para redirecionar o usuário de uma página para outra? Exemplo: após logar o usuário deveria ser redirecionado para a página "home.jsf", qual seria a melhor forma de implementar isso? como você tem feito para resolver essas situações?

    Abs.,
    Marcelo

    ResponderExcluir
    Respostas
    1. Olá Marcelo! geralmente essa configuração de página está no web.xml tem uma tag welcome-file, é aqui que vc deve colocar a página que deseja, por padrão ela vem com o index configurado.

      Excluir
    2. Oi Andii! Consegui resolver! Agora entendi o conceito da tag "welcome-file"!! Estou iniciando os estudos no JSF, então estou com alguma dúvidas bem básicas!! rsrs

      Muito obrigado!

      Excluir
  17. Boa tarde andii, tudo bem?

    Fiz o exemplo de spring sem o BD, segui todos os passos, no entanto quando digito as credenciais, não sou redirecionado para a pagina que esta na pasta admin, simplesmente permaneço na mesma pagina de login e não da erro. O que posso esta fazendo de errado?

    Abs!

    Eudo Primo

    ResponderExcluir
    Respostas
    1. Eudo, verifique se seu form é form mesmo (html) e não h:form, e se vc tem um input como botão e não um h:commandButton, pois isso faz diferença

      Excluir
  18. Bom dia Anddi parabens pelo Blog.

    Estou tentando traduzir as mensagens de erro do spring como bad credentials, procurei
    na net mas até agora nada , voce pode ajudar ?

    Abraços.

    ResponderExcluir
    Respostas
    1. Bad Credentials quer dizer que o login e senha do usuário que você está usando para teste estão errados, as vezes você criptografou a senha no banco, mas não criptografou na configuração do Spring... ou vice e versa

      Excluir
    2. Ok andii.brunetta obrigado ! Agora me surgiu outro problema aqui como eu faço para customizar o formulário de login, no meu caso eu preciso passar mais um campo para ele autenticar não só login e senha, exemplo eu passo o login e a senha e o tipo de acesso do usuário para ele autenticar no context,
      voce ja viu algo assim, procurei na net e até agora nada.

      Abraços

      Excluir
  19. Bom dia andii!
    Cara, estou com um problema quando acrescento o a configuração do Spring Security no WEB-INF/web.xml

    Ele dá a seguinte mensagem:

    FAIL - Deployed application at context path /SigWeb but context failed to start
    C:\Users\CgSoft\Desktop\BlogDois\SigWeb\nbproject\build-impl.xml:1070: O módulo não foi implantado.
    Verifique o log do servidor para ver mais detalhes.
    FALHA NA CONSTRUÇÃO (tempo total: 6 segundos)

    Me ajude, por favor!

    ResponderExcluir
  20. Olá, parabens pelo post,
    pode me enviar o link para o projeto?? eu tentei baixar mas esta dando pagina nao encontrada.
    abraços,
    meu email é cristiandrj@gmail.com

    ResponderExcluir

Deixe seu comentário... ;)