Introdução ao Apache Shiro
1. Visão geral
Neste artigo, veremosApache Shiro, uma estrutura de segurança Java versátil.
A estrutura é altamente personalizável e modular, pois oferece autenticação, autorização, criptografia e gerenciamento de sessões.
2. Dependência
Apache Shiro tem muitosmodules. No entanto, neste tutorial, usamos apenas o artefatoshiro-core.
Vamos adicioná-lo ao nossopom.xml:
org.apache.shiro
shiro-core
1.4.0
A última versão dos módulos Apache Shiro pode ser encontradaon Maven Central.
3. Configurando o Security Manager
OSecurityManager é a peça central da estrutura do Apache Shiro. Os aplicativos geralmente terão uma única instância em execução.
Neste tutorial, exploramos a estrutura em um ambiente de desktop. Para configurar a estrutura, precisamos criar um arquivoshiro.ini na pasta de recursos com o seguinte conteúdo:
[users]
user = password, admin
user2 = password2, editor
user3 = password3, author
[roles]
admin = *
editor = articles:*
author = articles:compose,articles:save
A seção[users] do arquivo de configuraçãoshiro.ini define as credenciais do usuário que são reconhecidas porSecurityManager. O formato é:principal (username) = password, role1, role2, …, role.
As funções e suas permissões associadas são declaradas na seção[roles]. A funçãoadmin recebe permissão e acesso a todas as partes do aplicativo. Isso é indicado pelo símbolo curinga(*).
A funçãoeditor tem todas as permissões associadas aarticles, enquanto a funçãoauthor só podecomposeesave um artigo.
OSecurityManager é usado para configurar a classeSecurityUtils. A partir deSecurityUtils, podemos obter o usuário atual interagindo com o sistema e realizar operações de autenticação e autorização.
Vamos usarIniRealm para carregar nossas definições de usuário e função do arquivoshiro.ini e, em seguida, usá-lo para configurar o objetoDefaultSecurityManager:
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);
SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();
Agora que temos umSecurityManager que está ciente das credenciais e funções do usuário definidas no arquivoshiro.ini, vamos prosseguir para a autenticação e autorização do usuário.
4. Autenticação
Nas terminologias do Apache Shiro, aSubject é qualquer entidade interagindo com o sistema. Pode ser um humano, um script ou um cliente REST.
ChamarSecurityUtils.getSubject() retorna uma instância doSubject atual, ou seja, ocurrentUser.
Agora que temos o objetocurrentUser, podemos realizar a autenticação nas credenciais fornecidas:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token
= new UsernamePasswordToken("user", "password");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.error("Username Not Found!", uae);
} catch (IncorrectCredentialsException ice) {
log.error("Invalid Credentials!", ice);
} catch (LockedAccountException lae) {
log.error("Your Account is Locked!", lae);
} catch (AuthenticationException ae) {
log.error("Unexpected Error!", ae);
}
}
Primeiro, verificamos se o usuário atual ainda não foi autenticado. Em seguida, criamos um token de autenticação com o principal(username)e credencial(password). do usuário
Em seguida, tentamos fazer login com o token. Se as credenciais fornecidas estiverem corretas, tudo ficará bem.
Existem exceções diferentes para casos diferentes. Também é possível lançar uma exceção personalizada que melhor se adapte aos requisitos do aplicativo. Isso pode ser feito criando uma subclasse da classeAccountException.
5. Autorização
A autenticação está tentando validar a identidade de um usuário enquanto a autorização está tentando controlar o acesso a determinados recursos no sistema.
Lembre-se de que atribuímos uma ou mais funções a cada usuário que criamos no arquivoshiro.ini. Além disso, na seção de funções, definimos permissões ou níveis de acesso diferentes para cada função.
Agora vamos ver como podemos usar isso em nosso aplicativo para aplicar o controle de acesso do usuário.
No arquivoshiro.ini, damos ao administrador acesso total a todas as partes do sistema.
O editor tem acesso total a todos os recursos / operações referentes aarticles, e um autor está restrito a apenas compor e salvararticles.
Vamos dar as boas-vindas ao usuário atual com base na função:
if (currentUser.hasRole("admin")) {
log.info("Welcome Admin");
} else if(currentUser.hasRole("editor")) {
log.info("Welcome, Editor!");
} else if(currentUser.hasRole("author")) {
log.info("Welcome, Author");
} else {
log.info("Welcome, Guest");
}
Agora, vamos ver o que o usuário atual tem permissão para fazer no sistema:
if(currentUser.isPermitted("articles:compose")) {
log.info("You can compose an article");
} else {
log.info("You are not permitted to compose an article!");
}
if(currentUser.isPermitted("articles:save")) {
log.info("You can save articles");
} else {
log.info("You can not save articles");
}
if(currentUser.isPermitted("articles:publish")) {
log.info("You can publish articles");
} else {
log.info("You can not publish articles");
}
6. Configuração de Reino
Em aplicativos reais, precisaremos de uma maneira de obter as credenciais do usuário de um banco de dados, em vez do arquivoshiro.ini. É aqui que o conceito de Reino entra em jogo.
Na terminologia do Apache Shiro, aRealm é um DAO que aponta para um armazenamento de credenciais de usuário necessárias para autenticação e autorização.
Para criar um domínio, precisamos apenas implementar a interfaceRealm. Isso pode ser entediante; no entanto, a estrutura vem com implementações padrão das quais podemos subclassificar. Uma dessas implementações éJdbcRealm.
Criamos uma implementação de domínio customizada que estende a classeJdbcRealme substitui os seguintes métodos:doGetAuthenticationInfo(),doGetAuthorizationInfo(),getRoleNamesForUser()egetPermissions().
Vamos criar um reino criando uma subclasse da classeJdbcRealm:
public class MyCustomRealm extends JdbcRealm {
//...
}
Por uma questão de simplicidade, usamosjava.util.Map para simular um banco de dados:
private Map credentials = new HashMap<>();
private Map> roles = new HashMap<>();
private Map> perm = new HashMap<>();
{
credentials.put("user", "password");
credentials.put("user2", "password2");
credentials.put("user3", "password3");
roles.put("user", new HashSet<>(Arrays.asList("admin")));
roles.put("user2", new HashSet<>(Arrays.asList("editor")));
roles.put("user3", new HashSet<>(Arrays.asList("author")));
perm.put("admin", new HashSet<>(Arrays.asList("*")));
perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
perm.put("author",
new HashSet<>(Arrays.asList("articles:compose",
"articles:save")));
}
Vamos prosseguir e substituir odoGetAuthenticationInfo():
protected AuthenticationInfo
doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken uToken = (UsernamePasswordToken) token;
if(uToken.getUsername() == null
|| uToken.getUsername().isEmpty()
|| !credentials.containsKey(uToken.getUsername())) {
throw new UnknownAccountException("username not found!");
}
return new SimpleAuthenticationInfo(
uToken.getUsername(),
credentials.get(uToken.getUsername()),
getName());
}
Primeiro lançamos oAuthenticationToken fornecido paraUsernamePasswordToken. DeuToken, extraímos o nome de usuário (uToken.getUsername()) e o usamos para obter as credenciais do usuário (senha) do banco de dados.
Se nenhum registro for encontrado - lançamos umUnknownAccountException, caso contrário, usamos a credencial e o nome de usuário para construir um objetoSimpleAuthenticatioInfo que é retornado do método.
Se a credencial do usuário for hash com um salt, precisamos retornar umSimpleAuthenticationInfo com o salt associado:
return new SimpleAuthenticationInfo(
uToken.getUsername(),
credentials.get(uToken.getUsername()),
ByteSource.Util.bytes("salt"),
getName()
);
Também precisamos substituirdoGetAuthorizationInfo(), bem comogetRoleNamesForUser()egetPermissions().
Finalmente, vamos conectar o domínio personalizado emsecurityManager. Tudo o que precisamos fazer é substituir oIniRealm acima por nosso domínio personalizado e passá-lo para o construtor deDefaultSecurityManager:
Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);
Todas as outras partes do código são as mesmas de antes. Isso é tudo de que precisamos para configurar osecurityManager com um realm personalizado corretamente.
Agora a pergunta é: como a estrutura corresponde às credenciais?
Por padrão, oJdbcRealm usa oSimpleCredentialsMatcher, que apenas verifica a igualdade comparando as credenciais emAuthenticationTokeneAuthenticationInfo.
Se fizermos hash de nossas senhas, precisamos informar ao framework para usar umHashedCredentialsMatcher. As configurações INI para domínios com senhas em hash podem ser encontradashere.
7. Sair
Agora que autenticamos o usuário, é hora de implementar o logout. Isso é feito simplesmente chamando um único método - o que invalida a sessão do usuário e o desconecta:
currentUser.logout();
8. Gerenciamento de sessões
A estrutura vem naturalmente com seu sistema de gerenciamento de sessões. Se usado em um ambiente da web, o padrão é a implementaçãoHttpSession.
Para um aplicativo independente, ele usa seu sistema de gerenciamento de sessões corporativas. O benefício é que, mesmo em um ambiente de desktop, você pode usar um objeto de sessão como faria em um ambiente típico da Web.
Vamos dar uma olhada em um exemplo rápido e interagir com a sessão do usuário atual:
Session session = currentUser.getSession();
session.setAttribute("key", "value");
String value = (String) session.getAttribute("key");
if (value.equals("value")) {
log.info("Retrieved the correct value! [" + value + "]");
}
9. Shiro para um aplicativo da Web com Spring
Até agora, descrevemos a estrutura básica do Apache Shiro e a implementamos em um ambiente de desktop. Vamos prosseguir integrando a estrutura em um aplicativo Spring Boot.
Observe que o foco principal aqui é Shiro, não o aplicativo Spring - vamos usar isso apenas para alimentar um aplicativo de exemplo simples.
9.1. Dependências
Primeiro, precisamos adicionar a dependência pai Spring Boot ao nossopom.xml:
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
Em seguida, temos que adicionar as seguintes dependências ao mesmo arquivopom.xml:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-freemarker
org.apache.shiro
shiro-spring-boot-web-starter
${apache-shiro-core-version}
9.2. Configuração
Adicionar a dependênciashiro-spring-boot-web-starter ao nossopom.xml configurará por padrão alguns recursos do aplicativo Apache Shiro, comoSecurityManager.
No entanto, ainda precisamos configurar os filtros de segurançaRealme Shiro. Usaremos o mesmo domínio personalizado definido acima.
E assim, na classe principal onde o aplicativo Spring Boot é executado, vamos adicionar as seguintes definições deBean:
@Bean
public Realm realm() {
return new MyCustomRealm();
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition filter
= new DefaultShiroFilterChainDefinition();
filter.addPathDefinition("/secure", "authc");
filter.addPathDefinition("/**", "anon");
return filter;
}
NoShiroFilterChainDefinition, aplicamos o filtroauthc ao caminho/securee aplicamos o filtroanon em outros caminhos usando o padrão Ant.
Os filtrosauthc eanon vêm por padrão para aplicativos da web. Outros filtros padrão podem ser encontradoshere.
Se não definimos o beanRealm,ShiroAutoConfiguration irá, por padrão, fornecer uma implementaçãoIniRealm que espera encontrar um arquivoshiro.ini emsrc/main/resources ousrc/main/resources/META-INF.
Se não definirmos um beanShiroFilterChainDefinition, a estrutura protege todos os caminhos e define a URL de login comologin.jsp.
Podemos alterar esse URL de login padrão e outros padrões adicionando as seguintes entradas ao nossoapplication.properties:
shiro.loginUrl = /login
shiro.successUrl = /secure
shiro.unauthorizedUrl = /login
Agora que o filtroauthc foi aplicado a/secure, todas as solicitações para essa rota exigirão uma autenticação de formulário.
9.3. Autenticação e autorização
Vamos criar umShiroSpringController com os seguintes mapeamentos de caminho:/index,/login, /logoute/secure.
O métodologin() é onde implementamos a autenticação do usuário real conforme descrito acima. Se a autenticação for bem-sucedida, o usuário será redirecionado para a página segura:
Subject subject = SecurityUtils.getSubject();
if(!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(
cred.getUsername(), cred.getPassword(), cred.isRememberMe());
try {
subject.login(token);
} catch (AuthenticationException ae) {
ae.printStackTrace();
attr.addFlashAttribute("error", "Invalid Credentials");
return "redirect:/login";
}
}
return "redirect:/secure";
E agora, na implementaçãosecure(), ocurrentUser foi obtido invocando oSecurityUtils.getSubject().. A função e as permissões do usuário são passadas para a página segura, assim como o principal do usuário:
Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";
if(currentUser.hasRole("admin")) {
role = role + "You are an Admin";
} else if(currentUser.hasRole("editor")) {
role = role + "You are an Editor";
} else if(currentUser.hasRole("author")) {
role = role + "You are an Author";
}
if(currentUser.isPermitted("articles:compose")) {
permission = permission + "You can compose an article, ";
} else {
permission = permission + "You are not permitted to compose an article!, ";
}
if(currentUser.isPermitted("articles:save")) {
permission = permission + "You can save articles, ";
} else {
permission = permission + "\nYou can not save articles, ";
}
if(currentUser.isPermitted("articles:publish")) {
permission = permission + "\nYou can publish articles";
} else {
permission = permission + "\nYou can not publish articles";
}
modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);
return "secure";
And we’re done. É assim que podemos integrar o Apache Shiro em um aplicativo Spring Boot.
Além disso, observe que a estrutura ofereceannotations adicionais que podem ser usados junto com as definições da cadeia de filtros para proteger nosso aplicativo.
10. Integração JEE
11. Conclusão
Neste tutorial, vimos os mecanismos de autenticação e autorização do Apache Shiro. Também nos concentramos em como definir um domínio personalizado e conectá-lo aoSecurityManager.
Como sempre, o código-fonte completo está disponívelover on GitHub.