第一章:Shiro 概述与核心架构
1.1 什么是 Apache Shiro
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Apache Shiro | 一个强大且易用的 Java 安全框架,提供认证、授权、会话管理和加密等核心安全功能 | 不依赖 Web 容器,可用于 JavaSE 和 JavaEE 环境 |
| 设计目标 | 简化应用安全开发,提供直观、一致的 API | 相比 Spring Security 更轻量、学习曲线更平缓 |
| 开源协议 | Apache License 2.0 | 可免费用于商业项目 |
| 所属组织 | Apache 软件基金会(ASF) | 社区活跃,文档完善 |
1.2 Shiro 的核心功能(认证、授权、会话、加密)
| 功能名称 | 说明 | 注意事项 |
|---|---|---|
| Authentication(认证) | 验证用户身份,如用户名/密码、OAuth、LDAP 等凭证校验 | 支持多数据源(多 Realm)并行或策略化认证 |
| Authorization(授权) | 控制用户能否访问特定资源或执行操作,支持基于角色和细粒度权限 | 权限格式推荐使用 resource:action 形式(如 user:delete) |
| Session Management(会话管理) | 管理用户会话状态,支持非 Web 环境下的会话(如命令行应用) | 默认会话超时为 30 分钟,可自定义 |
| Cryptography(加密) | 提供哈希(MD5、SHA)、加盐、多次迭代等密码保护机制 | 禁止明文存储密码,应使用 HashedCredentialsMatcher 配合盐值 |
| Web Support(Web 集成) | 提供过滤器链、JSP 标签库,支持 URL 级别安全控制 | 需引入 shiro-web 模块 |
| Caching(缓存) | 缓存认证/授权结果,提升性能 | 需配合 CacheManager,默认无缓存 |
| Concurrency(并发支持) | 支持多线程环境下 Subject 的传播(如异步任务中保持用户上下文) | 使用 ThreadContext.bind() 显式绑定 Subject |
| Run As | 允许当前用户临时”伪装”为其他用户身份 | 适用于管理员模拟用户操作场景 |
| Remember Me | 记住用户登录状态,下次访问无需重新输入凭证 | 不等于自动登录,仅记住身份标识,仍需认证才能执行敏感操作 |
1.3 Shiro 核心组件介绍(Subject、SecurityManager、Realm)
| 组件名称 | 说明 | 注意事项 |
|---|---|---|
| Subject | 代表当前与系统交互的”主体”,可以是用户、服务、爬虫等 | 所有安全操作入口,通过 SecurityUtils.getSubject() 获取;线程绑定(ThreadLocal) |
| SecurityManager | Shiro 的核心安全管理器,协调所有安全操作(认证、授权、会话等) | 应用中通常只有一个实例;必须通过 SecurityUtils.setSecurityManager() 绑定 |
| Realm | 安全数据源的桥梁,负责从数据库、LDAP、INI 文件等获取用户/角色/权限信息 | 至少配置一个 Realm;支持多个 Realm,可组合使用;需实现 doGetAuthenticationInfo 和 doGetAuthorizationInfo 方法 |
| Authenticator | 内部组件,由 SecurityManager 调用,负责执行认证逻辑 | 默认实现为 ModularRealmAuthenticator,支持多 Realm 认证策略 |
| Authorizer | 内部组件,负责授权决策 | 默认实现为 ModularRealmAuthorizer |
| SessionManager | 管理 Session 生命周期 | Web 环境下可使用 ServletContainerSessionManager 委托给容器,也可自定义(如 Redis) |
| CacheManager | 为 Realm、Session 等提供缓存支持 | 若未配置,则每次授权/认证都会查询数据源,影响性能 |
1.4 Shiro 架构图与工作流程
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 1. 应用调用 Subject | 应用通过 Subject.login(token) 发起认证请求 | Subject 是门面(Facade),实际操作委托给 SecurityManager |
| 2. Subject 委托 SecurityManager | Subject 将请求转发给全局 SecurityManager 实例 | SecurityManager 是单例,需提前初始化并绑定 |
| 3. SecurityManager 调用 Authenticator | SecurityManager 内部的 Authenticator 执行认证逻辑 | Authenticator 决定使用哪个或哪些 Realm |
| 4. Authenticator 调用 Realm | 调用配置的 Realm 的 doGetAuthenticationInfo 方法 | Realm 从数据源加载用户凭证(如密码哈希值) |
| 5. 凭证比对 | Shiro 自动比对用户提交的密码与 Realm 返回的密码(支持加盐、哈希) | 需配置 CredentialsMatcher,否则默认字符串比对 |
| 6. 认证成功后创建 Principal | 用户身份信息(如用户名)被封装为 Principal 存入 Subject | 可通过 subject.getPrincipal() 获取 |
| 7. 授权检查流程 | 调用 subject.isPermitted("xxx") → SecurityManager → Authorizer → Realm | Realm 的 doGetAuthorizationInfo 返回角色/权限集合 |
| 8. 会话创建 | 登录成功后自动创建 Session(即使非 Web 环境) | 可通过 subject.getSession() 获取 |
| 9. 缓存生效(若配置) | 认证/授权结果可被缓存,后续请求直接命中缓存 | 缓存键基于 Principal,需确保其可序列化 |
第二章:快速入门
2.1 环境准备(Maven 依赖、日志配置)
| 配置项 | 语法/内容 | 用途 | 注意事项 |
|---|---|---|---|
| 基础 Maven 依赖 | <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.13.0</version></dependency> | 引入 Shiro 核心功能(认证、授权、会话、加密) | 版本建议使用最新稳定版(截至 2026 年为 1.13.0);若用于 Web 项目,需额外引入 shiro-web |
| Web 支持依赖 | <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.13.0</version></dependency> | 提供 Servlet 过滤器、JSP 标签等 Web 集成功能 | 仅在 Web 项目中需要 |
| 日志桥接(SLF4J + Logback) | <dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.12</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.4.14</version></dependency> | 启用 Shiro 内部日志输出(如调试 Realm 调用) | Shiro 默认使用 SLF4J,必须提供具体日志实现,否则无日志输出 |
| 最小可运行 pom.xml 示例 | <project><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>shiro-demo</artifactId><version>1.0</version><dependencies>…(包含 shiro-core、slf4j-api、logback-classic) </dependencies></project> | 构建可运行的 Shiro 控制台项目 | 确保 JDK ≥ 8;避免与 Spring Security 冲突 |
2.2 配置文件 shiro.ini 详解
| 配置段 | 语法示例 | 用途 | 注意事项 |
|---|---|---|---|
[main] | securityManager=org.apache.shiro.mgt.DefaultSecurityManagermyRealm=com.example.MyRealmsecurityManager.realms=$myRealm | 配置核心对象(SecurityManager、Realm、策略等) | $ 表示引用已定义的对象;支持 setter 注入(如 realm.credentialsMatcher=$matcher) |
[users] | zhang=123,admin,userli=abc,guest | 定义静态用户及其密码和角色 | 仅适用于演示或小型系统;密码建议哈希处理 |
[roles] | admin=user:*,order:*user=user:view | 定义角色对应的权限集合 | 权限格式推荐 resource:action;支持通配符 * |
[urls] | /login=anon/admin/**=authc,roles[admin]/api/**=authc,perms["user:edit"] | Web 环境下配置 URL 拦截规则 | 需配合 IniShiroFilter 使用;过滤器顺序敏感(从左到右执行) |
| 对象属性注入(集合) | securityManager.sessionManager.sessionListeners=$listener1,$listener2 | 注入多个监听器或组件 | 多个引用用逗号分隔 |
| 密码匹配器配置 | credentialsMatcher=org.apache.shiro.authc.credential.Sha256CredentialsMatcheriniRealm.credentialsMatcher=$credentialsMatcher | 指定密码比对算法 | 必须与存储的密码哈希方式一致 |
| 登录跳转页 | authc.loginUrl=/login.html | 未认证时重定向地址 | 仅在 Web 环境生效 |
2.3 第一个 Shiro 应用(命令行示例)
| 方法/操作 | 代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 初始化 SecurityManager | Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");SecurityManager securityManager = factory.getInstance();SecurityUtils.setSecurityManager(securityManager); | 从 shiro.ini 加载配置并绑定全局 SecurityManager | 必须在任何 Subject 操作前完成;IniSecurityManagerFactory 位于 shiro-core |
| 获取当前 Subject | Subject currentUser = SecurityUtils.getSubject(); | 获取代表当前用户的 Subject 实例 | 线程安全,内部使用 ThreadLocal |
| 创建登录令牌 | UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123"); | 封装用户凭证 | 支持 rememberMe:new UsernamePasswordToken("zhang", "123", true) |
| 执行登录 | currentUser.login(token); | 触发认证流程 | 若失败抛出 AuthenticationException 子类(如 UnknownAccountException) |
| 判断是否认证 | if (currentUser.isAuthenticated()) { ... } | 检查用户是否已通过认证 | 不同于 isRemembered() |
| 获取身份信息 | String username = (String) currentUser.getPrincipal(); | 获取登录用户名(或其他 Principal) | 返回类型由 Realm 的 getAuthenticationInfo 决定 |
| 退出登录 | currentUser.logout(); | 清除会话和身份信息 | 自动销毁 Session |
2.4 常见认证/授权操作演示
| 操作名称 | 方法/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 角色检查 | currentUser.hasRole("admin") | 判断用户是否拥有指定角色 | 返回 boolean;角色名需与 [roles] 或 Realm 返回一致 |
| 多角色检查 | currentUser.hasAllRoles(Arrays.asList("admin", "user")) | 判断是否同时拥有多个角色 | 全部满足才返回 true |
| 权限检查(单个) | currentUser.isPermitted("user:delete") | 检查是否拥有某权限 | 权限字符串需与 [roles] 或 Realm 中定义匹配 |
| 权限检查(多个) | currentUser.isPermittedAll("user:view", "order:create") | 检查是否拥有所有指定权限 | 全部满足才返回 true |
| 抛异常式检查 | currentUser.checkRole("admin");currentUser.checkPermission("user:*"); | 若不满足则抛出 AuthorizationException | 适用于强制校验场景 |
| 获取所有角色 | 不可直接获取角色列表 | Shiro 不提供直接获取角色集合的方法 | 需自定义 Realm 缓存或通过权限反推 |
| Remember Me 判断 | currentUser.isRemembered() | 判断是否为”记住我”状态 | 与 isAuthenticated() 互斥(通常不会同时为 true) |
| 会话操作 | Session session = currentUser.getSession();session.setAttribute("key", "value");Object val = session.getAttribute("key"); | 存取会话数据 | 即使非 Web 环境也可使用;默认内存存储 |
第三章:身份认证(Authentication)
3.1 认证流程与核心类(UsernamePasswordToken、Authenticator)
| 类/接口名称 | 语法/方法 | 用途 | 注意事项 |
|---|---|---|---|
Subject.login(AuthenticationToken token) | subject.login(new UsernamePasswordToken("user", "pass")); | 启动认证流程的入口方法 | 抛出 AuthenticationException 及其子类表示失败 |
UsernamePasswordToken | new UsernamePasswordToken(username, password);new UsernamePasswordToken(username, password, rememberMe); | 封装用户名和密码的标准令牌 | 支持”记住我”;可继承扩展自定义字段(如验证码) |
AuthenticationToken | 自定义实现需实现此接口 | 通用认证令牌接口 | 必须提供 getPrincipal() 和 getCredentials() |
Authenticator | SecurityManager.getAuthenticator() | 负责协调 Realm 执行认证逻辑 | 默认实现为 ModularRealmAuthenticator |
ModularRealmAuthenticator | 配置在 [main] 段:authcStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategysecurityManager.authenticator.authenticationStrategy=$authcStrategy | 支持多 Realm 认证策略 | 常见策略:AllSuccessful、AtLeastOneSuccessful、FirstSuccessful |
AuthenticationInfo | SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, hashedPassword, salt, realmName); | Realm 返回的认证信息封装 | 包含用户身份、凭证、盐值、Realm 名称 |
doGetAuthenticationInfo | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException | 自定义 Realm 中必须实现的方法 | 从数据源加载用户凭证并返回 AuthenticationInfo |
3.2 内置 Realm 使用(IniRealm、TextConfigurationRealm)
| Realm 类型 | 配置方式(shiro.ini) | 用途 | 注意事项 |
|---|---|---|---|
| IniRealm | [main]iniRealm=org.apache.shiro.realm.text.IniRealminiRealm.resourcePath=classpath:users.inisecurityManager.realms=$iniRealmusers.ini 内容: [users]admin=123,admin,user[roles]admin=user:*,order:* | 从 INI 文件加载用户/角色数据 | 适用于演示或小型系统;不支持动态更新 |
| PropertiesRealm | [main]propRealm=org.apache.shiro.realm.text.PropertiesRealmpropRealm.userDefinitions=classpath:users.propertiespropRealm.roleDefinitions=classpath:roles.propertiessecurityManager.realms=$propRealmusers.properties: admin=123,admin,user | 从 properties 文件加载用户数据 | 格式与 IniRealm 的 [users] 段相同 |
| TextConfigurationRealm | 抽象基类,IniRealm 和 PropertiesRealm 均继承自它 | 提供文本配置解析能力 | 不可直接实例化 |
| 直接内嵌配置 | [users]zhang=123,admin[roles]admin=user:*(无需显式声明 Realm) | 快速原型开发 | Shiro 自动创建 IniRealm 并加载 [users]/[roles] 段 |
| 密码匹配器集成 | [main]matcher=org.apache.shiro.authc.credential.Sha256CredentialsMatcheriniRealm.credentialsMatcher=$matcher | 对 INI 中存储的哈希密码进行校验 | INI 中的密码必须是对应算法的哈希值(如 SHA-256) |
3.3 自定义 Realm 实现
| 方法/组件 | 代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 继承 AuthorizingRealm | public class MyRealm extends AuthorizingRealm { ... } | 推荐方式,同时支持认证和授权 | 比直接实现 Realm 接口更简便 |
| 重写 supports | @Overridepublic boolean supports(AuthenticationToken token) {return token instanceof UsernamePasswordToken;} | 判断当前 Realm 是否处理该类型 Token | 多 Realm 场景下用于路由 |
| 实现 doGetAuthenticationInfo | @Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String username = (String) token.getPrincipal();User user = userService.findByUsername(username);if (user == null) throw new UnknownAccountException();if (user.isLocked()) throw new LockedAccountException();return new SimpleAuthenticationInfo(user.getUsername(),user.getPasswordHash(),ByteSource.Util.bytes(user.getSalt()),getName());} | 从数据库等数据源加载用户凭证 | 返回 null 表示该 Realm 不处理此用户;盐值必须为 ByteSource |
| 配置自定义 Realm | [main]myRealm=com.example.MyRealmsecurityManager.realms=$myRealm | 在 shiro.ini 中注册 | 若使用 Spring,可通过 Bean 注入 |
| 多 Realm 组合 | [main]realm1=com.example.LdapRealmrealm2=com.example.DbRealmsecurityManager.realms=$realm1,$realm2 | 同时支持 LDAP 和数据库认证 | 需配置合适的 AuthenticationStrategy |
| 缓存认证结果 | @Overridepublic void setCacheManager(CacheManager cacheManager) {super.setCacheManager(cacheManager);} | 启用认证信息缓存 | 需在配置中设置 cacheManager,否则每次都会调用数据库 |
3.4 认证异常处理(UnknownAccountException、LockedAccountException 等)
| 异常类 | 触发条件 | 建议处理方式 | 注意事项 |
|---|---|---|---|
AuthenticationException | 所有认证失败的基类 | 捕获后统一提示”用户名或密码错误” | 避免暴露具体失败原因(防信息泄露) |
UnknownAccountException | 用户不存在 | 提示”账户不存在”或统一错误信息 | 生产环境建议与 IncorrectCredentialsException 合并提示 |
IncorrectCredentialsException | 密码错误 | 提示”密码错误”或统一错误信息 | 防止枚举有效用户名 |
LockedAccountException | 账户被锁定 | 提示”账户已被锁定,请联系管理员” | 需在 Realm 中主动抛出 |
DisabledAccountException | 账户被禁用 | 提示”账户已禁用” | 与锁定语义略有不同(通常由管理员手动禁用) |
ExcessiveAttemptsException | 登录尝试次数过多(需自定义实现) | 提示”尝试次数过多,请稍后再试” | Shiro 未内置,需在 Realm 或 Filter 中实现计数逻辑 |
ExpiredCredentialsException | 凭证过期(如密码过期) | 提示”密码已过期,请重置” | 需在 Realm 中检查用户状态 |
异常捕获示例:
try {
subject.login(token);
} catch (UnknownAccountException e) {
// handle
} catch (IncorrectCredentialsException e) {
// handle
} catch (AuthenticationException e) {
// fallback
}
第四章:授权控制(Authorization)
4.1 授权模型(角色 vs 权限)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 基于角色的访问控制(RBAC) | 用户 → 角色 → 权限;角色是权限的集合 | Shiro 支持 RBAC,但不强制;可直接基于权限控制 |
| 角色(Role) | 代表一组职能或身份(如 admin、user) | 角色本身无语义,其价值在于绑定的权限集合 |
| 权限(Permission) | 对资源的操作许可,是最小授权单元(如 user:delete、file:read:/etc/passwd) | 推荐使用权限而非角色进行细粒度控制,避免”角色爆炸” |
| 角色授权检查 | subject.hasRole("admin") | 仅判断是否拥有该角色名,不关心角色包含哪些权限 |
| 权限授权检查 | subject.isPermitted("user:edit") | 直接校验操作合法性,与角色解耦,更灵活 |
| Shiro 授权设计哲学 | ”权限优于角色” —— 鼓励使用权限字符串表达业务语义 | 角色适合粗粒度分组,权限适合细粒度控制 |
4.2 权限语法(通配符权限、实例级权限)
| 权限类型 | 语法示例 | 说明 | 注意事项 |
|---|---|---|---|
| 简单权限 | "print" | 单一操作标识 | 不推荐,缺乏结构 |
| 分层权限(推荐) | "printer:query""user:delete""file:read:/home/admin/report.pdf" | 格式:resource:action[:instance] | 多部分用冒号分隔;支持任意层级 |
通配符 * | "user:*" → 匹配 user:create、user:delete"*:view" → 匹配 user:view、order:view | 表示任意值 | 不能跨段匹配(user:* 不匹配 user:profile:view) |
通配符 ? | "user:edit:??" → 匹配 user:edit:12(两位) | 表示单个字符(较少使用) | 实际项目中几乎不用 |
| 多值权限 | "user:edit,delete" | 逗号分隔多个动作 | 等价于两个独立权限:user:edit 和 user:delete |
| 实例级权限 | "document:edit:DOC-2025-001""bankAccount:transfer:ACC-987654321" | 控制对特定对象实例的操作 | 需在 Realm 中动态生成(如根据用户 ID 返回其拥有的文档权限) |
| 权限继承(逻辑) | 若用户有 "user:*",则自动拥有 "user:create" | Shiro 自动处理通配符匹配 | 无需显式声明子权限 |
| 权限字符串规范建议 | 使用小写 + 冒号分隔格式:domain:action[:target]如 order:cancel:ORD-1001 | — | 避免空格和特殊字符;保持一致性 |
4.3 授权检查方法(hasRole、isPermitted 等)
| 方法名称 | 语法/签名 | 用途 | 注意事项 |
|---|---|---|---|
hasRole(String role) | boolean hasAdmin = subject.hasRole("admin"); | 检查是否拥有指定角色 | 返回 boolean;角色名需与 Realm 返回一致 |
hasAllRoles(Collection<String> roles) | subject.hasAllRoles(Arrays.asList("admin", "auditor")); | 检查是否同时拥有所有角色 | 全部满足才返回 true |
hasRoles(List<String> roles) | boolean[] results = subject.hasRoles(Arrays.asList("admin", "user")); | 批量检查多个角色 | 返回布尔数组,顺序对应输入 |
isPermitted(String permission) | if (subject.isPermitted("user:delete")) { ... } | 检查是否拥有指定权限 | 支持通配符匹配(如 "user:*" 匹配 "user:delete") |
isPermitted(Permission p) | subject.isPermitted(new WildcardPermission("file:read:*")); | 传入 Permission 对象 | 可自定义 Permission 实现类 |
isPermittedAll(String... permissions) | subject.isPermittedAll("user:view", "user:edit"); | 检查是否拥有所有指定权限 | 全部满足才返回 true |
checkRole(String role) | subject.checkRole("admin"); | 若无角色则抛出 UnauthorizedException | 适用于强制校验,避免 if 判断 |
checkPermission(String perm) | subject.checkPermission("order:cancel"); | 若无权限则抛出 UnauthorizedException | 常用于方法入口校验 |
getPrincipals() | PrincipalCollection pc = subject.getPrincipals(); | 获取所有身份信息(含多 Realm 场景) | 可用于调试或审计 |
4.4 注解式授权(@RequiresRoles、@RequiresPermissions)
| 注解名称 | 语法示例 | 用途 | 注意事项 |
|---|---|---|---|
@RequiresAuthentication | @RequiresAuthenticationpublic void updateUser(User u) { ... } | 要求用户已通过认证(等价于 subject.isAuthenticated()) | 不检查角色或权限 |
@RequiresGuest | @RequiresGuestpublic void register() { ... } | 要求当前为游客(未认证且未 RememberMe) | 通常用于注册、登录页面 |
@RequiresUser | @RequiresUserpublic void profile() { ... } | 要求是”用户”(已认证 或 RememberMe) | 比 @RequiresAuthentication 宽松 |
@RequiresRoles | @RequiresRoles("admin")public void deleteUser(Long id) { ... } | 要求拥有指定角色 | 支持多个:@RequiresRoles({"admin", "super"})(默认逻辑 AND) |
@RequiresPermissions | @RequiresPermissions("user:delete")public void removeUser(Long id) { ... } | 要求拥有指定权限 | 支持多个:@RequiresPermissions({"user:view", "user:delete"})(默认 AND) |
| 逻辑关系配置 | @RequiresRoles(value={"admin","editor"}, logical=Logical.OR) | 多角色/权限间使用 OR 逻辑 | 默认为 Logical.AND;需显式指定 logical = Logical.OR |
| AOP 依赖 | 需启用 Shiro 注解支持(必须配置,否则注解被忽略) | 使注解生效 | 必须配置,否则注解被忽略 |
AOP 启用配置示例:
// Java Config:
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
<!-- XML: -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">...</bean>
| 异常类型 | 抛出 AuthorizationException 子类(如 UnauthorizedException) | 可全局捕获统一处理 | 建议配合 Spring 的 @ControllerAdvice 处理 |
| --- | --- | --- |
| 使用位置 | 只能用于类或 public 方法 | 不支持 private/protected 方法 | 因基于代理实现(JDK Proxy 或 CGLIB) |
| 性能影响 | 每次调用前执行授权检查 | 高频方法慎用;可结合缓存优化 | 检查结果默认不缓存 |
第五章:会话管理(Session Management)
5.1 Session 对象的获取与使用
| 操作名称 | 方法/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 获取当前会话(自动创建) | Session session = SecurityUtils.getSubject().getSession(); | 获取与当前 Subject 关联的 Session;若不存在则自动创建 | 默认行为等价于 getSession(true) |
| 获取当前会话(不自动创建) | Session session = SecurityUtils.getSubject().getSession(false); | 仅当已存在会话时返回,否则返回 null | 适用于检查会话是否存在而不触发创建 |
| 设置会话属性 | session.setAttribute("userId", 1001);session.setAttribute("cart", shoppingCart); | 在会话中存储任意对象(键值对) | 值需可序列化(若用于分布式环境) |
| 获取会话属性 | Object userId = session.getAttribute("userId");String username = (String) session.getAttribute("username"); | 从会话中读取数据 | 若 key 不存在,返回 null |
| 删除会话属性 | session.removeAttribute("tempToken"); | 移除指定 key 的属性 | 不影响其他属性 |
| 获取会话 ID | Serializable id = session.getId(); // 通常为 String | 获取唯一会话标识符 | 默认由 JavaUuidSessionIdGenerator 生成(UUID) |
| 获取会话创建时间 | Date startTime = session.getStartTimestamp(); | 获取会话创建时间戳 | 返回 java.util.Date |
| 获取最后访问时间 | Date lastAccess = session.getLastAccessTime(); | 获取最近一次访问时间 | 每次调用 Session 方法都会更新 |
| 会话超时 | int timeoutSec = session.getTimeout(); // 默认 1800000 ms = 30 分钟session.setTimeout(600000); // 设置为 10 分钟 | 获取或设置会话最大空闲时间 | 单位为毫秒;设为 -1 表示永不过期(不推荐) |
| 判断会话是否过期 | Shiro 会在访问过期会话时抛出异常 | 手动判断会话有效性 | 无直接方法;可通过 try-catch 或检查操作是否抛出 InvalidSessionException |
5.2 会话生命周期管理
| 生命周期阶段 | 操作细节 | 注意事项 |
|---|---|---|
| 创建 | 调用 subject.getSession() 或登录成功后自动创建 | 由 SessionManager.start(SessionContext) 触发 |
| 活跃 | 每次访问 Session(如 getAttribute)会更新 lastAccessTime | 空闲超时从最后一次访问开始计时 |
| 过期 | 超过 timeout 时间未访问,下次访问时被标记为无效 | Shiro 不主动清理,而是在访问时检测并抛出 InvalidSessionException |
| 销毁(主动) | subject.logout(); 或 session.stop(); | 清除所有属性,释放资源;logout() 同时清除身份信息 |
| 全局超时配置 | [main]securityManager.sessionManager.globalSessionTimeout=1800000 | 设置所有会话默认超时(30 分钟) |
| 会话监听器 | 实现 SessionListener 接口配置: securityManager.sessionManager.sessionListeners=$myListener | 监听会话创建、停止、过期事件 |
会话监听器示例:
public class MySessionListener implements SessionListener {
public void onStart(Session session) { log("start: " + session.getId()); }
public void onStop(Session session) { ... }
public void onExpiration(Session session) { ... }
}
| 验证任务(可选) | [main]sessionValidationScheduler=org.apache.shiro.session.mgt.ExecutorServiceSessionValidationSchedulersessionValidationScheduler.interval=3600000securityManager.sessionManager.sessionValidationScheduler=$sessionValidationScheduler | 定期扫描并清理过期会话(避免内存泄漏) |
5.3 分布式会话支持(SessionDAO、Redis 集成)
| 组件/配置 | 代码/配置示例 | 用途 | 注意事项 |
|---|---|---|---|
| SessionDAO 接口 | 自定义实现需继承 CachingSessionDAO 或实现 SessionDAO | 提供会话的持久化 CRUD 操作 | 核心方法:create、readSession、update、delete |
| MemorySessionDAO | 默认实现,将会话存储在 JVM 内存中 | 仅适用于单机应用 | 不支持集群 |
| EnterpriseCacheSessionDAO | 支持与 Shiro 缓存集成(如 Ehcache、Redis) | 分布式会话基础 | 需配合 CacheManager 使用 |
| Redis 集成依赖 | <dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>3.4.1</version></dependency> | 第三方库简化 Redis 集成 | 非 Apache 官方维护,但社区广泛使用 |
| Redis 配置(shiro.ini) | [main]redisManager=org.crazycake.shiro.RedisManagerredisManager.host=127.0.0.1redisManager.port=6379redisManager.expire=1800redisSessionDAO=org.crazycake.shiro.RedisSessionDAOredisSessionDAO.redisManager=$redisManagersecurityManager.sessionManager.sessionDAO=$redisSessionDAOcacheManager=org.crazycake.shiro.RedisCacheManagercacheManager.redisManager=$redisManagersecurityManager.cacheManager=$cacheManager | 将会话和缓存存储到 Redis | expire 单位为秒;需确保 Redis 可访问 |
自定义 RedisSessionDAO 核心逻辑:
public class RedisSessionDAO extends CachingSessionDAO {
@Override
protected void doCreate(Session session) {
redisTemplate.opsForValue().set(
session.getId(), session, timeout, TimeUnit.MILLISECONDS);
}
@Override
protected Session doReadSession(Serializable sessionId) {
return redisTemplate.opsForValue().get(sessionId);
}
// 实现 update/delete…
}
| 会话 ID 生成器 | [main]sessionIdGenerator=org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGeneratorsessionDAO.sessionIdGenerator=$sessionIdGenerator | 自定义会话 ID 生成策略 | 默认 UUID;可替换为雪花算法等 |
| --- | --- | --- |
| 序列化要求 | 所有存入 Session 的对象(包括 Principal)必须实现 java.io.Serializable | 确保跨 JVM 传输安全 | 否则 Redis 存储会失败 |
| 集群亲和性 | 无状态设计:任何节点均可处理任一会话请求 | 得益于集中式存储(Redis) | 避免使用 Servlet 容器原生 Session |
| 性能优化 | 启用 CachingSessionDAO 的本地缓存(如 Caffeine)减少 Redis 访问 | 读多写少场景有效 | 需处理缓存一致性问题 |
第六章:加密与密码安全(Cryptography)
6.1 哈希算法(MD5、SHA、加盐)
| 概念/操作 | 说明/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 哈希(Hash) | 单向函数,将任意长度输入映射为固定长度输出(如 MD5 → 128 位) | 密码存储、数据完整性校验 | 不可逆;相同输入始终产生相同输出 |
| MD5 | new SimpleHash("MD5", "password"); | 生成 128 位(32 字符十六进制)哈希值 | 已不安全,存在碰撞攻击,禁止用于密码存储 |
| SHA-1 | new SimpleHash("SHA-1", "password"); | 生成 160 位(40 字符)哈希值 | 已破解,不推荐用于安全场景 |
| SHA-256 | new SimpleHash("SHA-256", "password"); | 生成 256 位(64 字符)哈希值 | 当前推荐标准,安全性高 |
| SHA-512 | new SimpleHash("SHA-512", "password"); | 生成 512 位(128 字符)哈希值 | 更高安全强度,计算开销略大 |
| 加盐(Salt) | 随机字符串,与密码拼接后哈希:String salt = "random123";new SimpleHash("SHA-256", "password", salt, 1024); | 防止彩虹表攻击、相同密码暴露 | 盐值必须唯一且随机(每用户不同) |
| 多次迭代(Hashing Iterations) | 第 4 个参数指定迭代次数(如 1024、10000) | 增加暴力破解成本 | 推荐 ≥ 1000 次;Shiro 默认为 1 |
| 盐值存储 | 将盐值与哈希结果一同存入数据库:{ id: 1, username: "zhang", password: "a1b2...", salt: "random123" } | 登录时需用相同盐值重新哈希比对 | 盐值无需保密,可明文存储 |
| 安全密码实践 | 使用 SHA-256/512 + 随机盐 + ≥1000 次迭代 | 构建抗破解的密码存储机制 | 永远不要存储明文密码 |
6.2 密码匹配器(HashedCredentialsMatcher)
| 配置/方法 | 代码/配置示例 | 用途 | 注意事项 |
|---|---|---|---|
| 声明匹配器 | [main]credentialsMatcher=org.apache.shiro.authc.credential.Sha256CredentialsMatcher | 指定使用 SHA-256 进行密码比对 | 也可用 Md5CredentialsMatcher(不推荐) |
| 设置哈希次数 | credentialsMatcher.hashIterations=1024 | 配置迭代次数 | 必须与注册时哈希次数一致 |
| 启用十六进制编码 | credentialsMatcher.storedCredentialsHexEncoded=true | 指定存储的哈希值为十六进制字符串(默认) | 若为 Base64,设为 false |
| 绑定到 Realm | myRealm.credentialsMatcher=$credentialsMatcher | 使自定义 Realm 使用该匹配器 | 必须在 Realm 配置之后设置 |
| 自动加盐支持 | Realm 返回 SimpleAuthenticationInfo 时传入盐值:return new SimpleAuthenticationInfo(username, hashedPass, ByteSource.Util.bytes(salt), getName()); | 匹配器自动使用盐值重新哈希用户输入 | 盐值必须为 ByteSource 类型 |
| 自定义匹配器 | 继承 HashedCredentialsMatcher 并重写 doCredentialsMatch | 实现特殊比对逻辑(如兼容旧系统) | 一般无需自定义 |
| 匹配流程 | 1. 用户提交密码 2. Shiro 用 Realm 返回的盐 + 迭代次数 + 算法重新哈希 3. 与 Realm 返回的存储哈希值比对 | 自动完成,开发者无需手动哈希 | 确保注册和登录使用完全相同的参数 |
6.3 加密工具类(CodecSupport、SimpleHash)
| 工具类/方法 | 语法/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| SimpleHash(核心哈希工具) | SimpleHash hash = new SimpleHash("SHA-256", "password", "salt", 1024);String hex = hash.toHex();byte[] bytes = hash.getBytes(); | 生成指定算法、盐、迭代次数的哈希值 | 支持算法:MD5, SHA-1, SHA-256, SHA-512 等 |
| SimpleHash 构造器 | new SimpleHash(algorithm)new SimpleHash(algorithm, source)new SimpleHash(algorithm, source, salt)new SimpleHash(algorithm, source, salt, iterations) | 灵活构造哈希对象 | source 和 salt 可为 Object、byte[]、String |
| CodecSupport(编解码工具) | String hex = CodecSupport.encodeHex(bytes);byte[] decoded = CodecSupport.decodeHex("a1b2c3");String base64 = CodecSupport.encodeBase64(bytes);byte[] fromB64 = CodecSupport.decodeBase64("abc..."); | 提供十六进制和 Base64 编解码 | 内部工具类,通常通过 SimpleHash 间接使用 |
| Hex 工具类 | String hex = Hex.encodeToString("hello".getBytes());byte[] data = Hex.decode("68656c6c6f"); | 专用十六进制编解码 | 位于 org.apache.shiro.codec.Hex |
| Base64 工具类 | String b64 = Base64.encodeToString("hello".getBytes());byte[] data = Base64.decode("aGVsbG8="); | 专用 Base64 编解码 | 位于 org.apache.shiro.codec.Base64 |
| 生成随机盐值 | SecureRandomNumberGenerator gen = new SecureRandomNumberGenerator();String salt = gen.nextBytes(16).toHex(); | 创建 16 字节(128 位)安全随机盐 | 推荐盐长度 ≥ 8 字节 |
| 密码注册示例 | String rawPassword = "123456";String salt = new SecureRandomNumberGenerator().nextBytes(16).toHex();SimpleHash hash = new SimpleHash("SHA-256", rawPassword, salt, 1024);String hashedPassword = hash.toHex();// 存储 hashedPassword 和 salt 到数据库 | 用户注册时安全存储密码 | 迭代次数应与登录时匹配器配置一致 |
| 兼容性注意 | 不同语言/框架的哈希实现可能字节序或编码不同 | 跨平台系统需统一规范 | 建议使用标准算法(如 PBKDF2),Shiro 原生未提供但可集成 |
安全建议:虽然 Shiro 提供了基础哈希能力,但在现代应用中,更推荐使用专门的密码哈希函数如 bcrypt、scrypt 或 Argon2(Shiro 通过自定义
CredentialsMatcher可集成)。这些算法内置加盐和自适应计算强度,安全性远高于简单 SHA+迭代。
第七章:Web 应用集成
7.1 Shiro 与 Servlet 容器集成
| 集成方式 | 配置示例(web.xml) | 用途 | 注意事项 |
|---|---|---|---|
| 使用 IniShiroFilter(Shiro 1.1 及以前) | <filter><filter-name>shiroFilter</filter-name><filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class><init-param><param-name>configPath</param-name><param-value>classpath:shiro.ini</param-value></init-param></filter><filter-mapping><filter-name>shiroFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping> | 从 INI 文件加载配置并初始化 SecurityManager | 默认从 /WEB-INF/shiro.ini 或 classpath:shiro.ini 加载 |
| 使用 EnvironmentLoaderListener(Shiro 1.2+) | <listener><listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class></listener><filter><filter-name>shiroFilter</filter-name><filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class></filter><filter-mapping><filter-name>shiroFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping> | 通过 WebEnvironment 自动绑定 SecurityManager 到 ServletContext | 支持自定义 WebEnvironment 实现 |
| 自定义配置路径 | <context-param><param-name>shiroConfigLocations</param-name><param-value>classpath:my-shiro.ini</param-value></context-param> | 指定非默认的 INI 文件位置 | 仅在使用 EnvironmentLoaderListener 时生效 |
| 与 Spring 集成 | <filter><filter-name>shiroFilter</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> | 将 ShiroFilter 委托给 Spring 容器管理 | 需在 Spring 配置中定义名为 shiroFilter 的 ShiroFilterFactoryBean |
| 最小依赖 | <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.13.0</version></dependency> | 提供 Web 集成所需类(Filter、Session 管理等) | 必须与 Servlet API 兼容(≥ 2.5) |
| Filter 映射顺序 | <url-pattern>/*</url-pattern> | 拦截所有请求,确保安全控制前置 | 必须放在其他业务 Filter 之前 |
7.2 URL 过滤器链配置(authc、anon、roles、perms)
| 过滤器名称 | 配置语法(shiro.ini [urls] 段) | 用途 | 注意事项 |
|---|---|---|---|
anon | /static/** = anon/login.jsp = anon | 匿名访问,无需认证即可访问 | 通常用于静态资源、登录页 |
authc | /user/profile = authc/admin/** = authc | 表单认证;未登录则重定向到 loginUrl | 默认跳转页为 /login.jsp,可配置 authc.loginUrl=/login |
authcBasic | /api/** = authcBasic | HTTP Basic 认证(弹出浏览器登录框) | 适用于 REST API 或内部系统 |
logout | /logout = logout | 处理登出请求;调用 subject.logout() 并重定向 | 默认重定向到 /,可配置 logout.redirectUrl=/index |
roles[role1,role2] | /admin/** = authc,roles[admin]/audit = authc,roles[auditor,admin] | 角色检查;用户必须拥有指定角色 | 多角色默认为 AND 逻辑;不支持 OR |
perms["perm1","perm2"] | /user/delete = authc,perms["user:delete"]/order/** = authc,perms["order:*"] | 权限检查;用户必须拥有指定权限 | 权限字符串需加双引号(INI 格式要求) |
user | /profile = user | 要求是”用户”(已认证 或 RememberMe) | 比 authc 宽松 |
port[portNum] | /secure = port[443], authc | 限制请求必须来自指定端口 | 通常用于强制 HTTPS |
| 方法限定 | authcBasic[POST,PUT,DELETE] | 仅对特定 HTTP 方法应用过滤器 | 示例:/api/** = authcBasic[POST,DELETE], perms["api:write"] |
| 自定义过滤器 | [main]myFilter=com.example.MyFilter[urls]/custom = myFilter | 注册并使用自定义 Filter | 需继承 AccessControlFilter 或实现 Filter |
| 匹配顺序 | 从上到下匹配,第一个匹配成功即生效:/admin/user = roles[admin]/admin/** = authc→ 请求 /admin/user 不会走到第二行 | 更具体的路径应放在前面 | 否则可能被通配符提前拦截 |
7.3 登录/登出流程实现
| 步骤 | 代码示例(Servlet) | 说明 | 注意事项 |
|---|---|---|---|
| 跳转登录页 | @WebServlet("/toLogin")public class LoginViewServlet extends HttpServlet {protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {req.getRequestDispatcher("/login.jsp").forward(req, resp);}} | 渲染登录表单页面 | 该 URL 需配置为 anon |
| 执行登录 | @WebServlet("/login")public class LoginServlet extends HttpServlet {protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {String username = req.getParameter("username");String password = req.getParameter("password");Subject subject = SecurityUtils.getSubject();UsernamePasswordToken token = new UsernamePasswordToken(username, password);try {subject.login(token);resp.sendRedirect("/dashboard");} catch (AuthenticationException e) {resp.sendRedirect("/login?error=1");}}} | 处理登录请求,调用 subject.login() | 捕获异常并友好提示;避免暴露具体错误原因 |
| 获取登录前地址 | String savedRequest = (String) subject.getSession().getAttribute("shiroSavedRequest"); | Shiro 自动保存登录前的请求 URL | 登录成功后可重定向回原页面 |
| 执行登出 | @WebServlet("/logout")// 或直接配置 /logout = logout | 调用 subject.logout() 并清除会话 | 若使用 logout 过滤器,无需写代码 |
| Remember Me | 表单中添加 <input type="checkbox" name="rememberMe">Token 构造: new UsernamePasswordToken(user, pass, true) | 实现”记住我”功能 | 需在 Realm 中返回 SimplePrincipalCollection 且 Principal 可序列化 |
| 登录成功回调 | 无直接事件;可在登录后手动记录日志或更新最后登录时间 | 业务扩展点 | 可结合 AOP 或自定义 Authenticator |
7.4 JSP 标签库使用(shiro:hasRole、shiro:principal 等)
| 标签名称 | JSP 语法示例 | 用途 | 注意事项 |
|---|---|---|---|
<%@ taglib ... %> | <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> | 页面顶部声明,引入 Shiro JSP 标签库 | 必须先声明才能使用其他标签 |
<shiro:guest> | <shiro:guest><a href="/login">登录</a></shiro:guest> | 仅当游客(未认证且未 RememberMe)时显示内容 | — |
<shiro:user> | <shiro:user>Hello, <shiro:principal/></shiro:user> | 仅当是”用户”(认证或 RememberMe)时显示 | — |
<shiro:authenticated> | <shiro:authenticated><a href="/logout">退出</a></shiro:authenticated> | 仅当已通过认证(非 RememberMe)时显示 | — |
<shiro:notAuthenticated> | <shiro:notAuthenticated>Please log in.</shiro:notAuthenticated> | 未认证时显示 | — |
<shiro:principal> | <shiro:principal/><shiro:principal property="username"/> | 输出当前用户身份信息(Principal) | 若 Principal 是对象,可用 property 指定字段(需有 getter) |
<shiro:hasRole name="admin"> | <shiro:hasRole name="admin"><a href="/admin">管理后台</a></shiro:hasRole> | 拥有指定角色时显示 | — |
<shiro:lacksRole name="guest"> | <shiro:lacksRole name="guest">高级功能</shiro:lacksRole> | 不拥有指定角色时显示 | — |
<shiro:hasAnyRoles name="admin,editor"> | <shiro:hasAnyRoles name="admin,editor">编辑入口</shiro:hasAnyRoles> | 拥有任意一个角色时显示 | 角色名用逗号分隔 |
<shiro:hasPermission name="user:delete"> | <shiro:hasPermission name="user:delete"><button>Delete</button></shiro:hasPermission> | 拥有指定权限时显示 | — |
<shiro:lacksPermission name="user:delete"> | <shiro:lacksPermission name="user:delete">无权操作</shiro:lacksPermission> | 不拥有指定权限时显示 | — |
| 标签嵌套 | <shiro:hasRole name="admin"><shiro:hasPermission name="log:view">审计日志</shiro:hasPermission></shiro:hasRole> | 组合条件控制 | 支持任意嵌套 |
注意:JSP 标签库需引入
shiro-web依赖,且 JSP 页面必须运行在 Servlet 容器中。前后端分离项目(如 Vue/React)不适用,应改用 AJAX 接口返回权限数据由前端控制。
第八章:与 Spring / Spring Boot 集成
8.1 Spring 配置方式(XML 与 Java Config)
| 配置方式 | 代码/配置示例 | 用途 | 注意事项 |
|---|---|---|---|
| XML 配置 SecurityManager | <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><property name="realm" ref="myRealm"/></bean><bean id="myRealm" class="com.example.MyRealm"/> | 在 Spring XML 中声明核心安全组件 | 需手动绑定到 ShiroFilter |
| XML 配置 ShiroFilter | <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><property name="securityManager" ref="securityManager"/><property name="filterChainDefinitions"><value>/login = anon/admin/** = authc,roles[admin]</value></property></bean> | 定义 URL 过滤器链 | ShiroFilterFactoryBean 会生成实际的 Filter 实例 |
| XML 启用注解授权 | <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/><bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"><property name="securityManager" ref="securityManager"/></bean> | 使 @RequiresRoles 等注解生效 | 必须配置,否则注解被忽略 |
| Java Config(@Configuration) | @Configurationpublic class ShiroConfig {@Bean public Realm myRealm() { return new MyRealm(); }@Bean public SessionsSecurityManager securityManager() {DefaultWebSecurityManager sm = new DefaultWebSecurityManager();sm.setRealm(myRealm()); return sm; }} | 使用 Java 类替代 XML 配置 | 推荐方式,类型安全 |
| Java Config 注册 Filter | @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();bean.setSecurityManager(securityManager);Map<String, String> chains = new LinkedHashMap<>();chains.put("/login", "anon");chains.put("/admin/**", "authc,roles[admin]");bean.setFilterChainDefinitionMap(chains); return bean; } | 动态构建过滤器链 | 返回类型为 ShiroFilterFactoryBean,Spring 会自动注册为 Filter |
| Java Config 启用注解 | @Bean @DependsOn("lifecycleBeanPostProcessor")public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {return new DefaultAdvisorAutoProxyCreator(); }@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager); return advisor; } | 启用方法级授权注解 | @DependsOn 确保生命周期处理器先初始化 |
8.2 Spring Boot 自动配置(shiro-spring-boot-web-starter)
| 配置项 | Maven 依赖 / 配置示例 | 用途 | 注意事项 |
|---|---|---|---|
| Starter 依赖 | <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-web-starter</artifactId><version>1.13.0</version></dependency> | 自动配置 Shiro Web 环境(SecurityManager、Filter 等) | 版本需与 Spring Boot 兼容(1.13.0 支持 SB 2.x/3.x) |
| 自动配置类 | ShiroWebAutoConfiguration | 自动创建 SecurityManager、ShiroFilter 等 Bean | 若自定义了 SecurityManager,自动配置将退避 |
| application.yml 配置 | shiro:enabled: trueweb:enabled: true | 启用 Shiro Web 支持(默认已启用) | 可通过 shiro.enabled=false 关闭 |
| 过滤器链配置(YAML) | shiro:filterChainDefinition:"/login": anon"/admin/**": "authc,roles[admin]" | 在 YAML 中定义 URL 权限规则 | 键为路径,值为过滤器链字符串 |
| 自动绑定 Realm | 只需将自定义 Realm 声明为 @Component 或 @Bean | Starter 会自动将其注入 SecurityManager | 支持多个 Realm,按顺序组合 |
| 禁用自动配置 | @SpringBootApplication(exclude = ShiroWebAutoConfiguration.class) | 完全接管 Shiro 配置 | 适用于复杂定制场景 |
| 默认行为 | 自动注册 ShiroFilter 到 /*,启用注解支持 | 开箱即用 | 无需额外配置 Filter 映射 |
8.3 自定义 Realm 注入与 SecurityManager 配置
| 操作 | 代码示例(Spring Boot) | 说明 | 注意事项 |
|---|---|---|---|
| 自定义 Realm 声明 | @Componentpublic class DbRealm extends AuthorizingRealm {@Autowired private UserService userService;@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { ... }@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { ... }} | 使用 Spring 注解注入依赖(如 Service) | 必须标注 @Component 或通过 @Bean 返回 |
| 手动配置 SecurityManager | @Beanpublic SessionsSecurityManager securityManager(DbRealm dbRealm) {DefaultWebSecurityManager manager = new DefaultWebSecurityManager();manager.setRealm(dbRealm); return manager; } | 显式控制 SecurityManager 构建过程 | 若存在多个 Realm,使用 setRealms(List<Realm>) |
| 配置 CredentialsMatcher | @Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher() {HashedCredentialsMatcher matcher = new Sha256CredentialsMatcher();matcher.setHashIterations(1024); return matcher; }// 在 Realm 中注入@Autowiredpublic void setMatcher(HashedCredentialsMatcher matcher) {this.setCredentialsMatcher(matcher); } | 分离密码匹配逻辑 | Matcher 需与注册时哈希参数一致 |
| 多 Realm 配置 | @Beanpublic SessionsSecurityManager securityManager(LdapRealm ldap, DbRealm db) {DefaultWebSecurityManager sm = new DefaultWebSecurityManager();sm.setRealms(Arrays.asList(ldap, db));sm.setAuthenticator(new ModularRealmAuthenticator()); return sm; } | 组合 LDAP + 数据库认证 | 需显式设置 ModularRealmAuthenticator |
| 缓存集成 | @Beanpublic CacheManager cacheManager() {return new MemoryConstrainedCacheManager(); // 或 Redis/Ehcache 实现}// 在 SecurityManager 中设置manager.setCacheManager(cacheManager()); | 启用认证/授权结果缓存 | 提升性能,避免重复查询数据库 |
8.4 前后端分离场景下的无状态认证(JWT + Shiro)
| 组件/步骤 | 代码示例 | 说明 | 注意事项 |
|---|---|---|---|
| 自定义 Token | public class JwtToken implements AuthenticationToken {private String token;public JwtToken(String jwt) { this.token = jwt; }@Override public Object getPrincipal() { return token; }@Override public Object getCredentials() { return token; }} | 封装 JWT 字符串为 Shiro Token | 无用户名/密码,仅携带令牌 |
| 自定义 JWT Realm | public class JwtRealm extends AuthorizingRealm {@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JwtToken; }@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String jwt = (String) token.getPrincipal();if (!JwtUtil.verify(jwt)) throw new IncorrectCredentialsException();String username = JwtUtil.getUsername(jwt);return new SimpleAuthenticationInfo(username, jwt, getName()); }@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {String username = (String) principals.getPrimaryPrincipal();// 从 DB 加载权限return new SimpleAuthorizationInfo(roles); }} | 验证 JWT 并返回身份/权限 | JwtUtil 需自行实现(含签名验证、过期检查) |
| 登录接口(颁发 JWT) | @PostMapping("/login")public String login(@RequestBody LoginDTO dto) {// 验证用户名密码(可复用传统 Realm)if (valid) {return JwtUtil.generateToken(username); }throw new RuntimeException("Login failed"); } | 成功登录后返回 JWT 字符串 | 不调用 subject.login(),仅用于凭证校验 |
| 认证过滤器(JWT) | public class JwtFilter extends BasicHttpAuthenticationFilter {@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {String token = getToken(request);if (token != null && JwtUtil.verify(token)) {Subject subject = getSubject(request, response);if (!subject.isAuthenticated()) {JwtToken jwtToken = new JwtToken(token);try { subject.login(jwtToken); } catch (Exception e) { ... }}return true; }return false; }} | 从 Header 解析 JWT 并自动登录 | 继承 BasicHttpAuthenticationFilter 简化实现 |
| 注册 JWT Filter | @Beanpublic ShiroFilterFactoryBean shiroFilter(SecurityManager sm) {ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();bean.setSecurityManager(sm);Map<String, Filter> filters = new HashMap<>();filters.put("jwt", new JwtFilter());bean.setFilters(filters);Map<String, String> chains = new LinkedHashMap<>();chains.put("/login", "anon");chains.put("/**", "jwt"); // 所有请求走 JWT 校验bean.setFilterChainDefinitionMap(chains); return bean; } | 将自定义 Filter 注入 Shiro | 路径 /** 需放在最后 |
| 前端使用 | axios.defaults.headers.common['Authorization'] = 'Bearer ' + jwt; | 每次请求携带 JWT | 通常放在 Authorization Header |
| 无 Session 设计 | @Beanpublic SessionsSecurityManager securityManager() {DefaultWebSecurityManager sm = new DefaultWebSecurityManager();sm.setSessionManager(new NoOpSessionManager()); // 禁用会话return sm; } | 完全无状态,不创建 Session | 提升扩展性,适合微服务 |
关键点:在无状态场景中,Shiro 仅作为认证/授权框架,不管理会话。每次请求都需携带有效 JWT,由自定义 Realm 验证并重建 Subject 上下文。
第九章:高级特性与最佳实践
9.1 缓存机制(CacheManager、EhCache/Redis 集成)
| 组件/操作 | 配置/代码示例 | 用途 | 注意事项 |
|---|---|---|---|
| 启用缓存(INI) | [main]cacheManager=org.apache.shiro.cache.MemoryConstrainedCacheManagersecurityManager.cacheManager=$cacheManagermyRealm.cachingEnabled=true | 为 Realm 启用认证/授权结果缓存 | MemoryConstrainedCacheManager 是内存限制版,适用于小型应用 |
| EhCache 集成(依赖) | <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>1.13.0</version></dependency> | 使用 EhCache 作为缓存后端 | 自动包含 EhCache 依赖 |
| EhCache 配置(INI) | [main]cacheManager=org.apache.shiro.cache.ehcache.EhCacheManagercacheManager.cacheManagerConfigFile=classpath:ehcache.xmlsecurityManager.cacheManager=$cacheManager | 指定 EhCache 配置文件 | ehcache.xml 需定义名为 shiro-activeSessionCache 等的缓存 |
| Redis 集成(第三方) | <dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>3.4.1</version></dependency> | 将缓存存储到 Redis(支持集群) | 非 Apache 官方,但广泛使用 |
| Redis 缓存配置(INI) | [main]redisManager=org.crazycake.shiro.RedisManagerredisManager.host=127.0.0.1redisManager.port=6379cacheManager=org.crazycake.shiro.RedisCacheManagercacheManager.redisManager=$redisManagersecurityManager.cacheManager=$cacheManager | 全局缓存管理器指向 Redis | 同时支持 Session 和 Authorization 缓存 |
| Spring Boot 中配置 | @Bean public CacheManager cacheManager() {return new RedisCacheManager(redisTemplate); // 自定义实现}@Bean public SecurityManager securityManager(CacheManager cm) {DefaultWebSecurityManager sm = new DefaultWebSecurityManager();sm.setCacheManager(cm); sm.setRealm(myRealm()); return sm; } | 在 Java Config 中注入自定义 CacheManager | Realm 需启用缓存:realm.setCachingEnabled(true) |
| 缓存键生成 | 默认使用 PrincipalCollection 的 toString() 作为缓存键 | 确保 Principal 可序列化且 toString() 唯一 | 若 Principal 是复杂对象,建议重写 toString() |
| 缓存失效 | 用户修改权限后需手动清除缓存:realm.getAuthorizationCache().remove(principal); | 保证权限变更实时生效 | 可结合消息队列实现分布式缓存清理 |
| 性能收益 | 避免重复查询数据库获取角色/权限 | 对高频授权检查(如 API)提升显著 | 缓存命中率是关键指标 |
9.2 多 Realm 认证策略(AtLeastOneSuccessfulStrategy 等)
| 策略类 | 配置方式(INI) | 行为说明 | 适用场景 |
|---|---|---|---|
| AtLeastOneSuccessfulStrategy | [main]authcStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategyauthenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticatorauthenticator.authenticationStrategy=$authcStrategysecurityManager.authenticator=$authenticator | 只要一个 Realm 认证成功即整体成功(默认策略) | 混合登录(如本地账号 + LDAP) |
| AllSuccessfulStrategy | authcStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy | 所有 Realm 必须全部认证成功 | 强安全要求(如双因子:密码 + 短信) |
| FirstSuccessfulStrategy | authcStrategy=org.apache.shiro.authc.pam.FirstSuccessfulStrategy | 仅第一个成功的 Realm 被采纳,后续 Realm 不执行 | 性能优化,避免多余查询 |
| 自定义策略 | public class CustomStrategy extends AbstractAuthenticationStrategy {@Overridepublic AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t) throws AuthenticationException {// 自定义聚合逻辑return super.afterAttempt(realm, token, info, aggregate, t); }} | 实现特殊认证逻辑(如加权投票) | 极少数定制需求 |
| Realm 顺序控制 | securityManager.realms=$realm1,$realm2 | Realm 执行顺序由列表顺序决定 | FirstSuccessfulStrategy 依赖此顺序 |
| 异常处理 | 若 Realm 抛出异常(如 DB 连接失败),策略决定是否继续 | AtLeastOneSuccessful 会跳过失败 Realm 继续尝试 | 需确保 Realm 异常不影响整体流程 |
| 授权策略 | 同样存在 ModularRealmAuthorizer 和 AuthorizationStrategy | 控制多 Realm 授权结果合并方式 | 默认为”任一 Realm 授权即通过” |
9.3 “Remember Me” 功能实现
| 配置/操作 | 代码/配置示例 | 说明 | 注意事项 |
|---|---|---|---|
| 表单勾选框 | <input type="checkbox" name="rememberMe"> 记住我 | 前端提供用户选择 | 名称必须为 rememberMe(Shiro 默认识别) |
| Token 构造 | new UsernamePasswordToken(username, password, true); | 第三个参数为 rememberMe 标志 | 设为 true 启用 Remember Me |
| Cookie 配置(INI) | [main]cookie=org.apache.shiro.web.servlet.SimpleCookiecookie.name=rememberMecookie.maxAge=2592000 ; 30天rememberMeManager=org.apache.shiro.web.mgt.CookieRememberMeManagerrememberMeManager.cookie=$cookiesecurityManager.rememberMeManager=$rememberMeManager | 自定义 Remember Me Cookie 名称和有效期 | 默认有效期为 1 年;单位为秒 |
| 加密密钥 | rememberMeManager.cipherKey=... | 设置 AES 加密密钥(必须 16 字节) | 默认使用弱密钥,生产环境必须替换:cipherKey=Base64.decode("kPH+bIxk5D2deZiIxcaaaA==") |
| 判断状态 | subject.isRemembered() | 检查是否为 Remember Me 登录 | 与 isAuthenticated() 互斥(通常不会同时为 true) |
| 权限限制 | Remember Me 用户不应拥有敏感操作权限 | 例如:禁止转账、修改密码 | 可在授权检查中排除 Remember Me 用户:if (subject.isRemembered()) return false; |
| 安全风险 | Cookie 可被窃取导致账户被盗 | 切勿在公共电脑启用 | 敏感系统应禁用此功能 |
| 与 JWT 区别 | Remember Me 依赖 Cookie,有状态;JWT 无状态 | 两者不兼容 | 前后端分离项目通常用 JWT 替代 |
9.4 安全事件监听与审计
| 监听器类型 | 实现方式 | 监听事件 | 用途 |
|---|---|---|---|
| AuthenticatorListener | public class MyAuthcListener implements AuthenticatorListener {public void onSuccess(AuthenticationToken token, AuthenticationInfo info) { ... }public void onFailure(AuthenticationToken token, AuthenticationException ae) { ... }} | 认证成功/失败 | 记录登录日志、触发告警、锁定账户 |
| AuthorizerListener | public class MyAuthzListener implements AuthorizerListener {public void onSuccess(PrincipalCollection principals, String permission) { ... }public void onFailure(PrincipalCollection principals, String permission) { ... }} | 授权成功/失败 | 审计权限使用、检测越权行为 |
| SessionListener | (见 5.2 节) | 会话创建/停止/过期 | 统计在线用户、清理资源 |
| 注册监听器(INI) | [main]authcListener=com.example.MyAuthcListenersecurityManager.authenticator.authenticatorListeners=$authcListener | 将监听器注入 Authenticator | 支持多个监听器(List 属性) |
| Spring 中注册 | @Beanpublic AuthenticatorListener authcListener() { return new MyAuthcListener(); }// 在 SecurityManager 配置中:authenticator.setAuthenticatorListeners(Arrays.asList(authcListener())); | 通过 DI 管理监听器 | 更灵活,可注入其他 Service |
| 审计日志内容 | - 用户名 - IP 地址( request.getRemoteAddr())- 时间戳 - 操作类型(登录/访问 URL/权限) - 结果(成功/失败) | 完整安全审计追踪 | 日志应写入独立存储(如 ELK) |
| 失败次数统计 | 在 onFailure 中记录失败次数,达到阈值锁定账户 | 防暴力破解 | 需结合缓存(如 Redis)实现分布式计数 |
| 异步处理 | 监听器中调用异步服务(如 @Async) | 避免阻塞主流程 | 审计日志可异步写入,不影响性能 |
第十章:项目实战
10.1 基于 RBAC 的权限管理系统
| 组件 | 设计/代码示例 | 说明 | 注意事项 |
|---|---|---|---|
| 核心实体模型 | 用户(User):id, username, password, enabled 角色(Role):id, name, description 权限(Permission):id, name, resource, action(如 "user:delete")关联表:user_role (user_id, role_id)、role_permission (role_id, permission_id) | 实现 RBAC0 模型(用户-角色-权限三层) | 避免在用户表直接存储角色列表(违反范式) |
| 数据库建表示例 | CREATE TABLE role_permission (role_id INT NOT NULL,permission_id INT NOT NULL,PRIMARY KEY (role_id, permission_id),FOREIGN KEY (role_id) REFERENCES role(id),FOREIGN KEY (permission_id) REFERENCES permission(id)); | 多对多关系通过中间表解耦 | 使用联合主键避免重复绑定 |
| 自定义 Realm 实现 | protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {String username = (String) principals.getPrimaryPrincipal();List<String> roles = roleService.findRolesByUser(username);List<String> perms = permissionService.findPermissionsByUser(username);SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.addRoles(roles);info.addStringPermissions(perms);return info; } | 动态加载用户的角色和权限 | 权限建议使用 resource:action 格式(如 "order:cancel") |
| 权限分配接口 | POST /api/role/{roleId}/permissions请求体: { "permissionIds": [1,2,3] } | 管理员为角色分配权限 | 需校验操作者是否拥有 role:assign 权限 |
| 菜单动态渲染 | 前端调用 /api/user/menu 获取当前用户可见菜单:[{ "name": "用户管理", "path": "/user", "perms": ["user:view"] }] | 基于权限控制菜单显示 | 后端需根据用户权限过滤菜单项 |
| 字段级权限扩展 | 在权限表增加 fields 字段:{ "resource": "employee", "action": "view", "fields": ["name","dept"] } | 控制可查看的字段(如 HR 可看薪资,普通员工不可) | 需在数据查询层实现字段过滤(如 MyBatis 拦截器) |
| 安全实践 | - 密码使用 SHA-256 + salt + 1024 次迭代存储 - 敏感操作(如删除)需二次认证 - 权限变更后清除缓存 | 防止常见安全漏洞 | 避免”超级管理员”角色拥有所有权限(应显式分配) |
10.2 前后端分离 + JWT + Shiro 实战
| 组件 | 实现细节 | 注意事项 |
|---|---|---|
| 登录流程 | 1. 前端提交用户名/密码 2. 后端验证凭证(可复用传统 Realm) 3. 验证成功后生成 JWT 返回 | 不调用 subject.login(),仅用于凭证校验 |
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
// 1. 手动验证密码
if (userService.validate(dto)) {
String jwt = JwtUtil.createToken(dto.getUsername());
return Result.success(jwt);
}
throw new AuthException("Invalid credentials");
}
| JWT 工具类 | 包含生成、解析、验证方法 | SECRET 必须保密且足够长(≥32 字节) |
public static String createToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + 86400000))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public static boolean verify(String token) {
try { Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token); return true; }
catch (Exception e) { return false; }
}
| 自定义 JWT Realm | 验证 Token 并重建 Subject 上下文 | 不查询数据库验证密码,仅验证 Token 签名和有效期 |
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String jwt = (String) token.getCredentials();
if (!JwtUtil.verify(jwt)) throw new IncorrectCredentialsException();
String username = JwtUtil.getUsername(jwt);
return new SimpleAuthenticationInfo(username, jwt, getName());
}
| 无状态过滤器 | 从 Header 解析 JWT 并自动登录 | 继承 BasicHttpAuthenticationFilter 简化实现 |
protected boolean isAccessAllowed(...) {
String authHeader = getRequestHeader(request, "Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String jwt = authHeader.substring(7);
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated()) {
subject.login(new JwtToken(jwt));
}
return true;
}
return false;
}
| 禁用 Session | 配置无操作 NoOpSessionManager | 确保完全无状态,提升水平扩展能力 |
| 刷新 Token 机制 | 登录时返回 access_token(短有效期)和 refresh_token(长有效期)POST /refresh 使用 refresh_token 获取新 access_token | refresh_token 需存储在数据库并设置过期时间,防止滥用 |
| 跨域支持 | 配置 CORS 允许前端域名,allowCredentials=true 时不能使用 * 通配符 | — |
10.3 单点登录(SSO)基础实现思路
| 方案 | 核心思想 | 实现要点 | 局限性 |
|---|---|---|---|
| 基于 Cookie + 域名共享 | 所有子系统部署在同一父域名下(如 a.example.com、b.example.com) | 1. 认证中心(sso.example.com)登录后设置 Cookie 的 domain 为 .example.com2. 各子系统读取该 Cookie 并向认证中心验证有效性 | 必须同父域;无法跨顶级域名(如 example.com 和 other.com) |
| 基于 Token 重定向(CAS 流程简化版) | 1. 应用 A 发现未登录,重定向到 SSO 2. SSO 登录后重定向回 A,并附带 ticket 3. A 用 ticket 向 SSO 验证获取用户信息 | - SSO 需提供 /login 和 /verify?ticket=xxx 接口- ticket 一次性有效且短期过期 - 各应用需配置 SSO 地址 | 需处理重定向循环;首次登录体验较差 |
| Shiro 集成难点 | Shiro 本身不提供 SSO 原生支持 | 需自定义:SSO 认证中心(独立应用)、客户端 Filter(拦截未认证请求并重定向)、Ticket 验证 Realm | 不如 Spring Security OAuth2 成熟 |
| 退出登录同步 | 用户在任一系统登出,需通知其他系统 | 1. SSO 提供 /logout 接口2. 重定向到各已登录系统的 /sso-logout3. 各系统清除本地会话 | 依赖浏览器跳转,可能遗漏;推荐结合 WebSocket 或消息队列 |
| 替代方案建议 | 对于新项目,优先考虑 OAuth2 / OpenID Connect | 使用 Keycloak、Auth0 或 Spring Authorization Server | 标准化程度高,社区支持好;Shiro 更适合传统 Web 应用 |
总结:Shiro 可实现基础 SSO,但复杂度高。在微服务或跨域场景下,建议采用标准协议(OAuth2)替代自研方案。
总结:Shiro 认证流程
一、登录认证流程(Authentication)
登录认证的目标是验证用户身份是否合法,即确认”你是谁”。Shiro 的认证流程遵循清晰的组件协作机制:
- 用户提交凭证:前端或客户端将用户名、密码等信息封装为
UsernamePasswordToken对象。 - 获取 Subject 实例:通过
SecurityUtils.getSubject()获取当前用户主体(Subject)。 - 调用 login 方法:调用
subject.login(token)启动认证流程。 - 委托给 SecurityManager:Subject 将认证请求委托给 SecurityManager。
- Authenticator 执行认证:SecurityManager 再将任务交给 Authenticator(默认为
ModularRealmAuthenticator)。 - Realm 查询数据源:Authenticator 调用配置好的 Realm(如自定义的 UserRealm 或 IniRealm),从数据库、文件或内存中获取用户信息。
- 凭证比对:
- 如果使用缓存(CacheManager),会先尝试从缓存中读取认证信息;
- 若无缓存,则调用 Realm 的
doGetAuthenticationInfo()方法,返回AuthenticationInfo; - Authenticator 比较用户输入的凭证(如密码)与数据源中的凭证是否一致。
- 返回结果:
- 成功则设置
subject.authenticated = true; - 失败则抛出异常,如
UnknownAccountException(账号不存在)、IncorrectCredentialsException(密码错误)等。
- 成功则设置
整个流程可简化为:Subject → SecurityManager → Authenticator → Realm → 数据源
二、权限认证流程(Permission Authorization)
权限认证用于判断用户是否有权执行某项操作,即回答”你能做什么?“。Shiro 支持细粒度的权限控制(如 user:delete、order:view)。
- 用户发起权限校验:通过
subject.isPermitted("user:delete")或subject.checkPermission("user:delete")触发。 - 委托给 SecurityManager:Subject 将请求转发给 SecurityManager。
- Authorizer 执行授权:SecurityManager 调用内部的 Authorizer(默认为
ModularRealmAuthorizer)。 - Realm 获取权限数据:Authorizer 调用 Realm 的
getAuthorizationInfo()方法,获取该用户拥有的所有权限集合(通常来自数据库的权限表)。 - 权限匹配:
- Shiro 支持通配符权限(如
user:*匹配user:add、user:delete); - Authorizer 判断用户权限集合中是否包含所请求的权限。
- Shiro 支持通配符权限(如
- 返回结果:
isPermitted()返回布尔值;checkPermission()若无权限则抛出UnauthorizedException。
典型应用场景:
if (subject.isPermitted("article:publish")) {
// 允许发布文章
}
三、角色认证流程(Role Authorization)
角色认证是权限控制的一种高层抽象,用于判断用户是否属于某个角色(如”管理员”、“运营”),从而间接赋予一组权限。
- 用户发起角色校验:通过
subject.hasRole("admin")或subject.checkRole("admin")。 - 流程与权限类似:Subject → SecurityManager → Authorizer → Realm;Realm 从数据源加载用户的角色列表(如通过用户 ID 关联角色表)。
- 角色匹配:检查用户角色集合中是否包含指定角色。
- 返回结果:
hasRole()返回 true/false;checkRole()若无该角色则抛出UnauthorizedException。hasAllRoles(List<String> roles):必须拥有所有角色才返回 true;hasRoles(List<String> roles):返回boolean[],按顺序对应每个角色是否存在。
补充说明:Realm 的核心作用
无论是认证还是授权,Realm 都是连接 Shiro 与实际数据源的桥梁。开发者通常需要自定义 AuthorizingRealm 并重写以下方法:
doGetAuthenticationInfo():用于登录认证,返回用户名、密码、盐值等;doGetAuthorizationInfo():用于授权,返回用户的角色和权限集合。
例如,在数据库认证中:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
Set<String> roles = userDAO.findRolesByUsername(username);
Set<String> permissions = userDAO.findPermissionsByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
info.setStringPermissions(permissions);
return info;
}
认证对比
| 流程类型 | 核心问题 | 关键方法 | 异常类型 | 数据来源 |
|---|---|---|---|---|
| 登录认证 | 你是谁? | subject.login(token) | UnknownAccountException、IncorrectCredentialsException | 用户表(用户名/密码) |
| 权限认证 | 你能做什么? | isPermitted()、checkPermission() | UnauthorizedException | 权限表、角色-权限关联表 |
| 角色认证 | 你是什么角色? | hasRole()、checkRole() | UnauthorizedException | 用户-角色关联表 |