Vivendo e Aprendendo

Experiência prática na administração de Banco de Dados

Apache Shiro Parte 1 - básico

by Gilberto C. Andrade on 07 abril 2011

Tagged as: Apache-Shiro, Application-Server, Autenticação, Autorização, Java, Apache-Maven, Proteção, Recursos-de-Segurança, Traduções,

Este post é uma tradução livre do artigo Apache Shiro Part 1 - Basics, publicado por Meri em 27 de março 2011

ATUALIZAÇÃO: duas novas seções adicionadas - tratamento de erros e hashing de senhas.

Apache Shiro inicialmente chamado JSecurity, é um framework de segurança desenvolvido em java. Foi aceito e tornou-se um projeto Apache de nível superior em 2010. Tem como objetivo ser poderoso e fácil de usar. O projeto está em constante desenvolvimento e com listas de e-mails ativas para usuários e desenvolvedores. Áreas mais importantes estão documentadas em sua página web. No entanto, existem algumas lacunas na documentação. Não é possível aprender a usar o máximo dos recursos do Shiro somente com a documentação. Felizmente, o código é bem documentado e onde o testei, ela foi de fácil leitura.

Principais recursos do projeto Shiro são:

  • autenticação,

  • autorização,

  • criptografia,

  • gerenciamento de sessão.

Neste artigo tentaremos demonstrar vários recursos do Shiro. Vamos começar com uma simples aplicação sem segurança e então adicionaremos recursos de segurança. Todo código está disponível no projeto SimpleShiroSecuredApplication hospedado no Github.

Aplicação sem Segurança

O código da aplicação sem segurança está localizado no ramo(branch) unsecured_application. A aplicação representa um sistema interno para uma companhia fictícia. A companhia tem quatro departamentos:

  • administradores(administrators),

  • reparadores(repairmen),

  • cientistas(scientists),

  • vendedores(sales).

Cada departamento tem sua própria página. Cada página contém botões que serão usados por usuários em sua atividade. Quando o usuário pressiona o botão, o trabalho é realizado. Por exemplo, qualquer reparador pode acessar a página reparadores(repairmen) e pressionar o botão “Reparar Refrigerador(Repair Refrigerator)”. O botão repara refrigerador e mostra mensagem de sucesso.

Cada usuário tem sua própria página de gerenciamento de conta. Esta página de gerenciamento contém informações privadas do usuário. Como a aplicação sem segurança não tem usuários ainda, a página de gerenciamento de conta não faz nada. Adicionalmente, há uma página que contém todas funções da aplicação. Tudo que alguém pode fazer é possível ser feito nesta página.

Qualquer pessoa pode fazer qualquer coisa e ver todas as páginas. A aplicação exemplo é executada na classe de teste RunWaitTest. Não é uma boa prática usar teste de unidade desta forma, mas isso não é importante agora. Se você executar a classe a aplicação estará disponível no endereço (url) http://localhost:9180/simpleshirosecuredapplication/.

Adicionando Autenticação

Primeiro, temos que verificar a identidade do usuário. A mais fácil e padronizada forma de autenticação é feita por senha e nome usuário. Usuário preenche o seu nome de usuário e senha e o sistema verifica se os valores fornecidos combinam com alguma conta de usuário.

Para aplicações mais simples, é suficiente armazenar nome e senha de usuários em arquivos de texto puro. Em um cenário mais realista, o nome e senha são armazenados em um mecanismo de persistente or a verificação é feita por outro sistema como ldap ou active directory. Shiro suporta todos os métodos de autenticação mencionados. Se os recursos de autenticação pré-existentes não são suficientes, é possível estender o framework com sua própria implementação de verificação.

Neste capítulo, iremos adicionar autenticação baseada em nome de usuário e senha na aplicação. Nome de usuário e senha serão armazenados no arquivo de inicialização do Shiro, o qual é texto e estático.

Novos requisitos: É possível a entrada e saída de usuários(log in/out). A aplicação será acessível somente para usuários registrados. Usuário com autenticação sem erros é redirecionado para sua própria página de gerenciamento de conta. Todas as páginas e funções da aplicação serão acessíveis para qualquer usuário autenticado.

