Introducción a Apache Shiro

1. Información general

En este artículo, veremos Apache Shiro, un marco de seguridad de Java versátil.

El marco es altamente personalizable y modular, ya que ofrece autenticación, autorización, criptografía y administración de sesiones.

2. Dependencia

Apache Shiro tiene muchos módulos. Sin embargo, en este tutorial, solo usamos el artefacto shiro-core .

Agreguémoslo a nuestro pom.xml :

 org.apache.shiro shiro-core 1.4.0 

La última versión de los módulos de Apache Shiro se puede encontrar en Maven Central.

3. Configuración de Security Manager

El SecurityManager es la pieza central del marco de Apache Shiro. Las aplicaciones generalmente tendrán una sola instancia en ejecución.

En este tutorial, exploramos el marco en un entorno de escritorio. Para configurar el marco, necesitamos crear un archivo shiro.ini en la carpeta de recursos con el siguiente contenido:

[users] user = password, admin user2 = password2, editor user3 = password3, author [roles] admin = * editor = articles:* author = articles:compose,articles:save

La sección [usuarios] del archivo de configuración shiro.ini define las credenciales de usuario que reconoce SecurityManager . El formato es: p rincipal (nombre de usuario) = contraseña, rol1, rol2,…, rol .

Los roles y sus permisos asociados se declaran en la sección [roles] . El rol de administrador tiene permiso y acceso a todas las partes de la aplicación. Esto se indica mediante el símbolo comodín (*) .

La función de editor tiene todos los permisos asociados con los artículos, mientras que la función de autor solo puede redactar y guardar un artículo.

El SecurityManager se utiliza para configurar el SecurityUtils clase. Desde los SecurityUtils podemos obtener el usuario actual interactuando con el sistema y realizar operaciones de autenticación y autorización.

Vamos a usar la IniRealm para cargar nuestras definiciones de usuarios y roles de la shiro.ini archivo y luego utilizarlo para configurar el DefaultSecurityManager objeto:

IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); SecurityManager securityManager = new DefaultSecurityManager(iniRealm); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject();

Ahora que tenemos un SecurityManager que conoce las credenciales de usuario y los roles definidos en el archivo shiro.ini , procedamos a la autenticación y autorización del usuario.

4. Autenticación

En la terminología de Apache Shiro, un sujeto es cualquier entidad que interactúa con el sistema. Puede ser un humano, un script o un cliente REST.

Llamar a SecurityUtils.getSubject () devuelve una instancia del Subject actual , es decir, el currentUser .

Ahora que tenemos el objeto de usuario actual , podemos realizar la autenticación en las credenciales proporcionadas:

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); } }

Primero, verificamos si el usuario actual aún no ha sido autenticado. Luego creamos un token de autenticación con el principal (nombre de usuario) y la credencial (contraseña) del usuario.

A continuación, intentamos iniciar sesión con el token. Si las credenciales proporcionadas son correctas, todo debería ir bien.

Existen diferentes excepciones para diferentes casos. También es posible lanzar una excepción personalizada que se adapte mejor a los requisitos de la aplicación. Esto se puede hacer subclasificando la clase AccountException .

5. Autorización

La autenticación intenta validar la identidad de un usuario, mientras que la autorización intenta controlar el acceso a determinados recursos del sistema.

Recuerde que asignamos uno o más roles a cada usuario que hemos creado en el archivo shiro.ini . Además, en la sección de roles, definimos diferentes permisos o niveles de acceso para cada rol.

Ahora veamos cómo podemos usar eso en nuestra aplicación para hacer cumplir el control de acceso de los usuarios.

En el archivo shiro.ini , le damos al administrador acceso total a cada parte del sistema.

El editor tiene acceso total a todos los recursos / operaciones relacionados con los artículos , y un autor está restringido a componer y guardar artículos únicamente.

Demos la bienvenida al usuario actual según el rol:

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"); }

Ahora, veamos qué puede hacer el usuario actual en el 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. Configuración del reino

En aplicaciones reales, necesitaremos una forma de obtener las credenciales de usuario de una base de datos en lugar del archivo shiro.ini . Aquí es donde entra en juego el concepto de Realm.

En la terminología de Apache Shiro, un Realm es un DAO que apunta a un almacén de credenciales de usuario necesarias para la autenticación y autorización.

Para crear un reino, solo necesitamos implementar la interfaz de Realm . Eso puede resultar tedioso; sin embargo, el marco viene con implementaciones predeterminadas de las que podemos crear subclases. Una de estas implementaciones es JdbcRealm .

Creamos una implementación de reino personalizada que extiende la clase JdbcRealm y anula los siguientes métodos: doGetAuthenticationInfo () , doGetAuthorizationInfo () , getRoleNamesForUser () y getPermissions () .

Creemos un reino subclasificando la clase JdbcRealm :

