Configuration et flux d'authentification avec Apache Shiro et OAuth2

Apache Shiro est un framework de sécurité complet pour Java, couramment déployé pour orchestrer l'authentification et les règles d'autorisation. Au sein d'une architecture Spring Boot, son intégration repose sur l'instanciation de gestionnaires de sessions, d'un noyau de sécurité et de chaînes de filtres spécifiques.

Architecture des filtres de sécurité

Le point d'antrée du mécanisme de protection se situe au niveau de la classe de configuration. L'objectif principal est de sécuriser les points de terminaison (endpoints) de l'API en imposant une stratégie d'authentification basée sur OAuth2, tout en maintenant l'accessibilité publique pour certaines routes utilitaires.


@Configuration
public class SecurityConfiguration {

    @Bean
    public DefaultWebSessionManager webSessionManager() {
        DefaultWebSessionManager manager = new DefaultWebSessionManager();
        manager.setSessionValidationSchedulerEnabled(false);
        manager.setSessionIdUrlRewritingEnabled(false);
        return manager;
    }

    @Bean("coreSecurityManager")
    public SecurityManager coreSecurityManager(CustomOAuth2Realm authRealm, SessionManager sessionManager) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(authRealm);
        manager.setSessionManager(sessionManager);
        manager.setRememberMeManager(null); 
        return manager;
    }

    @Bean("securityFilterChain")
    public ShiroFilterFactoryBean securityFilterChain(SecurityManager coreSecurityManager) {
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        filterFactory.setSecurityManager(coreSecurityManager);

        Map<String, Filter> customFilters = new HashMap<>();
        customFilters.put("oauth2", new CustomOAuth2Filter());
        filterFactory.setFilters(customFilters);

        Map<String, String> accessRules = new LinkedHashMap<>();
        accessRules.put("/public/assets/**", "anon");
        accessRules.put("/api/v1/auth/login", "anon");
        accessRules.put("/v3/api-docs/**", "anon");
        accessRules.put("/swagger-ui/**", "anon");
        accessRules.put("/health", "anon");
        accessRules.put("/**", "oauth2"); 
        filterFactory.setFilterChainDefinitionMap(accessRules);

        return filterFactory;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAdvisor(SecurityManager coreSecurityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(coreSecurityManager);
        return advisor;
    }
}

Dans cette configuration, la fabrique de filtres (ShiroFilterFactoryBean) joue un rôle central. Les routes annotées avec la règle anon sont exemptées de contrôle, permettant un accès anonyme. À l'inverse, le filtre personnalisé oauth2 capture l'ensemble des requêtes restantes pour valider la présence et l'intégrité des jetons d'accès.

Traitement de la requête d'identification

La route dédiée à la connexion étant déclarée comme publique, la requête HTTP contourne le filtre OAuth2 pour atteindre directement le contrôleur REST. Le cycle de vie de l'authentification englobe la vérificasion des formulaires, la validation anti-robot (captcha) et la confrontation cryptographique des identifiants.


@RestController
@RequestMapping("/api/v1/auth")
public class AuthenticationController {

    @PostMapping("/login")
    public ResponseEntity<AuthResponse> authenticateUser(@RequestBody AuthRequest request, HttpServletRequest httpRequest) {
        ValidationHelper.checkConstraints(request);

        boolean isCaptchaValid = captchaVerifier.verify(request.getCaptchaId(), request.getCaptchaCode());
        if (!isCaptchaValid) {
            throw new SecurityException(ErrorCodes.INVALID_CAPTCHA);
        }

        UserProfile profile = userRepository.findByUsername(request.getUsername());
        
        AuditLog loginAudit = AuditLog.builder()
                .action(ActionType.LOGIN_ATTEMPT)
                .timestamp(Instant.now())
                .ipAddress(NetworkUtils.extractClientIp(httpRequest))
                .userAgent(httpRequest.getHeader(HttpHeaders.USER_AGENT))
                .build();

        if (profile == null) {
            loginAudit.setStatus(Status.FAILED);
            loginAudit.setTargetUser(request.getUsername());
            auditService.record(loginAudit);
            throw new AuthenticationException(ErrorCodes.BAD_CREDENTIALS);
        }

        if (!passwordEncoder.matches(request.getPassword(), profile.getHashedPassword())) {
            loginAudit.setStatus(Status.FAILED);
            loginAudit.setUserId(profile.getId());
            auditService.record(loginAudit);
            throw new AuthenticationException(ErrorCodes.BAD_CREDENTIALS);
        }

        if (profile.getAccountStatus() == AccountStatus.SUSPENDED) {
            loginAudit.setStatus(Status.BLOCKED);
            loginAudit.setUserId(profile.getId());
            auditService.record(loginAudit);
            throw new AuthenticationException(ErrorCodes.ACCOUNT_DISABLED);
        }

        loginAudit.setStatus(Status.SUCCESS);
        loginAudit.setUserId(profile.getId());
        auditService.record(loginAudit);

        String accessToken = tokenProvider.generateJwt(profile);
        return ResponseEntity.ok(new AuthResponse(accessToken));
    }
}

Après avoir franchi avec succès les étapes de validation, le mot de pasce est comparé via un algorithme de hachage. En cas de concordance et si le compte n'est pas restreint, un événement de succès est persisté dans les journaux d'audit, et un jeton d'accès est généré pour autoriser les futures requêtes protégées par le filtre Shiro.

Étiquettes: Apache Shiro Spring Boot oauth2 Java sécurité web

Publié le 6 juillet à 18h23