从零到实战:Apache Shiro安全框架核心案例解析与权限模型落地指南**

目录导读
- Shiro是什么?——先解决“为什么选它”
- 环境准备与最小化集成(Maven + Spring Boot)
- 基于数据库的用户认证(登录校验)
- 细粒度授权(角色与权限控制)
- 自定义Realm实现动态权限加载
- 会话管理与会话共享(Redis集群场景)
- 常见坑位与性能优化(含问答环节)
- 从Demo到生产环境的思维跃迁
Shiro是什么?——先解决“为什么选它”
Apache Shiro是一个强大且易用的Java安全框架,提供认证、授权、加密、会话管理四大核心功能,相比Spring Security,Shiro的API更直观,学习曲线平缓,尤其适合快速构建中小型系统的权限模块,根据2024年JetBrains生态调研,Shiro在传统企业级项目(非微服务)中的使用率仍占27%左右,这得益于其轻量级特性。
核心概念速览:
- Subject:当前操作用户(可能是人,也可能是第三方服务)。
- SecurityManager:所有安全操作的最终仲裁者。
- Realm:数据源桥接器,负责从数据库或缓存中读取用户、角色、权限数据。
关键认知:Shiro本身不维护用户数据,它只负责“校验逻辑”,而数据存储完全交由你的Realm实现。
环境准备与最小化集成(Maven + Spring Boot)
以Spring Boot 2.7.x为例,在pom.xml引入:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.12.0</version>
</dependency>
核心配置类:
@Configuration
public class ShiroConfig {
@Bean
public DefaultWebSecurityManager securityManager(MyRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(realm);
return manager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager manager) {
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
filter.setSecurityManager(manager);
// 匿名访问 /login, 其余需认证
filter.setLoginUrl("/login");
filter.setFilterChainDefinitionMap(Map.of("/login", "anon", "/**", "authc"));
return filter;
}
}
注意:
authc过滤器只校验登录状态,不校验角色,若需要角色细分,需配套使用roles["admin"]这种写法。
案例一:基于数据库的用户认证(登录校验)
需求:用户输入用户名/密码,查询MySQL数据库核对密码。
Realm实现:
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
// 认证:验证账号密码
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String username = (String) token.getPrincipal();
User user = userMapper.findByUsername(username);
if (user == null) return null; // Shiro自动抛UnknownAccountException
// 密码由Shiro自行比对(密文由Shiro的HashedCredentialsMatcher处理)
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
}
// 授权:稍后详述
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// ... 见案例二
}
}
密码匹配器(推荐BCrypt):
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("SHA-256");
matcher.setHashIterations(1024); // 或使用BCryptPasswordEncoder
Controller:
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token); // 调用Realm
return "登录成功";
} catch (AuthenticationException e) {
return "登录失败:" + e.getMessage();
}
案例二:细粒度授权(角色与权限控制)
需求:管理员可以删除用户,普通用户只能查看。
数据库设计:
user表 (id, username, password)role表 (id, role_name)permission表 (id, perm_code, perm_desc)- 中间表:user_role, role_permission
Realm授权方法:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
List<String> roles = roleMapper.findRolesByUsername(username);
List<String> perms = permMapper.findPermsByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles); // 角色集合
info.addStringPermissions(perms); // 权限集合
return info;
}
在Controller中使用:
@RequiresPermissions("user:delete")
@GetMapping("/delete/{id}")
public String delete(@PathVariable Long id) {
// 只有具备 user:delete 权限的用户才能访问
return "删除成功";
}
前端标签控制(JSP/Thymeleaf):
<shiro:hasPermission name="user:create"> <button>新增用户</button> </shiro:hasPermission>
案例三:自定义Realm实现动态权限加载
痛点:如果用户权限频繁变更,每次请求都查库会带来性能压力。
解决方案:引入Redis缓存,Shiro原生支持CacheManager接口。
配置缓存:
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory){
RedisCacheManager cacheManager = new RedisCacheManager(factory);
cacheManager.setKeyPrefix("shiro_cache_");
return cacheManager;
}
// 在securityManager中注入
manager.setCacheManager(cacheManager);
动态刷新:当管理员修改某个用户的角色时,调用SecurityUtils.getSubject().logout()强制该用户下次请求重新授权。
注意:Shiro默认对每个Realm的
getAuthorizationInfo结果缓存,但认证信息不缓存(每一次登录都查库),这一点与Spring Security不同,需要根据业务权衡。
案例四:会话管理与会话共享(Redis集群场景)
Shiro原生Session保存在JVM内存中,在分布式环境下会丢失,解决方案:使用RedisSessionDAO。
@Bean
public AbstractSessionDAO sessionDAO(RedisTemplate<String, Object> redisTemplate) {
RedisSessionDAO dao = new RedisSessionDAO();
dao.setRedisManager(redisTemplate.getConnectionFactory());
return dao;
}
@Bean
public DefaultWebSessionManager sessionManager(AbstractSessionDAO sessionDAO) {
DefaultWebSessionManager sm = new DefaultWebSessionManager();
sm.setSessionDAO(sessionDAO);
sm.setGlobalSessionTimeout(1800000); // 30分钟
sm.setDeleteInvalidSessions(true);
return sm;
}
集成到SecurityManager:
manager.setSessionManager(sessionManager);
会话监听器(可选):
public class MySessionListener implements SessionListener {
@Override public void onStart(Session session) { /* 日志记录 */ }
@Override public void onStop(Session session) { /* 清理资源 */ }
@Override public void onExpiration(Session session) { /* 用户掉线处理 */ }
}
常见坑位与性能优化(含问答环节)
Q1:为什么用户登录成功后,一段时间后突然报“会话已过期”?
A:检查DefaultWebSessionManager的globalSessionTimeout,默认是30分钟,若使用Redis,确认Redis持久化策略是否将session数据清空。
Q2:修改了用户角色,但是权限还是旧的?
A:首先检查是否启用了缓存;其次清理除user:delete外的权限缓存,最直接的方法是调用subject.logout()强制登出。
Q3:如何防止Shiro的URL拦截绕过?
A:在设置FilterChainDefinitionMap时,必须将放置于最后,并且避免使用Ant通配符的歧义,如/admin/*不会匹配/admin/*/edit,建议使用/admin/**。
性能优化建议:
- 认证缓存:虽然Shiro默认不缓存认证信息,但如果你确认用户密码不会频繁变动,可以手动启用
Realm的setAuthenticationCachingEnabled(true),并配合HashedCredentialsMatcher。 - 去除无用过滤器:Shiro默认注册多个过滤器(如
logout、ssl),如果不用就删除,减少匹配开销。 - 数据库索引:在
user_role和role_permission表的user_id、role_id字段上加联合索引。
从Demo到生产环境的思维跃迁
Shiro的核心不是一堆API,而是一种把复杂的安全逻辑抽象为“认证流 + 授权流”的思维方式,在实际案例中,你会发现:
- 认证解决了“你是谁”的问题。
- 授权解决了“你能做什么”的问题。
- 会话解决了“你怎么保持状态”的问题。
最后的关键建议:
- 生产环境一定要使用密码加盐(如
ByteSource.Util.bytes(user.getSalt()))。 - 权限码命名规则建议采用
资源:操作格式,例如order:export。 - 如果未来有微服务演进计划,建议尽早从Shiro迁移至Spring Security OAuth2或Spring Security + JWT。
(全文完)