Passos necessários:

  • adicionar Apache Shiro,

  • criar página de autenticação(log in),

  • configurar usuários e senhas,

  • criar página de saída(log out).

Adicionar Apache Shiro

Shiro é integrado à aplicação web através de filtros servlet. Um filtro intercepta requisições e respostas antes do servlet e executa todas as tarefas necessárias (como a identificação do usuário atualmente autenticado, anexar o usuário ao thread corrente, …​). Filtros Shiro pré-definidos fornecem recursos de segurança básicos, como:

  • força a autenticação do usuário(log in),

  • força ssl,

  • verificação dos direitos de acesso à página.

Se você quiser aprender mais sobre filtros Shiro pré-definidos, um bom lugar para começar é a enumeração DefaultFilter. Ela lista todos os filtros Shiro pré-definidos disponíveis. Se estes não forem suficientes para suas necessidades, você pode criar um personalizado.

Usaremos o filtro altamente configurável IniShiroFilter. Ele lê a configuração Shiro a partir do arquivo ini e inicializa o framework de segurança. Ele não executa qualquer verificação de segurança. Verificação de segurança, autenticação de usuário, verificação de protocolo, etc., são todos transferidos(delegados) tanto para o filtro Shiro pré-definido quanto para o filtro personalizado(outra implementação). O filtro IniShiroFilter apenas os inicializa.

A configuração ini é descrita na documentação e no javadoc. O arquivo de configuração ini tem quatro seções:

  • Seção [main] contém a inicialização do Shiro. Filtros e objetos personalizados são configurados aqui.

  • Seção [users] define usuários, senhas e perfis de usuário(roles).

  • Seção [roles] associa perfis(roles) com permissões.

  • Seção [urls] especifica direitos de acesso às páginas da aplicação (urls). É feito ligando filtro tanto pré-definido quanto personalizado às urls

Adicione Apache Shiro como dependência ao arquivo pom.xml:

<properties>
    <shiro.version>1.1.0</shiro.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>${shiro.version}</version>
    </dependency>
</dependencies>

Crie o arquivo Shiro.ini e o coloque no classpath. Configure o arquivo web.xml para chamar o filtro IniShiroFilter antes de cada requisição:

<filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
    <init-param>
        <param-name>configPath</param-name>
        <param-value>classpath:Shiro.ini</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
Criar Página de Autenticação(Log in)

A página de autenticação é uma simples página html contendo um botão submissão, nome de usuário e senha. A funcionalidade de autenticação é tratada pelo filtro Shiro authc pré-definido. O filtro autch permite acesso à página (url) somente a usuários autenticados. Se o usuário não está autenticado, o filtro o redirecionará para a página de autenticação (log in).

Crie a página login.jsp:

<form name="loginform" action="" method="post">
    <table align="left" border="0" cellspacing="0" cellpadding="3">
        <tr>
            <td>Username:</td>
            <td><input type="text" name="user" maxlength="30"></td>
        </tr>
        <tr>
            <td>Password:</td>
            <td><input type="password" name="pass" maxlength="30"></td>
        </tr>
        <tr>
            <td colspan="2" align="left"><input type="checkbox" name="remember"><font size="2">Remember Me</font></td>
        </tr>
        <tr>
            <td colspan="2" align="right"><input type="submit" name="submit" value="Login"></td>
        </tr>
    </table>
</form>

Habilite o filtro authc para todas as páginas da aplicação:

[main]
# specify login page
authc.loginUrl = /simpleshirosecuredapplication/account/login.jsp
 
# name of request parameter with username; if not present filter assumes 'username'
authc.usernameParam = user
# name of request parameter with password; if not present filter assumes 'password'
authc.passwordParam = pass
# does the user wish to be remembered?; if not present filter assumes 'rememberMe'
authc.rememberMeParam = remember
 
# redirect after successful login
authc.successUrl  = /simpleshirosecuredapplication/account/personalaccountpage.jsp
 
