Introduzione ad Apache Shiro

1. Panoramica

In questo articolo vedremo Apache Shiro, un framework di sicurezza Java versatile.

Il framework è altamente personalizzabile e modulare, in quanto offre autenticazione, autorizzazione, crittografia e gestione delle sessioni.

2. Dipendenza

Apache Shiro ha molti moduli. Tuttavia, in questo tutorial, usiamo solo l'artefatto shiro-core .

Aggiungiamolo al nostro pom.xml :

 org.apache.shiro shiro-core 1.4.0 

L'ultima versione dei moduli Apache Shiro può essere trovata su Maven Central.

3. Configurazione di Security Manager

Il SecurityManager è l'elemento centrale del framework di Apache Shiro. Le applicazioni di solito hanno una singola istanza in esecuzione.

In questo tutorial, esploriamo il framework in un ambiente desktop. Per configurare il framework, dobbiamo creare un file shiro.ini nella cartella delle risorse con il seguente contenuto:

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

La sezione [utenti] del file di configurazione shiro.ini definisce le credenziali utente riconosciute da SecurityManager . Il formato è: p rincipal (nome utente) = password, ruolo1, ruolo2,…, ruolo .

I ruoli e le autorizzazioni associate vengono dichiarati nella sezione [ruoli] . Al ruolo di amministratore vengono concessi l'autorizzazione e l'accesso a ogni parte dell'applicazione. Ciò è indicato dal simbolo del carattere jolly (*) .

Il ruolo di editore dispone di tutte le autorizzazioni associate agli articoli, mentre il ruolo di autore può solo comporre e salvare un articolo.

Il SecurityManager viene utilizzato per configurare la classe SecurityUtils . Da SecurityUtils possiamo ottenere l'utente corrente che interagisce con il sistema ed eseguire operazioni di autenticazione e autorizzazione.

Usiamo IniRealm per caricare le nostre definizioni di utente e ruolo dal file shiro.ini e poi usarlo per configurare l' oggetto DefaultSecurityManager :

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

Ora che abbiamo un SecurityManager che è a conoscenza delle credenziali utente e dei ruoli definiti nel file shiro.ini , procediamo all'autenticazione e autorizzazione dell'utente.

4. Autenticazione

Nella terminologia di Apache Shiro, un Soggetto è qualsiasi entità che interagisce con il sistema. Può essere un essere umano, uno script o un client REST.

La chiamata di SecurityUtils.getSubject () restituisce un'istanza dell'oggetto corrente , ovvero l' attuale utente .

Ora che abbiamo l' oggetto currentUser , possiamo eseguire l'autenticazione sulle credenziali fornite:

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

Innanzitutto, controlliamo se l'utente corrente non è già stato autenticato. Quindi creiamo un token di autenticazione con il principal (nome utente) e la credenziale (password) dell'utente.

Successivamente, proviamo ad accedere con il token. Se le credenziali fornite sono corrette, tutto dovrebbe andare bene.

Esistono diverse eccezioni per casi diversi. È anche possibile generare un'eccezione personalizzata che si adatta meglio ai requisiti dell'applicazione. Questo può essere fatto creando una sottoclasse della classe AccountException .

5. Autorizzazione

L'autenticazione sta tentando di convalidare l'identità di un utente mentre l'autorizzazione sta tentando di controllare l'accesso a determinate risorse nel sistema.

Ricordiamo che assegniamo uno o più ruoli a ogni utente che abbiamo creato nel file shiro.ini . Inoltre, nella sezione ruoli, definiamo diversi permessi o livelli di accesso per ogni ruolo.

Vediamo ora come possiamo usarlo nella nostra applicazione per imporre il controllo dell'accesso degli utenti.

Nel file shiro.ini , diamo all'amministratore l'accesso totale a ogni parte del sistema.

L'editor ha accesso totale a ogni risorsa / operazione relativa agli articoli e un autore è limitato alla sola composizione e al salvataggio degli articoli .

Diamo il benvenuto all'utente corrente in base al ruolo:

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

Ora, vediamo cosa può fare l'utente corrente nel 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. Configurazione reami

Nelle applicazioni reali, avremo bisogno di un modo per ottenere le credenziali dell'utente da un database piuttosto che dal file shiro.ini . È qui che entra in gioco il concetto di Reame.

Nella terminologia di Apache Shiro, un Realm è un DAO che punta a un archivio di credenziali utente necessarie per l'autenticazione e l'autorizzazione.

Per creare un reame, dobbiamo solo implementare l' interfaccia Reame . Questo può essere noioso; tuttavia, il framework viene fornito con implementazioni predefinite da cui è possibile creare una sottoclasse. Una di queste implementazioni è JdbcRealm .

Creiamo un'implementazione realm personalizzata che estende la classe JdbcRealm e sovrascrive i seguenti metodi: doGetAuthenticationInfo () , doGetAuthorizationInfo () , getRoleNamesForUser () e getPermissions () .

Creiamo un realm creando una sottoclasse della classe JdbcRealm :

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

Per semplicità, utilizziamo java.util.Map per simulare un database:

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

Procediamo e sovrascriviamo 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

L'integrazione di Apache Shiro in un'applicazione JEE è solo una questione di configurazione del file web.xml . Come al solito, la configurazione prevede che shiro.ini sia nel percorso classi. Una configurazione di esempio dettagliata è disponibile qui. Inoltre, i tag JSP possono essere trovati qui.

11. Conclusione

In questo tutorial, abbiamo esaminato i meccanismi di autenticazione e autorizzazione di Apache Shiro. Ci siamo inoltre concentrati su come definire un'area di autenticazione personalizzata e collegarla a SecurityManager .

Come sempre, il codice sorgente completo è disponibile su GitHub.