public class MyCustomRealm extends JdbcRealm { //... }

En aras de la simplicidad, usamos java.util.Map para simular una base de datos:

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"))); }
     
    

Procedamos y anulemos el doGetAuthenticationInfo () :

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()); }

We first cast the AuthenticationToken provided to UsernamePasswordToken. From the uToken, we extract the username (uToken.getUsername()) and use it to get the user credentials (password) from the database.

If no record is found – we throw an UnknownAccountException, else we use the credential and username to construct a SimpleAuthenticatioInfo object that's returned from the method.

If the user credential is hashed with a salt, we need to return a SimpleAuthenticationInfo with the associated salt:

return new SimpleAuthenticationInfo( uToken.getUsername(), credentials.get(uToken.getUsername()), ByteSource.Util.bytes("salt"), getName() );

We also need to override the doGetAuthorizationInfo(), as well as getRoleNamesForUser() and getPermissions().

Finally, let's plug the custom realm into the securityManager. All we need to do is replace the IniRealm above with our custom realm, and pass it to the DefaultSecurityManager‘s constructor:

Realm realm = new MyCustomRealm(); SecurityManager securityManager = new DefaultSecurityManager(realm);

Every other part of the code is the same as before. This is all we need to configure the securityManager with a custom realm properly.

Now the question is – how does the framework match the credentials?

By default, the JdbcRealm uses the SimpleCredentialsMatcher, which merely checks for equality by comparing the credentials in the AuthenticationToken and the AuthenticationInfo.

If we hash our passwords, we need to inform the framework to use a HashedCredentialsMatcher instead. The INI configurations for realms with hashed passwords can be found here.

7. Logging Out

Now that we've authenticated the user, it's time to implement log out. That's done simply by calling a single method – which invalidates the user session and logs the user out:

currentUser.logout();

8. Session Management

The framework naturally comes with its session management system. If used in a web environment, it defaults to the HttpSession implementation.

For a standalone application, it uses its enterprise session management system. The benefit is that even in a desktop environment you can use a session object as you would do in a typical web environment.

Let's have a look at a quick example and interact with the session of the current user:

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 for a Web Application With Spring

So far we've outlined the basic structure of Apache Shiro and we have implemented it in a desktop environment. Let's proceed by integrating the framework into a Spring Boot application.

Note that the main focus here is Shiro, not the Spring application – we're only going to use that to power a simple example app.

9.1. Dependencies

First, we need to add the Spring Boot parent dependency to our pom.xml:

 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE 

Next, we have to add the following dependencies to the same pom.xml file:

 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. Configuration

Adding the shiro-spring-boot-web-starter dependency to our pom.xml will by default configure some features of the Apache Shiro application such as the SecurityManager.

However, we still need to configure the Realm and Shiro security filters. We will be using the same custom realm defined above.

And so, in the main class where the Spring Boot application is run, let's add the following Bean definitions:

@Bean public Realm realm() { return new MyCustomRealm(); } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition(); filter.addPathDefinition("/secure", "authc"); filter.addPathDefinition("/**", "anon"); return filter; }

In the ShiroFilterChainDefinition, we applied the authc filter to /secure path and applied the anon filter on other paths using the Ant pattern.

Both authc and anon filters come along by default for web applications. Other default filters can be found here.

If we did not define the Realm bean, ShiroAutoConfiguration will, by default, provide an IniRealm implementation that expects to find a shiro.ini file in src/main/resources or src/main/resources/META-INF.

If we do not define a ShiroFilterChainDefinition bean, the framework secures all paths and sets the login URL as login.jsp.

We can change this default login URL and other defaults by adding the following entries to our application.properties:

shiro.loginUrl = /login shiro.successUrl = /secure shiro.unauthorizedUrl = /login

Now that the authc filter has been applied to /secure, all requests to that route will require a form authentication.

9.3. Authentication and Authorization

Let's create a ShiroSpringController with the following path mappings: /index, /login, /logout and /secure.

The login() method is where we implement actual user authentication as described above. If authentication is successful, the user is redirected to the secure page:

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";

And now in the secure() implementation, the currentUser was obtained by invoking the SecurityUtils.getSubject(). The role and permissions of the user are passed on to the secure page, as well the user's principal:

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. That's how we can integrate Apache Shiro into a Spring Boot Application.

Also, note that the framework offers additional annotations that can be used alongside filter chain definitions to secure our application.

10. JEE Integration

La integración de Apache Shiro en una aplicación JEE es solo una cuestión de configurar el archivo web.xml . Como de costumbre, la configuración espera que shiro.ini esté en la ruta de clases. Un ejemplo de configuración detallado está disponible aquí. Además, las etiquetas JSP se pueden encontrar aquí.

11. Conclusión

En este tutorial, analizamos los mecanismos de autenticación y autorización de Apache Shiro. También nos centramos en cómo definir un dominio personalizado y conectarlo al SecurityManager .

Como siempre, el código fuente completo está disponible en GitHub.