package com.label.common.shiro; import com.fasterxml.jackson.databind.ObjectMapper; import com.label.common.redis.RedisService; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import jakarta.servlet.Filter; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Shiro security configuration. * * Filter chain: * /api/auth/login → anon (no auth required) * /api/auth/logout → tokenFilter * /api/** → tokenFilter (all other API endpoints require auth) * /actuator/** → anon (health check) * /** → anon (default) * * NOTE: spring.mvc.pathmatch.matching-strategy=ant_path_matcher MUST be set * in application.yml for Shiro to work correctly with Spring Boot 3. */ @Configuration public class ShiroConfig { @Bean public UserRealm userRealm(RedisService redisService) { return new UserRealm(redisService); } @Bean public SecurityManager securityManager(UserRealm userRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealms(List.of(userRealm)); return manager; } @Bean public TokenFilter tokenFilter(RedisService redisService, ObjectMapper objectMapper) { return new TokenFilter(redisService, objectMapper); } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, TokenFilter tokenFilter) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); // Register custom filters Map filters = new LinkedHashMap<>(); filters.put("tokenFilter", tokenFilter); factory.setFilters(filters); // Filter chain definition (ORDER MATTERS - first match wins) Map filterChainDef = new LinkedHashMap<>(); filterChainDef.put("/api/auth/login", "anon"); filterChainDef.put("/actuator/**", "anon"); filterChainDef.put("/api/**", "tokenFilter"); filterChainDef.put("/**", "anon"); factory.setFilterChainDefinitionMap(filterChainDef); return factory; } }