Autenticação e Autorização: JAAS com JDBC Realm

Recentemente ministrei um curso sobre Segurança em aplicações JavaEE, onde, grande parte do treinamento é a utilização e configuração do JAAS (Java Authentication and Authorization Service) abordando Basic e Form Authentication usando os realms: File, JDBC e LDAP.

Aproveitando o embalo do treinamento e principalmente pela empolgação dos alunos da turma quando viram quão simples e fácil é usar o JAAS, vou mostrar a configuração de autenticação e autorização em uma aplicação JavaEE usando Basic Authentication e JDBC Realm.

O JAAS (Java Authentication and Authorization Service) é a API padrão do Java para controle de acesso e autorização em aplicações JavaEE. Com JAAS é possível autenticar e validar usuários e certificados, bem como controlar a possibilidade de acesso e/ou utilização de recursos na aplicação (arquivos, diretórios, URLs, conteúdo, etc). Resumidamente o JAAS cuida de:

  • Autenticação: deve verificar se um usuário é ele mesmo através de validação de senhas e/ou certificados.
  • Autorização: com base em um usuário identificado, as regras e controles de permissão e acesso serão aplicadas na aplicação/produto e seus recursos.

O exemplo a seguir foi feito usando o Eclipse, Glassfish_v2 e MySQL, e não há nenhum segredo obscuro, simplesmente criaremos um Dynamic Web Project e usaremos o console do Glassfish para criar o realm.

A infra-estrutura (servidor e banco de dados)

O ponto chave para a configuração do JDBC Realm é o banco de dados (tabelas de usuário e grupos). Criaremos o schema com o SQL a seguir. Basicamente são apenas duas tabelas: usertable e grouptable.

schema

A configuração do schema no realm será feita adiante e este exemplo está simples desta forma apenas para ficar o mais claro possível na configuração do realm. Certamente no seu caso você já terá suas tabelas de usuários e grupos e para usá-los basta fazer a configuração do realm de acordo com a sua realidade, alterando os valores das propriedades de configuração.

1
2
3
4
5
6
CREATE DATABASE learn_jaas;
USE learn_jaas;
CREATE TABLE usertable(userid VARCHAR(10) PRIMARY KEY, password VARCHAR(32) NOT NULL);
CREATE TABLE grouptable(userid VARCHAR(10), groupid VARCHAR(20) NOT NULL, PRIMARY KEY (userid, groupid));
ALTER TABLE grouptable ADD CONSTRAINT FK_USERID FOREIGN KEY(userid) REFERENCES usertable(userid);
COMMIT;

E agora vamos inserir alguns usuários e grupos para os testes, conforme o SQL abaixo. Os usuários serão: user, guest e admin e os grupos serão: users, e admins. Reparem que os grupos estão no plural, enquanto os usuários estão no singular. Este é um importante detalhe, a seguir, na configuração das roles usaremos os grupos para aplicar as regras e não os usuários.

1
2
3
4
5
6
INSERT INTO usertable VALUES ('user', 'user');
INSERT INTO grouptable VALUES ('user', 'users');
INSERT INTO usertable VALUES ('guest', 'guest');
INSERT INTO grouptable VALUES ('guest', 'users');
INSERT INTO usertable VALUES ('admin', 'admin');
INSERT INTO grouptable VALUES ('admin', 'admins');

Com o banco de dados pronto ainda precisamos de um passo antes do realm, precisaremos criar agora o Connection Pool e o DataSource para este banco de dados. O JNDI Name deste DataSource será usado na criação do realm, portanto, este passo é essencial para o funcionamento do exemplo. Entretanto, como o foco aqui não é a criação do DataSource, darei apenas o JNDI Name conforme usei no exemplo: learn-jaas-jdbc.

Agora só precisamos criar o realm, o nome usado para ele será learn-jaas-realm. No console do glassfish vamos em: Configuration >> Securiy >> Realm >> New. As propriedades deverão ser preenchidas de acordo com a imagem e a tabela abaixo (se o seu schema for diferente é só ajustar os valores):

glassfish-new-jdbc-realm

