Introduction à Apache Shiro

Introduction à Apache Shiro

1. Vue d'ensemble

Dans cet article, nous examineronsApache Shiro, une infrastructure de sécurité Java polyvalente.

La structure est hautement personnalisable et modulaire, car elle offre une authentification, une autorisation, une cryptographie et une gestion de session.

2. Dépendance

Apache Shiro a de nombreuxmodules. Cependant, dans ce didacticiel, nous utilisons uniquement l'artefactshiro-core.

Ajoutons-le à nospom.xml:


    org.apache.shiro
    shiro-core
    1.4.0

La dernière version des modules Apache Shiro peut être trouvéeon Maven Central.

3. Configuration de Security Manager

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

Dans ce tutoriel, nous explorons le framework dans un environnement de bureau. Pour configurer le framework, nous devons créer un fichiershiro.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 configurationshiro.ini définit les informations d'identification de l'utilisateur qui sont reconnues par lesSecurityManager. Le format est:principal (username) = password, role1, role2, …, role.

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

Le rôleeditor possède toutes les autorisations associées àarticles tandis que le rôleauthor ne peut quecompose etsave un article.

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

Utilisons lesIniRealm pour charger nos définitions d'utilisateur et de rôle à partir du fichiershiro.ini, puis utilisons-le pour configurer l'objetDefaultSecurityManager:

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

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

4. Authentification

Dans les terminologies d'Apache Shiro, unSubject 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 deSecurityUtils.getSubject() renvoie une instance desSubject actuels, c'est-à-dire descurrentUser.

Maintenant que nous avons l'objetcurrentUser, 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(username) de l'utilisateur et les informations d'identification(password).

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 lancer une exception personnalisée qui correspond mieux aux exigences de l'application. Cela peut être fait en sous-classant la classeAccountException.

5. Autorisation

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

Rappelons que nous attribuons un ou plusieurs rôles à chaque utilisateur que nous avons créé dans le fichiershiro.ini. De plus, dans la section rôles, nous définissons des autorisations ou des niveaux d'accès différents pour chaque rôle.

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

Dans le fichiershiro.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 concernantarticles, et un auteur est limité à la composition et à l'enregistrement dearticles 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 qu'à partir du fichiershiro.ini. C’est là que le concept de royaume entre en jeu.

Dans la terminologie d'Apache Shiro, unRealm 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'interfaceRealm. Cela peut être fastidieux. Cependant, le cadre est fourni avec des implémentations par défaut que nous pouvons sous-classer. L'une de ces implémentations estJdbcRealm.

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

Créons un domaine en sous-classant la classeJdbcRealm:

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

Par souci de simplicité, nous utilisonsjava.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 lesdoGetAuthenticationInfo():

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

Nous convertissons d'abord lesAuthenticationToken fournis enUsernamePasswordToken. À partir desuToken, nous extrayons le nom d'utilisateur (uToken.getUsername()) et l'utilisons pour obtenir les informations d'identification de l'utilisateur (mot de passe) de la base de données.

Si aucun enregistrement n'est trouvé - nous lançons unUnknownAccountException, sinon nous utilisons les informations d'identification et le nom d'utilisateur pour construire un objetSimpleAuthenticatioInfo qui est retourné par la méthode.

Si les informations d'identification de l'utilisateur sont hachées avec un sel, nous devons renvoyer unSimpleAuthenticationInfo avec le sel associé:

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

Nous devons également remplacer lesdoGetAuthorizationInfo(), ainsi que lesgetRoleNamesForUser() etgetPermissions().

Enfin, connectons le domaine personnalisé auxsecurityManager. Tout ce que nous devons faire est de remplacer leIniRealm ci-dessus par notre domaine personnalisé et de le transmettre au constructeur deDefaultSecurityManager:

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

Toutes les autres parties du code sont les mêmes que précédemment. C'est tout ce dont nous avons besoin pour configurer correctement lessecurityManager avec un domaine personnalisé.

Maintenant, la question est de savoir comment le cadre correspond aux informations d'identification.

Par défaut, leJdbcRealm utilise leSimpleCredentialsMatcher, qui vérifie simplement l'égalité en comparant les informations d'identification dans lesAuthenticationToken et lesAuthenticationInfo.

Si nous hachons nos mots de passe, nous devons informer le framework d'utiliser unHashedCredentialsMatcher à la place. Les configurations INI pour les royaumes avec des mots de passe hachés peuvent être trouvéeshere.

