Architecture de Shiro
Pour utiliser Shiro, il faut comprendre son architecture. Shiro fonctionne sans conteneur spécifique et peut être employé dans JavaSE, mais il est couramment utilisé dans JavaEE. Voici un exemple simplifié de processus de connexion :
// Création d'un jeton d'authentification
UsernamePasswordToken userToken = new UsernamePasswordToken(identifiant, motDePasse);
Le jeton représente l'identité de l'utilisateur, et la connexion consiste à valider ce jeton via Shiro.
// Initialisation du gestionnaire de sécurité
SecurityUtils.setSecurityManager(gestionnaireSecurite);
// Récupération du sujet actuel
Subject currentSubject = SecurityUtils.getSubject();
// Tentative de connexion
currentSubject.login(userToken);
Le SecurityManager est le cœur de Shiro, gérant l'authentification et l'autorisation. Subject est une abstraction du contexte sécurisé, distincte de l'utilisateur physique. Il représente le composant protégé par Shiro.
Implémentation du Realm
Le Realm est un module crucial, souvent personnalisé pour interagir avec les sources de données. Voici les concepts clés :
Mécanisme de Cache
Ehcache est utilisé pour stocker des données en cache, par exemple pour limiter les tentatives de connexion. Configuration de base :
<ehcache name="cacheShiro">
<diskStore path="java.io.tmpdir" />
<cache name="cacheRetryMotDePasse"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
timeToLiveSeconds et timeToIdleSeconds contrôlent la durée de vie du cache.
Algorithmes de Hachage
MD5 est utilisé pour le hachage des mots de passe. Contrairement au chiffrement, le hachage est irréversible. Lors de l'enregistrement d'un utilisateur :
public class AideMotDePasse {
private GenerateurAleatoire generateur = new GenerateurAleatoireSecurise();
private String algorithme = "md5";
private final int iterations = 2;
public void hasherMotDePasse(Utilisateur utilisateur) {
String sel = generateur.genererOctets().enHex();
utilisateur.setSel(sel);
String motDePasseHashe = new SimpleHash(algorithme, utilisateur.getMotDePasse(),
ByteSource.Util.bytes(utilisateur.getSel()), iterations).enHex();
utilisateur.setMotDePasse(motDePasseHashe);
}
}
Le sel est ajouté pour renforcer la sécurité. Le même processus est appliqué lors de la vérification de la connexion.
Matcher de Crédentials
Cette classe vérifie la correspondance entre le jeton et les informations stockées. Exemple avec limitation de tentatives :
public class MatcherAvecLimite extends HashedCredentialsMatcher {
private Cache<String, AtomicInteger> cacheTentatives;
public MatcherAvecLimite(CacheManager gestionnaireCache) {
cacheTentatives = gestionnaireCache.getCache("cacheRetryMotDePasse");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String nomUtilisateur = (String) token.getPrincipal();
AtomicInteger compteur = cacheTentatives.get(nomUtilisateur);
if (compteur == null) {
compteur = new AtomicInteger(0);
cacheTentatives.put(nomUtilisateur, compteur);
}
if (compteur.incrementAndGet() > 5) {
throw new ExcessiveAttemptsException();
}
boolean correspondance = super.doCredentialsMatch(token, info);
if (correspondance) {
cacheTentatives.remove(nomUtilisateur);
}
return correspondance;
}
}
Cela empêche les tentatives abusives de connexion.
Realm Personnalisé
Le Realm fournit les informations d'autorisation et d'authentification. Exemple :
public class RealmUtilisateur extends AuthorizingRealm {
private ServiceUtilisateur serviceUtilisateur;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String nomUtilisateur = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo infoAutorisation = new SimpleAuthorizationInfo();
Set<Role> roles = serviceUtilisateur.trouverRoles(nomUtilisateur);
Set<String> nomsRoles = new HashSet<>();
for (Role role : roles) {
nomsRoles.add(role.getNom());
}
infoAutorisation.setRoles(nomsRoles);
Set<Permission> permissions = serviceUtilisateur.trouverPermissions(nomUtilisateur);
Set<String> nomsPermissions = new HashSet<>();
for (Permission permission : permissions) {
nomsPermissions.add(permission.getNom());
}
infoAutorisation.setStringPermissions(nomsPermissions);
return infoAutorisation;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String nomUtilisateur = (String) token.getPrincipal();
Utilisateur utilisateur = serviceUtilisateur.trouverParNom(nomUtilisateur);
if (utilisateur == null) {
throw new UnknownAccountException();
}
if (utilisateur.getVerrouille() == 0) {
throw new LockedAccountException();
}
SimpleAuthenticationInfo infoAuthentification = new SimpleAuthenticationInfo(
utilisateur.getNomUtilisateur(),
utilisateur.getMotDePasse(),
ByteSource.Util.bytes(utilisateur.getSel()),
getName());
return infoAuthentification;
}
}
La base de données doit stocker les utilisateurs, rôles et permissions avec des relations plusieurs-à-plusieurs.
Intégration avec SpringMVC
L'intégration se fait via des fichiers de configuration XML. D'abord, dans web.xml :
<filter>
<filter-name>filtreShiro</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>filtreShiro</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Ensuite, configurer spring-shiro-web.xml :
<bean id="gestionnaireCache" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean>
<bean id="matcherCredits" class="com.exemple.MatcherAvecLimite">
<constructor-arg ref="gestionnaireCache" />
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="2" />
</bean>
<bean id="realmUtilisateur" class="com.exemple.RealmUtilisateur">
<property name="credentialsMatcher" ref="matcherCredits" />
</bean>
<bean id="gestionnaireSecurite" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realmUtilisateur" />
</bean>
<bean id="filtreShiro" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="gestionnaireSecurite" />
<property name="loginUrl" value="/connexion" />
<property name="unauthorizedUrl" value="/erreur" />
<property name="filterChainDefinitions">
<value>
/admin/** = roles[administrateur]
/protege/** = authc
/** = anon
</value>
</property>
</bean>
Les filtres sont appliqués dans l'ordre, avec les chemins spécifiques en premier.
Contrôleur de connexion exemple :
@Controller
public class ControleurConnexion {
@Autowired
private ServiceUtilisateur serviceUtilisateur;
@RequestMapping("connexion")
public ModelAndView connecter(@RequestParam("identifiant") String identifiant,
@RequestParam("motDePasse") String motDePasse) {
UsernamePasswordToken jeton = new UsernamePasswordToken(identifiant, motDePasse);
Subject sujet = SecurityUtils.getSubject();
try {
sujet.login(jeton);
} catch (IncorrectCredentialsException e) {
return new ModelAndView("erreur", "message", "Mot de passe incorrect");
} catch (UnknownAccountException e) {
return new ModelAndView("erreur", "message", "Utilisateur inconnu");
} catch (ExcessiveAttemptsException e) {
return new ModelAndView("erreur", "message", "Trop de tentatives");
}
Utilisateur utilisateur = serviceUtilisateur.trouverParNom(identifiant);
sujet.getSession().setAttribute("utilisateur", utilisateur);
return new ModelAndView("succes");
}
}
Pour accéder à la session sécurisée :
@Controller
@RequestMapping("protege")
public class ControleurProtege {
@RequestMapping("tableau")
public ModelAndView tableauBord() {
Subject sujet = SecurityUtils.getSubject();
Utilisateur utilisateur = (Utilisateur) sujet.getSession().getAttribute("utilisateur");
// Traitement des données
return new ModelAndView("tableau");
}
}
Shiro gère les sessions de manière centralisée, offrant une alternative aux sessions des conteneurs traditionnels.