Introduction à Apache Shiro

1. Vue d'ensemble

Dans cet article, nous examinerons Apache Shiro, un cadre de sécurité Java polyvalent.

Le framework est hautement personnalisable et modulaire, car il offre l'authentification, l'autorisation, la cryptographie et la gestion de session.

2. Dépendance

Apache Shiro a de nombreux modules. Cependant, dans ce didacticiel, nous utilisons uniquement l'artefact shiro-core .

Ajoutons-le à notre pom.xml :

 org.apache.shiro shiro-core 1.4.0 

La dernière version des modules Apache Shiro est disponible sur Maven Central.

3. Configuration de Security Manager

Le SecurityManager est la pièce maîtresse du framework Apache Shiro. Les applications auront généralement une seule instance en cours d'exécution.

Dans ce didacticiel, nous explorons le cadre dans un environnement de bureau. Pour configurer le framework, nous devons créer un fichier shiro.ini dans le dossier de ressources avec le contenu suivant:

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

La section [users] du fichier de configuration shiro.ini définit les informations d'identification de l'utilisateur reconnues par SecurityManager . Le format est le suivant: p rincipal (nom d'utilisateur) = mot de passe, rôle1, rôle2,…, rôle .

Les rôles et leurs autorisations associées sont déclarés dans la section [rôles] . Le rôle d' administrateur est autorisé et a accès à toutes les parties de l'application. Ceci est indiqué par le symbole générique (*) .

Le rôle d' éditeur dispose de toutes les autorisations associées aux articles, tandis que le rôle d' auteur ne peut que composer et enregistrer un article.

Le SecurityManager est utilisé pour configurer la classe SecurityUtils . À partir des SecurityUtils, nous pouvons obtenir l'utilisateur actuel interagissant avec le système et effectuer des opérations d'authentification et d'autorisation.

Utilisons IniRealm pour charger nos définitions d'utilisateur et de rôle à partir du fichier shiro.ini , puis utilisons-le pour configurer l' objet DefaultSecurityManager :

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

Maintenant que nous avons un SecurityManager qui connaît les informations d'identification et les rôles utilisateur définis dans le fichier shiro.ini , passons à l'authentification et à l'autorisation des utilisateurs.

4. Authentification

Dans les terminologies d'Apache Shiro, un sujet est toute entité interagissant avec le système. Il peut s'agir d'un humain, d'un script ou d'un client REST.

L'appel de SecurityUtils.getSubject () retourne une instance du sujet actuel , c'est-à-dire le currentUser .

Maintenant que nous avons l' objet currentUser , nous pouvons effectuer une authentification sur les informations d'identification fournies:

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

Tout d'abord, nous vérifions si l'utilisateur actuel n'a pas déjà été authentifié. Ensuite, nous créons un jeton d'authentification avec le principal (nom d'utilisateur) et les informations d'identification (mot de passe) de l'utilisateur.

Ensuite, nous essayons de nous connecter avec le jeton. Si les informations d'identification fournies sont correctes, tout devrait bien se passer.

Il existe différentes exceptions pour différents cas. Il est également possible de lever une exception personnalisée qui correspond mieux aux exigences de l'application. Cela peut être fait en sous- classant la classe AccountException .

5. Autorisation

L'authentification tente de valider l'identité d'un utilisateur tandis que l'autorisation tente de contrôler l'accès à certaines ressources du système.

Rappelez-vous que nous attribuons un ou plusieurs rôles à chaque utilisateur que nous avons créé dans le fichier shiro.ini . De plus, dans la section des rôles, nous définissons différentes autorisations ou niveaux d'accès pour chaque rôle.

Voyons maintenant comment nous pouvons l'utiliser dans notre application pour appliquer le contrôle d'accès des utilisateurs.

Dans le fichier shiro.ini , nous donnons à l'administrateur un accès total à chaque partie du système.

L'éditeur a un accès total à toutes les ressources / opérations concernant les articles , et un auteur est limité à la composition et à l'enregistrement d' articles uniquement.

Accueillons l'utilisateur actuel en fonction de son rôle:

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

Voyons maintenant ce que l'utilisateur actuel est autorisé à faire dans le système:

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

Dans les applications réelles, nous aurons besoin d'un moyen d'obtenir les informations d'identification des utilisateurs à partir d'une base de données plutôt que du fichier shiro.ini . C'est là que le concept de royaume entre en jeu.

Dans la terminologie d'Apache Shiro, un royaume est un DAO qui pointe vers un magasin d'informations d'identification utilisateur nécessaires pour l'authentification et l'autorisation.

Pour créer un royaume, il suffit d'implémenter l' interface Realm . Cela peut être fastidieux; cependant, le framework est livré avec des implémentations par défaut à partir desquelles nous pouvons sous-classer. L'une de ces implémentations est JdbcRealm .

Nous créons une implémentation de domaine personnalisée qui étend la classe JdbcRealm et remplace les méthodes suivantes: doGetAuthenticationInfo () , doGetAuthorizationInfo () , getRoleNamesForUser () et getPermissions () .

Créons un royaume en sous- classant la classe JdbcRealm :

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

Par souci de simplicité, nous utilisons java.util.Map pour simuler une base de données:

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

Continuons et remplaçons 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'intégration d'Apache Shiro dans une application JEE est juste une question de configuration du fichier web.xml . Comme d'habitude, la configuration s'attend à ce que shiro.ini soit dans le chemin de classe. Un exemple de configuration détaillé est disponible ici. En outre, les balises JSP peuvent être trouvées ici.

11. Conclusion

Dans ce didacticiel, nous avons examiné les mécanismes d'authentification et d'autorisation d'Apache Shiro. Nous nous sommes également concentrés sur la manière de définir un domaine personnalisé et de le brancher dans SecurityManager .

Comme toujours, le code source complet est disponible à l'adresse over sur GitHub.