Propriedade Valor
jaas-context jdbcRealm
datasource-jndi learn-jaas-jdbc
user-table usertable
user-name-column userid
password-column password
group-table grouptable
group-name-column groupid
digest-algorithm none

A aplicação

Com a infra-estrutura necessária criada e funcionando, partiremos para a aplicação. Este será um ponto bem simples, a aplicação não terá nada além de 4 páginas JSP, web.xml e sun-web.xml. No Eclipse, criaremos um Dynamic Web Project usando o glassfish como servidor (JavaEE 5). As páginas citadas anteriormente ficarão dispostas conforme o projeto abaixo:

project_jaas

Em cada página index.jsp dos diretórios admin, public e users há um dizer assim: “Welcome Admin!”; “Welcome Guest!”; “Welcome User!”; respectivamente. E em /index.jsp há apenas os links para as demais páginas. Estes diretórios serão protegidos posteriormente.

O próximo passo consiste na criação das roles em nossa aplicação. Este costuma ser o ponto onde os iniciantes se confundem muito, por isso houve aquela distinção entre os nomes dos usuários e os grupos anteriormente. A criação das roles é livre, podemos criá-las como bem entendermos, o único ponto de atenção aqui é que a role é associada ao grupo, ou seja, uma role será aplicada para todos os usuários de um determinado grupo.

Então, em WEB-INF/sun-web.xml criaremos as roles associadas com os seus respectivos grupos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    <security-role-mapping>
        <role-name>user-role</role-name>
        <group-name>admins</group-name>
        <group-name>users</group-name>
    </security-role-mapping>

    <security-role-mapping>
        <role-name>admin-role</role-name>
        <group-name>admins</group-name>
    </security-role-mapping>
   
    <security-role-mapping>
        <role-name>guest-role</role-name>
        <group-name>admins</group-name>
        <group-name>users</group-name>
        <group-name>guests</group-name>
    </security-role-mapping>

As roles foram criadas com o sufixo -role apenas para ficar bem claro que são roles e não usuários ou grupos. Vejam que as roles estão associadas aos grupos e não aos usuários.

Este é um momento onde aplicaremos as regras hierárquicas e organizacionais da empresa, repare que o grupo admins aparece em todas as roles, afinal de contas, não queremos barrar ao administrador do sistema o acesso às páginas públicas ou de usuários, certo?

Próximo passo: Configuração do web.xml! A configuração da autenticação em si não passa de 4 linhas de xml, informaremos o tipo (método) de autenticação (pode ser none, basic, form e digest/certificate) e qual o nome do realm associado:

1
2
3
4
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>learn-jaas-realm</realm-name>
    </login-config>

O que precisaremos agora é, com base nas roles, criar as regras de acesso na aplicação. Elas podem ser feitas de várias formas e como podemos usar URL Pattern para definir onde a regra será aplicada, faremos isso por diretórios, combinando os diretórios com as roles (admin, user e guest):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>paginas admnistrativas</web-resource-name>
            <url-pattern>/admin/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>

        <auth-constraint>
            <role-name>admin-role</role-name>
        </auth-constraint>
    </security-constraint>
   
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>paginas para usuários</web-resource-name>
            <url-pattern>/users/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>

        <auth-constraint>
            <role-name>user-role</role-name>
        </auth-constraint>
    </security-constraint>
   
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>paginas para visitantes</web-resource-name>
            <url-pattern>/public/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>

        <auth-constraint>
            <role-name>guest-role</role-name>
        </auth-constraint>
    </security-constraint>

Repare que o código ficou grande, mas é muito repetitivo. Os pontos de atenção são o elemento url-pattern, onde é gerada a regra para onde as restrições serão aplicadas e, principalmente, o elemento role-name, onde definimos qual a role está associada com aquela regra. Mais uma vez repare que a regra de acesso é associada à role, já a role, por sua vez está associada com um grupo, que por sua vez está associado com um usuário.

Agora é só fazer o deploy e executar o projeto. Lembre-se das senhas (ou consulte no banco de dados) e faça o teste de acesso em cada página.