7. Déconnecter

Maintenant que nous avons authentifié l'utilisateur, il est temps de mettre en œuvre la déconnexion. Cela se fait simplement en appelant une seule méthode - qui invalide la session utilisateur et déconnecte l'utilisateur:

currentUser.logout();

8. Gestion de session

Le cadre vient naturellement avec son système de gestion de session. S'il est utilisé dans un environnement Web, il utilise par défaut l'implémentationHttpSession.

Pour une application autonome, il utilise son système de gestion de session d'entreprise. L'avantage est que même dans un environnement de bureau, vous pouvez utiliser un objet de session comme vous le feriez dans un environnement Web classique.

Jetons un coup d'œil à un exemple rapide et interagissons avec la session de l'utilisateur actuel:

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 pour une application Web avec Spring

Jusqu'à présent, nous avons décrit la structure de base d'Apache Shiro et nous l'avons implémentée dans un environnement de bureau. Continuons en intégrant le framework dans une application Spring Boot.

Notez que l'objectif principal ici est Shiro, pas l'application Spring - nous n'allons l'utiliser que pour alimenter un exemple d'application simple.

9.1. Les dépendances

Tout d'abord, nous devons ajouter la dépendance parent Spring Boot à nospom.xml:


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

Ensuite, nous devons ajouter les dépendances suivantes au même fichierpom.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. Configuration

L'ajout de la dépendanceshiro-spring-boot-web-starter à nospom.xml configurera par défaut certaines fonctionnalités de l'application Apache Shiro telles que lesSecurityManager.

Cependant, nous devons encore configurer les filtres de sécuritéRealm et Shiro. Nous utiliserons le même domaine personnalisé que celui défini ci-dessus.

Et donc, dans la classe principale où l'application Spring Boot est exécutée, ajoutons les définitionsBean suivantes:

@Bean
public Realm realm() {
    return new MyCustomRealm();
}

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter
      = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/secure", "authc");
    filter.addPathDefinition("/**", "anon");

    return filter;
}

Dans lesShiroFilterChainDefinition, nous avons appliqué le filtreauthc au chemin/secure et appliqué le filtreanon sur d'autres chemins en utilisant le modèle Ant.

Les filtresauthc etanon sont fournis par défaut pour les applications Web. D'autres filtres par défaut peuvent être trouvéshere.

Si nous n'avons pas défini le beanRealm,ShiroAutoConfiguration fournira, par défaut, une implémentation deIniRealm qui s'attend à trouver un fichiershiro.ini danssrc/main/resources ousrc/main/resources/META-INF.

Si nous ne définissons pas de beanShiroFilterChainDefinition, le framework sécurise tous les chemins et définit l'URL de connexion commelogin.jsp.

Nous pouvons modifier cette URL de connexion par défaut et d'autres valeurs par défaut en ajoutant les entrées suivantes à nosapplication.properties:

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

Maintenant que le filtreauthc a été appliqué à/secure, toutes les demandes vers cette route nécessiteront une authentification par formulaire.

9.3. Authentification et autorisation

Créons unShiroSpringController avec les mappages de chemins suivants:/index,/login, /logout et/secure.

La méthodelogin() est l'endroit où nous implémentons l'authentification utilisateur réelle comme décrit ci-dessus. Si l'authentification réussit, l'utilisateur est redirigé vers la page sécurisée:

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

Et maintenant dans l'implémentation desecure(), lecurrentUser a été obtenu en invoquant lesSecurityUtils.getSubject(). Le rôle et les permissions de l'utilisateur sont transmis à la page sécurisée, ainsi que le principal de l'utilisateur:

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. C'est ainsi que nous pouvons intégrer Apache Shiro dans une application Spring Boot.

Notez également que le framework offre desannotations supplémentaires qui peuvent être utilisés avec les définitions de chaînes de filtres pour sécuriser notre application.

10. Intégration JEE

L'intégration d'Apache Shiro dans une application JEE est juste une question de configuration du fichierweb.xml. Comme d'habitude, la configuration s'attend à ce queshiro.ini soit dans le chemin de classe. Un exemple de configuration détaillé est disponiblehere. De plus, les balises JSP peuvent être trouvéeshere.

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 façon de définir un domaine personnalisé et de le connecter auxSecurityManager.

Comme toujours, le code source complet est disponibleover on GitHub.