[urls]
# enable authc filter for all application pages
/simpleshirosecuredapplication/**=authc

Atualização: Shiro automaticamente executa a equiparação(comparação) de caminho(path matching) relativa ao contexto. Como a aplicação SimpleShiroSecuredApplication não tem o caminho do contexto definido, caminhos completos no arquivo Shiro.ini são necessários. No entanto, se o contexto da aplicação fosse definido para /simpleshirosecuredapplication, então os caminhos poderiam ser relativos, por exemplo, /**=authc ou /account/personalaccountpage.jsp.

Como não é seguro enviar nome de usuário e senha pela rede, devemos forçar autenticações com o protocolo ssl. O filtro ssl faz exatamente isso. Ele tem um parâmetro opcional: número da porta ssl. Se o parâmetro porta(port) for omitido, ele usa a porta ssl pré-definida 443.

Antes de configurar o protocolo ssl no Shiro, precisamos habilitá-lo no servidor web. Como fazer isso, depende do servidor web. Iremos demonstrar como habilitá-lo no Jetty. Primeiro, crie um keystore com o certificado auto assinado:

keytool -genkey -keyalg RSA -alias jetty -keystore keystore -storepass secret -validity 360 -keysize 2048

Responda todas questões e no final pressione ENTER, assim o keystore senha e a chave senha serão o mesmo.

Segundo, adicione o keystore ao projeto e configure o servidor Jetty para usar o protocolo ssl. O código java está disponível na classe AbstractContainerTest.

Agora é possível configurar o filtro ssl no arquivo Shiro.ini:

[urls]
# force ssl for login page
/simpleshirosecuredapplication/account/login.jsp=ssl[8443],authc
# enable authc filter for the all application pages; as Shiro reads urls from up to down, must be last
/simpleshirosecuredapplication/**=authc

Configurar Usuários e Senhas

A aplicação SimpleShiroSecuredApplication estará agora disponível somente para usuários autenticados. Assim, é necessário adicionar alguns usuários de forma que algumas pessoas tenham acesso à aplicação. A configuração é feita na seção [users] do arquivo Shiro.ini. O formato das entradas da seção é:

username = password, roleName1, roleName2, ..., roleNameN

A seguinte seção cria sete usuários, todos com a mesma senha ‘heslo’:

[users]
administrator=heslo,Administrator
friendlyrepairmen=heslo,repairmen
unfriendlyrepairmen=heslo,repairmen
mathematician=heslo,scientist
physicien=heslo,scientist
productsales=heslo,sales
servicessales=heslo,sales

Agora é possível ser autenticado e ter acesso à aplicação. No entanto, nenhuma mensagem de erro razoável é mostrada se o usuário comete um erro. Além disso, as senhas são armazenadas em arquivo texto.

Tratamento de Erro

Se o usuário comete um erro ao fazer a autenticação (log in), o Shiro o redireciona de volta para a página de login. A página parece exatamente a mesma de antes, o que pode confundir o usuário.

Novo requerimento: Mostrar mensagem de erro após cada tentativa de autenticação(log in) sem êxito.

Toda vez que ocorrer um erro de autenticação, uma exceção é lançada. Por definição, o filtro de autenticação captura a exceção e armazena nome da classe (exceção) em um parâmentro da requisição (request). Como desejamos personalizar os dados enviados à página, teremos que estender o filtro pré-definido FormAuthenticationFilter e sobrescrever o método setFailureAttribute:

@Override
protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
  String message = ae.getMessage();
  request.setAttribute(getFailureKeyAttribute(), message);
}

Substitua o filtro pré-definido FormAuthenticationFilter por VerboseFormAuthenticationFilter e o configure para usar o atributo de requisição (request) ‘simpleShiroApplicationLoginFailure’ para armazenar a informação do erro:

[main]
# replace form authentication filter with verbose filter
authc = org.meri.simpleshirosecuredapplication.servlet.VerboseFormAuthenticationFilter
# request parameter with login error information; if not present filter assumes 'shiroLoginFailure'
authc.failureKeyAttribute=simpleShiroApplicationLoginFailure

Mostrar o erro na página login.jsp:

<%
  String errorDescription = (String) request.getAttribute("simpleShiroApplicationLoginFailure");
  if (errorDescription!=null) {
%>
Login attempt was unsuccessful: <%=errorDescription%>
<%
  }
%>

Cuidado: uma aplicação real não deve apresentar muitas informações de erro de autenticação. Uma mensagem como “tentativa de logon sem sucesso.”, com nenhuma informação adicional é geralmente suficiente.

Hashing de Senhas

Na versão atual da aplicação, todas as senhas estão, ainda, em texto puro. É melhor armazenar e comparar somente o hashing da senha.

Objetos responsáveis por autenticação são chamados de realms. Por padrão, Shiro usa o IniRealm ]com comparador de senha plugável para comparar senhas. Iremos substituir as senhas no ini por suas correspondentes hasheadas com SHA-256 e configurar o IniRealm para usar este comparador de senhas.

Gerar o hash SHA-256 da senha:

import org.apache.shiro.crypto.hash.Sha256Hash;
 
public static void main(String[] args) {
    Sha256Hash sha256Hash = new Sha256Hash("heslo");
    System.out.println(sha256Hash.toHex());
}

Configurar o Shiro para comparar senha trasnformada(hashing) ao invés da própria senha:

[main]
# define matcher matching hashes instead of passwords
sha256Matcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
sha256Matcher.hashAlgorithmName=SHA-256
 
# enable matcher in iniRealm (object responsible for authentication)
iniRealm.credentialsMatcher = $sha256Matcher

Substituir as senhas dos usuários por senhas trasnformadas(hashing):

[users]
administrator=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, Administrator
friendlyrepairmen=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, repairmen
unfriendlyrepairmen=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, repairmen
mathematician=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, scientist
physicien=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005,  scientist
productsales=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005,        sales
servicessales=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005,  sales

Nota: não é possível especificar SALT no arquivo de configuração ini.

Criar Página de Saída(Log out)

Qualque aplicação que tenha recursos de autenticação deveria também ter o recurso de saída. Terminar uma sessão corrente com Shiro é fácil, use o comando:

//acquire currently logged user and log him out
SecurityUtils.getSubject().logout();

A página de saída(Log out) então fica assim:

<%@ page import="org.apache.shiro.SecurityUtils" %>
<% SecurityUtils.getSubject().logout();%>
You have succesfully logged out.

Adicionar Autorização

Concluiremos esta primeira parte adicionando autorização à aplicação. Começamos limitando o acesso às páginas aos usuários. Nenhum usuário deveria ser capaz de ver páginas de outros departamentos. Fornecendo assim, somente segurança parcial ao projeto, pois o usuário ainda é capaz de usar a página com “todas as funções da aplicação” ou editar o endereço (url) no navegador para realizar qualquer ação. Nós a chamaremos de autorização em nível de página.

Então, limitaremos a habilidade dos usuários para realizar ações próprias. Mesmo que abra a página com “todas as funções da aplicação” ou edite o endereço (url) no navegador, ele poderá realizar somente funções específicas de seu departamento. Nós a chamaremos de autorização em nível de função.

Novos requisitos: o usuário não é capaz de ver páginas de departamentos que não pertence. O usuário é capaz de realizar somente funções de seu departamento. Uma exceção a essa regra é o administrador, que pode realizar funções administrativas e de reparação.

Página de Autorização

Autorização em nível de página é feita com filtro de perfis(roles). Parâmetro parte do filtro pode conter qualquer número de perfis. Usuário autenticado pode acessar a página somente se ele tem todas os perfis fornecidos.

Como de costume, o filtro de perfis(roles) é configurado no arquivo Shiro.ini:

[urls]
# force ssl for login page
/simpleshirosecuredapplication/account/login.jsp=ssl[8443],authc
 
# only users with some roles are allowed to use role-specific pages
/simpleshirosecuredapplication/repairmen/**=authc, roles[repairman]
/simpleshirosecuredapplication/sales/**=authc, roles[sales]
/simpleshirosecuredapplication/scientists/**=authc, roles[scientist]
/simpleshirosecuredapplication/adminarea/**=authc, roles[Administrator]
 
# enable authc filter for the all application pages; as Shiro reads urls from up to down, must be last
/simpleshirosecuredapplication/**=authc

Teste se a segurança funciona: entre como um usuário de vendas, clique home, clique no link ‘repairmen page’. Você verá um erro feio.

Nós terminamos a página de autorização e substituimos o erro redirecionando-o para uma página de erro. Os filtros pré-definidos do Shiro possuem a propriedade unauthorizedUrl. Em caso de acesso não autorizado, o filtro redirecionará o usuário para um endereço (url) específico.

[main]
# redirect to an error page if user does not have access rights
roles.unauthorizedUrl = /simpleshirosecuredapplication/account/accessdenied.jsp

accessdenied.jsp:

<body>
Sorry, you do not have access rights to that area.
</body>
Autorização de Funções

Todas as páginas departamentais estão protegidas agora. No entanto, qualquer usuário pode ainda realizar qualquer função na página com “todas as funções da aplicação”. Além disso, qualquer usuário autenticado pode editar endereço (url) e assim fazer qualquer ação. Por exemplo, se você entrar como vendedor e colocar https://localhost:8443/simpleshirosecuredapplication/masterservlet?action=MANAGE_REPAIRMEN na url, a aplicação irá realizar a função gerenciar reparadores também (e então irá dispará a exceção: null point, mas a violação de segurança já foi feita). Atribuimos uma úncia permissão para cada função. Elas estão divididas em grupos:

  • todas as permissões estão no grupo “functions”,

  • todas as permissões administrativas estão no grupo “manage”,

  • todas as permissões reparação estão no grupo “repair”,

  • todas as permissões venda estão no grupo “sale”,

  • todas as permissões ciência estão no grupo “science”.

Shiro suporta permissões de múltiplos níveis representadas como strings. Níveis são separados com o símbolo ‘:’. p.e. “functions:manage:repairmen” tem três níveis: “functions”, “manage” e “repairman”. Permissões de múltiplos níveis permitem facilmente o agrupamento de permissões. Por exemplo, o grupo science pertence ao grupo functions e contém três permissões:

  • functions:science:research,

  • functions:science:writearticle,

  • functions:science:preparetalk.

A classe Ações verifica as permissões do usuário autenticado(log in) antes de fazer seu trabalho:

public String doIt() {
    String neededPermission = getNeededPermission();
    // acquire logged user and check permission
    if (SecurityUtils.getSubject().isPermitted(neededPermission))
        return "Function " + getName() + " run succesfully.";
 
    throw new UnauthorizedException("Logged user does not have " + neededPermission + " permission");
}

NOTA: Outro modo de se alcançar o mesmo objetivo é através de anotações.

O servlet PerformFunctionAndGoBackServlet captura exceções de autorização e as converte em mensagem de erro:

private String performAction(String actionName) {
    try {
        Actions action = findAction(actionName);
        String result = action == null ? null : action.doIt();
        log.debug("Performed function with result: " + result);
        return result;
    } catch (ShiroException ex) {
        log.debug("Function failed with " + ex.getMessage() + " message.");
        return "Error: " + ex.getMessage();
    }
}

Finalmente, precisamos configurar as permissões para os perfis no arquivo Shiro.ini. Shiro suporta curingas para permissões de múltiplo nível. Assim, não temos que especificar cada permissão departamental em separado:

[roles]
# members of departments should be able to perform all departmental functions
sales=functions:sale:*
scientist=functions:science:*
repairman=functions:repair:*
 
# administrators are able to do all management functions and repair functions
Administrator=functions:manage:*,functions:repair:*

Você pode agora acessar a página “todas as funções da aplicação” e testar as funções. Se um usuário autenticado não tiver a permissão requerida, uma mensagem de erro aparecerá no topo da página. Além disso, se você fizer a autenticação (log in) como vendedor e tentar hackear https://localhost:8443/simpleshirosecuredapplication/masterservlet?action=MANAGE_REPAIRMEN , você verá uma mensagem de erro no console (em vez de uma mensagem de sucesso).

Fim

A aplicação final está disponível no ramo(branch) static_authentication_and_authorization hospedado no Github.

Na segunda parte iremos criar um realm personalizado e mover usuários, senhas, perfis(roles) e permissões do arquivo ini para um banco de dados.

comments powered by Disqus