Para facilitar disponibilizei o projeto para download, basta clicar aqui. (por algum motivo alheio a minha compreensão o wordpress não deixou o nome do arquivo igual ao original, então, baste colocar um ponto antes do ‘tar’, deve ficar project-to-learn-jass.tar.gz)


46 comentários sobre “Autenticação e Autorização: JAAS com JDBC Realm

  1. Pingback: Jeveaux
  2. Parabéns… a dias procurando material. Não havia encontrado nada bem claro. Já havia entendido todo o funcionamento do JAAS porém não conseguia implementar nada. E o Logout ?

  3. É possível definir as roles em banco de dados tb?? para não precisar definir estáticamente em um arquivo xml, pois minha aplicação necessita possiblitar que o administrador possa definir regras de acesso para os usuários. Obrigado

  4. “…Sobre o logout, você pode fazer de maneira programática usando o método logout da classe LoginContext….”

    Também estou procurando algo sobre o logout. Mas toda referência que encontro é “session.invalidate()”.

    Mas como o LoginContext.logout() seria executado ?

    Alguns outros servidores como o Websphere adicionam cookies (ltpa) que inviabilizam o session.invalidate() como solução de logout.

    Abraço

  5. @sebastiao

    O session.invalidate() você poderá usar quando estiver, de alguma forma, fazendo o login/logout do usuário de forma manual, trabalhando com algum objeto na sessão. Para garantir é melhor você remover diretamente o objeto da sessão, desta forma você não correrá riscos.

    Já o logout mencionado, da LoginContext, você usará quando estiver trabalhando com autenticação através de JAAS.

  6. Jeveaux,

    Acho que não fui claro.

    Quando programei meu LoginModule, implementei também um método logout(). Quando faço “logout” na minha aplicação Web usando JAAS, a única coisa que faço é invalidar a sessão e invalidar os eventuais cookies ltpa do WAS.

    A questão é … quando o método logout() do meu LoginModule será disparado ? É necessário disparar ? Não fica lixo em memória dessa forma ?

    Eu deveria além de invalidar a sessão e cookies, de alguma forma disparar LoginContext.logout() ? Como ?

    Abraço

  7. Fala Jeveaux, blz?

    Ótimo post, “simprão de tudo”, por isso mesmo muito fácil de entender.

    Cara sempre que vejo um tutorial sobre jaas as permissões nas páginas ficam no web.xml, e se eu quiser manter isso no banco? entendeu minha dúvida?

    Outra coisa e pra controlar acesso á uma parte do código, como por exemplo um campo, ou um link?

  8. @Luiz e @Diego

    Sobre armazenar as roles em banco de dados, creio que não há nada ‘default’ pra isso. Dá pra usar um arquivo de policy com o controle de autorização, isso dá sim. Mas nada impede que implementemos um novo LoginModule para usar o banco de dados.

    @Sebastiao

    Você pode disparar o logou junto com o invalidate, não teria problemas. Mas é estranho, pois ao fazer o invalidate os cookies deveriam ser eliminados completamente e, com isso, a autenticação seria perdida.

    @Luiz

    Sobre a exibição de componentes na página de acordo com a permissão do usuário vai depender de que framework de view que você está usando, se não houver suporte a JAAS você vai ter que fazer ‘manualmente’.

  9. Pingback: claudionorjr
  10. Existe uma maneira de usarmos chaves primarias substitutas como idUsuario e idGrupo ao inves de usarmos os nomes (que no caso sao chaves naturaris) ?

    grande abraço

  11. Jeveaux, exelente conteúdo,parabéns e obrigado!
    Eu precisarei autenticar usando informações de mainframe. Existe Realm para “mainframe” ao invés de jbdcRealm?
    Outra coisa que notei é que no
    não fala se é application ou container.Sendo assim, por default esta autenticação não seria gerenciada pelo container,certo? me perguntei, como pode haver a configuração do realm no glasfish se não é autenticado por container?Fiquei confuso.

    Obrigado.

    1. Olá Leandro,

      Eu não conheço nenhum realm que possa ser utilizado diretamente com mainframe não =( Talvez (muito talvez), e ainda dependendo de qual mainframe for possa ser que existe algum conector ou algo do tipo para facilitar na comunicação, mas realmente eu não conheço praticamente nada nessa área, não posso ajudar muito.

  12. A documentação da Sun sobre JAAS é uma merda, já li tudo que você imagina e ainda não entendi qual é a dele. Entra as dúvidas, quando uso LoginModule com CallbackHandler ou quando devo fazer diretamente no AS como noseu exemplo?

  13. jeveaux,
    no final das contas descobri que não precisarei fazer um “realm” para mainframe. O que meu cliente faz é carregar um ldap com as informações do mainframe e então a aplicação obtém deste”repositório”.
    Ficou mains fácil,certo?rsss
    Grato pela atenção.

  14. Pelo que aprendi no decorrer de muito sofrimento, é responsabilidade do servidor fazer o logout no container J2EE.

    Cada um faz de uma maneira, no JBoss você precisa invalidar a sessão, no Websphere, dependendo da configuração, você precisa invalidar ou excluir um cookie ltap2, e por aí vai.

    Abraço,

  15. Olá,

    Estou estudando sobre o JAAS, tentei fazer o exemplo descrito acima mas não estou obtendo sucesso, configurei o pool de conexões e o realm como descrito, mas não ta funcionando. Quando eu tento acessar /admin/index.jsp aparece uma caixa de diálogo solicitando usuário e senha, e quando informo os dados a caixa de diálogo volta a aparecer, isso ocorre por 3 vezes, na terceira sou direcionado para uma tela de erro:

    HTTP Status 401 –

    ——————————————————————————–

    type Status report

    message

    description This request requires HTTP authentication ().

    Alguma ajuda?

  16. Olá, consegui resolver o problema era erro de conexão com o banco de dados, agora tenho outra dúvida, no request existe o método request.isUserInRole(String role) o qual verifica se o usuário logado está na role passada por parâmetro certo? tentei usar este método mas sempre me retorna false, vc sabe me dizer o porque disso? existe alguma configuração a mais que preciso fazer?

    obrigado

  17. Olá, consegui usar corretamente o método request.isUserInRole(String roleName), meu objetivo em usar tal método era restringir o acesso de um método em específico, ou seja, além de controlar o acesso por url pattern eu posso controlar acesso a métodos tbm. Para isso tive que adicionar no web.xml o seguinte:

    Adm
    admin-role

    para cada role criada em sun-web.xml.

    fica a dica para quem passar por aki

    flw

  18. Pingback: bunny_car
  19. Olá, parabéns pelo tutorial ;D
    mas ainda estou tendo algumas dificuldades na implementação do exemplo. Minha aplicação solicita o nome de usuário e senha e sempre volta para a mesma tela de autenticação :/
    Creio que estou com problemas na configuração do Connection Pool e do DataSource. Se possível, você poderia demonstrar como configurou os mesmos?

    Grato pela atenção!

    Carlos Henrique

  20. Estou com o mesmo problelam que do Geraldo e do Carlos, digito a senha e mas ele não autoriza.

    se eu coloco em um com não pede senha nenhuma, vai direto para a pagina.

  21. Ola amigo parabens pelo otimo tutorial.
    Consegui implementa-lo e esta tudo funcionando belezinha porem fiquei com uma duvida que voce não menciona no seu tutorial que seria o seguinte: Como faço para recuperar os dados da pessoa que logou no sistema por exemplo o login e senha do usuario logado para eu conseguir buscar por exemplo nome dele no banco de dados.

  22. Você não pode fazer essa consulta.

    O que você pode fazer é, após logado, fazer uma consulta a request.getUserPrincipal(), a senha você não pode recuperar.

    Ou se você estiver em uma camada EJB, EJBContext.getCallerPrincipal().

    Os grupos do usuário não estão acessíveis através de nenhuma consulta, o que você faz é chamadas do tipo request.isUserInRole(grupo) quando você quiser liberar ou restringir acesso a um usuário de um determinado grupo. O mesmo vale para EJB com EJBContext.isCallerInRole(grupo).

  23. Quem está com o problema de ficar sempre pedindo a senha mesmo digitando corretamente é bom dar uma checada se na propriedade digest-algorithm deixou como none porque se nao tiver ele vai tentar recuperar por md5 e como no banco não está em md5 nao vai dar certo.

  24. Talvez eu esteja cego, mas onde está a parte de login? Procurei a aplicação inteira e não entendi como o o usuário vai logar no site. Não tem um login.jsp nem nada parecido?

    1. Ok, já entendi que não existe login.jsp pq o tipo de autenticação é básica. Isso significa que o próprio navegador vai mostrar uma janela pedindo usuário e senha. Agora o meu problema é que eu já coloquei a senha “teste”, já defini o “digest-algorithm” como “none” e a m… não funciona. Alguém tem alguma dica de como posso debugar isso?

      1. Um dia inteiro de trabalho depois, descobri que o NOME DA COLUNA PRECISA SER O MESMO em ambas as tabelas. Fica a dica pro próximo coitado que resolver usar nomes diferentes. =)

  25. Meu amigo, parabéns pela iniciativa.
    Primeira coisa que reparei foi que o tabela de grupo tem uma chave estrangeira para a tabela de usuários e uma chave primária como nome de grupo. Eu quase comentei que isso estava errado pois assim um grupo poderia ter apenas um usuário, mas olhei, olhei de novo e vi que o desenho diz isso, mas o sql cria uma chave composta, então está certo, no entanto o desenho me deu uma grande má impressão.

    Segunda coisa é uma dúvida: Como um usuário autenticado vai participar de um grupo? Para ser mais específico como o containeir irá saber que usuário x pertence ao grupo y? Seria conforme o usuário André comentou, que os nomes da coluna tem que ser os mesmos? assim o algoritmo implementado pelo glassfish vai verificar isso internamente ao fazer os joins entre as duas tabelas? Putz, ter que saber como o algoritmo foi implementado para ter tal entendimento é osso, rsrss, o pessoal do glassfish poderia ter documentado isso melhor.

    Minha terceira duvida pode ser respondida pela segunda, pois se for dessa forma que o glassfish verifica isso, já sei como associar um usuário a um grupo dinâmicamente pela aplicação.

    No mais, muito bom tutorial. Agora tenho que procurar uma forma de fazê-lo no websphere, pois é o que estou utilizando.

    1. No Websphere eu programei um login module, precisa de um certo conhecimento do assunto, aí vc instala ele no WEB_INBOUND do websphere e o login passa a ser gerenciado por ele e pelos outros módulos de login do servidor.

      Este tutorial se aplica bem ao tomcat e ao JBoss, mas para o Websphere é bem diferente, embora o conceito base seja o mesmo, JAAS.

  26. Como obtenho uma instância do LoginContext para efetuar o logout?
    Existe mecanismo para injetar o LoginContext no EJB 3.1?
    É possível inclui algum exemplo sobre o logout JAAS!?

    Valeu, abraço!!

  27. Olá, configurei tudo direitinho, mas ele fica pedindo usuario e senha os quais eu já defini no banco de dados, e fiz o pool de conexão que está dando com sucesso quando faço o ping. Como resolver, ele até parece que não está lendo meu banco. Mas no ping ta tudo beleza.

  28. Muito bom o Post… Gostaria de saber mais uma coisa, como faço pra padronizar mensagens , caso o usuario tentar acessar uma url que não é permitida?

  29. Ótimo artigo!

    Tudo funcionou perfeitamente aqui, eu só tenho uma dúvida que é com relação ao campo “digest-algorithm”. Se for o caso de eu utilizar uma mescla de algoritmos, como ficaria o preenchimento deste campo?

    Grato.

  30. Boa tarde,
    Será que poderia me ajudar, segui o tutorial todo, mas na hora que informo login e senha, recebo o erro:
    AVISO: WEB9102: Web Login Failed: com.sun.enterprise.security.auth.login.common.LoginException: Login failed: Falha de log-in no arquivo para user.
    A Conexão com o banco está ok, o Realm foi configurado conforme o post. Estou utilizando o glassfish 3.1.2.

    Aguardo retorno agradecido.

Deixe uma resposta

O seu endereço de email não será publicado Campos obrigatórios são marcados *

Você pode usar estas tags e atributos de HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>