Article

身份验证(服务端验证)Shiro

更新于:2026-07-14

第一章: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)
SecurityManagerShiro 的核心安全管理器,协调所有安全操作(认证、授权、会话等)应用中通常只有一个实例;必须通过 SecurityUtils.setSecurityManager() 绑定
Realm安全数据源的桥梁,负责从数据库、LDAP、INI 文件等获取用户/角色/权限信息至少配置一个 Realm;支持多个 Realm,可组合使用;需实现 doGetAuthenticationInfodoGetAuthorizationInfo 方法
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 委托 SecurityManagerSubject 将请求转发给全局 SecurityManager 实例SecurityManager 是单例,需提前初始化并绑定
3. SecurityManager 调用 AuthenticatorSecurityManager 内部的 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 → RealmRealm 的 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.DefaultSecurityManager
myRealm=com.example.MyRealm
securityManager.realms=$myRealm
配置核心对象(SecurityManager、Realm、策略等)$ 表示引用已定义的对象;支持 setter 注入(如 realm.credentialsMatcher=$matcher
[users]zhang=123,admin,user
li=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.Sha256CredentialsMatcher
iniRealm.credentialsMatcher=$credentialsMatcher
指定密码比对算法必须与存储的密码哈希方式一致
登录跳转页authc.loginUrl=/login.html未认证时重定向地址仅在 Web 环境生效

2.3 第一个 Shiro 应用(命令行示例)

方法/操作代码示例用途注意事项
初始化 SecurityManagerFactory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
shiro.ini 加载配置并绑定全局 SecurityManager必须在任何 Subject 操作前完成;IniSecurityManagerFactory 位于 shiro-core
获取当前 SubjectSubject 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 及其子类表示失败
UsernamePasswordTokennew UsernamePasswordToken(username, password);
new UsernamePasswordToken(username, password, rememberMe);
封装用户名和密码的标准令牌支持”记住我”;可继承扩展自定义字段(如验证码)
AuthenticationToken自定义实现需实现此接口通用认证令牌接口必须提供 getPrincipal()getCredentials()
AuthenticatorSecurityManager.getAuthenticator()负责协调 Realm 执行认证逻辑默认实现为 ModularRealmAuthenticator
ModularRealmAuthenticator配置在 [main] 段:
authcStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$authcStrategy
支持多 Realm 认证策略常见策略:AllSuccessfulAtLeastOneSuccessfulFirstSuccessful
AuthenticationInfoSimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, hashedPassword, salt, realmName);Realm 返回的认证信息封装包含用户身份、凭证、盐值、Realm 名称
doGetAuthenticationInfoprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException自定义 Realm 中必须实现的方法从数据源加载用户凭证并返回 AuthenticationInfo

3.2 内置 Realm 使用(IniRealm、TextConfigurationRealm)

Realm 类型配置方式(shiro.ini)用途注意事项
IniRealm[main]
iniRealm=org.apache.shiro.realm.text.IniRealm
iniRealm.resourcePath=classpath:users.ini
securityManager.realms=$iniRealm

users.ini 内容:
[users]
admin=123,admin,user
[roles]
admin=user:*,order:*
从 INI 文件加载用户/角色数据适用于演示或小型系统;不支持动态更新
PropertiesRealm[main]
propRealm=org.apache.shiro.realm.text.PropertiesRealm
propRealm.userDefinitions=classpath:users.properties
propRealm.roleDefinitions=classpath:roles.properties
securityManager.realms=$propRealm

users.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.Sha256CredentialsMatcher
iniRealm.credentialsMatcher=$matcher
对 INI 中存储的哈希密码进行校验INI 中的密码必须是对应算法的哈希值(如 SHA-256)

3.3 自定义 Realm 实现

方法/组件代码示例用途注意事项
继承 AuthorizingRealmpublic class MyRealm extends AuthorizingRealm { ... }推荐方式,同时支持认证和授权比直接实现 Realm 接口更简便
重写 supports@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
判断当前 Realm 是否处理该类型 Token多 Realm 场景下用于路由
实现 doGetAuthenticationInfo@Override
protected 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.MyRealm
securityManager.realms=$myRealm
在 shiro.ini 中注册若使用 Spring,可通过 Bean 注入
多 Realm 组合[main]
realm1=com.example.LdapRealm
realm2=com.example.DbRealm
securityManager.realms=$realm1,$realm2
同时支持 LDAP 和数据库认证需配置合适的 AuthenticationStrategy
缓存认证结果@Override
public 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:deletefile: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:createuser:delete
"*:view" → 匹配 user:vieworder:view
表示任意值不能跨段匹配(user:* 不匹配 user:profile:view
通配符 ?"user:edit:??" → 匹配 user:edit:12(两位)表示单个字符(较少使用)实际项目中几乎不用
多值权限"user:edit,delete"逗号分隔多个动作等价于两个独立权限:user:edituser: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@RequiresAuthentication
public void updateUser(User u) { ... }
要求用户已通过认证(等价于 subject.isAuthenticated()不检查角色或权限
@RequiresGuest@RequiresGuest
public void register() { ... }
要求当前为游客(未认证且未 RememberMe)通常用于注册、登录页面
@RequiresUser@RequiresUser
public 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 的属性不影响其他属性
获取会话 IDSerializable 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.ExecutorServiceSessionValidationScheduler
sessionValidationScheduler.interval=3600000
securityManager.sessionManager.sessionValidationScheduler=$sessionValidationScheduler | 定期扫描并清理过期会话(避免内存泄漏) |

5.3 分布式会话支持(SessionDAO、Redis 集成)

组件/配置代码/配置示例用途注意事项
SessionDAO 接口自定义实现需继承 CachingSessionDAO 或实现 SessionDAO提供会话的持久化 CRUD 操作核心方法:createreadSessionupdatedelete
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.RedisManager
redisManager.host=127.0.0.1
redisManager.port=6379
redisManager.expire=1800
redisSessionDAO=org.crazycake.shiro.RedisSessionDAO
redisSessionDAO.redisManager=$redisManager
securityManager.sessionManager.sessionDAO=$redisSessionDAO
cacheManager=org.crazycake.shiro.RedisCacheManager
cacheManager.redisManager=$redisManager
securityManager.cacheManager=$cacheManager
将会话和缓存存储到 Redisexpire 单位为秒;需确保 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.JavaUuidSessionIdGenerator
sessionDAO.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 位)密码存储、数据完整性校验不可逆;相同输入始终产生相同输出
MD5new SimpleHash("MD5", "password");生成 128 位(32 字符十六进制)哈希值已不安全,存在碰撞攻击,禁止用于密码存储
SHA-1new SimpleHash("SHA-1", "password");生成 160 位(40 字符)哈希值已破解,不推荐用于安全场景
SHA-256new SimpleHash("SHA-256", "password");生成 256 位(64 字符)哈希值当前推荐标准,安全性高
SHA-512new 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
绑定到 RealmmyRealm.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.iniclasspath: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 配置中定义名为 shiroFilterShiroFilterFactoryBean
最小依赖<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/** = authcBasicHTTP 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)@Configuration
public 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: true
web:
enabled: true
启用 Shiro Web 支持(默认已启用)可通过 shiro.enabled=false 关闭
过滤器链配置(YAML)shiro:
filterChainDefinition:
"/login": anon
"/admin/**": "authc,roles[admin]"
在 YAML 中定义 URL 权限规则键为路径,值为过滤器链字符串
自动绑定 Realm只需将自定义 Realm 声明为 @Component@BeanStarter 会自动将其注入 SecurityManager支持多个 Realm,按顺序组合
禁用自动配置@SpringBootApplication(exclude = ShiroWebAutoConfiguration.class)完全接管 Shiro 配置适用于复杂定制场景
默认行为自动注册 ShiroFilter 到 /*,启用注解支持开箱即用无需额外配置 Filter 映射

8.3 自定义 Realm 注入与 SecurityManager 配置

操作代码示例(Spring Boot)说明注意事项
自定义 Realm 声明@Component
public class DbRealm extends AuthorizingRealm {
@Autowired private UserService userService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { ... }
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { ... }
}
使用 Spring 注解注入依赖(如 Service)必须标注 @Component 或通过 @Bean 返回
手动配置 SecurityManager@Bean
public SessionsSecurityManager securityManager(DbRealm dbRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(dbRealm); return manager; }
显式控制 SecurityManager 构建过程若存在多个 Realm,使用 setRealms(List<Realm>)
配置 CredentialsMatcher@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher matcher = new Sha256CredentialsMatcher();
matcher.setHashIterations(1024); return matcher; }
// 在 Realm 中注入
@Autowired
public void setMatcher(HashedCredentialsMatcher matcher) {
this.setCredentialsMatcher(matcher); }
分离密码匹配逻辑Matcher 需与注册时哈希参数一致
多 Realm 配置@Bean
public SessionsSecurityManager securityManager(LdapRealm ldap, DbRealm db) {
DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
sm.setRealms(Arrays.asList(ldap, db));
sm.setAuthenticator(new ModularRealmAuthenticator()); return sm; }
组合 LDAP + 数据库认证需显式设置 ModularRealmAuthenticator
缓存集成@Bean
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager(); // 或 Redis/Ehcache 实现
}
// 在 SecurityManager 中设置
manager.setCacheManager(cacheManager());
启用认证/授权结果缓存提升性能,避免重复查询数据库

8.4 前后端分离场景下的无状态认证(JWT + Shiro)

组件/步骤代码示例说明注意事项
自定义 Tokenpublic 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 Realmpublic class JwtRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken; }
@Override
protected 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()); }
@Override
protected 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 {
@Override
protected 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@Bean
public 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 设计@Bean
public 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.MemoryConstrainedCacheManager
securityManager.cacheManager=$cacheManager
myRealm.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.EhCacheManager
cacheManager.cacheManagerConfigFile=classpath:ehcache.xml
securityManager.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.RedisManager
redisManager.host=127.0.0.1
redisManager.port=6379
cacheManager=org.crazycake.shiro.RedisCacheManager
cacheManager.redisManager=$redisManager
securityManager.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 中注入自定义 CacheManagerRealm 需启用缓存:realm.setCachingEnabled(true)
缓存键生成默认使用 PrincipalCollectiontoString() 作为缓存键确保 Principal 可序列化且 toString() 唯一若 Principal 是复杂对象,建议重写 toString()
缓存失效用户修改权限后需手动清除缓存:
realm.getAuthorizationCache().remove(principal);
保证权限变更实时生效可结合消息队列实现分布式缓存清理
性能收益避免重复查询数据库获取角色/权限对高频授权检查(如 API)提升显著缓存命中率是关键指标

9.2 多 Realm 认证策略(AtLeastOneSuccessfulStrategy 等)

策略类配置方式(INI)行为说明适用场景
AtLeastOneSuccessfulStrategy[main]
authcStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
authenticator.authenticationStrategy=$authcStrategy
securityManager.authenticator=$authenticator
只要一个 Realm 认证成功即整体成功(默认策略)混合登录(如本地账号 + LDAP)
AllSuccessfulStrategyauthcStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy所有 Realm 必须全部认证成功强安全要求(如双因子:密码 + 短信)
FirstSuccessfulStrategyauthcStrategy=org.apache.shiro.authc.pam.FirstSuccessfulStrategy仅第一个成功的 Realm 被采纳,后续 Realm 不执行性能优化,避免多余查询
自定义策略public class CustomStrategy extends AbstractAuthenticationStrategy {
@Override
public 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,$realm2Realm 执行顺序由列表顺序决定FirstSuccessfulStrategy 依赖此顺序
异常处理若 Realm 抛出异常(如 DB 连接失败),策略决定是否继续AtLeastOneSuccessful 会跳过失败 Realm 继续尝试需确保 Realm 异常不影响整体流程
授权策略同样存在 ModularRealmAuthorizerAuthorizationStrategy控制多 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.SimpleCookie
cookie.name=rememberMe
cookie.maxAge=2592000 ; 30天
rememberMeManager=org.apache.shiro.web.mgt.CookieRememberMeManager
rememberMeManager.cookie=$cookie
securityManager.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 安全事件监听与审计

监听器类型实现方式监听事件用途
AuthenticatorListenerpublic class MyAuthcListener implements AuthenticatorListener {
public void onSuccess(AuthenticationToken token, AuthenticationInfo info) { ... }
public void onFailure(AuthenticationToken token, AuthenticationException ae) { ... }
}
认证成功/失败记录登录日志、触发告警、锁定账户
AuthorizerListenerpublic 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.MyAuthcListener
securityManager.authenticator.authenticatorListeners=$authcListener
将监听器注入 Authenticator支持多个监听器(List 属性)
Spring 中注册@Bean
public 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.comb.example.com1. 认证中心(sso.example.com)登录后设置 Cookie 的 domain 为 .example.com
2. 各子系统读取该 Cookie 并向认证中心验证有效性
必须同父域;无法跨顶级域名(如 example.comother.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-logout
3. 各系统清除本地会话
依赖浏览器跳转,可能遗漏;推荐结合 WebSocket 或消息队列
替代方案建议对于新项目,优先考虑 OAuth2 / OpenID Connect使用 Keycloak、Auth0 或 Spring Authorization Server标准化程度高,社区支持好;Shiro 更适合传统 Web 应用

总结:Shiro 可实现基础 SSO,但复杂度高。在微服务或跨域场景下,建议采用标准协议(OAuth2)替代自研方案。

总结:Shiro 认证流程

一、登录认证流程(Authentication)

登录认证的目标是验证用户身份是否合法,即确认”你是谁”。Shiro 的认证流程遵循清晰的组件协作机制:

  1. 用户提交凭证:前端或客户端将用户名、密码等信息封装为 UsernamePasswordToken 对象。
  2. 获取 Subject 实例:通过 SecurityUtils.getSubject() 获取当前用户主体(Subject)。
  3. 调用 login 方法:调用 subject.login(token) 启动认证流程。
  4. 委托给 SecurityManager:Subject 将认证请求委托给 SecurityManager。
  5. Authenticator 执行认证:SecurityManager 再将任务交给 Authenticator(默认为 ModularRealmAuthenticator)。
  6. Realm 查询数据源:Authenticator 调用配置好的 Realm(如自定义的 UserRealm 或 IniRealm),从数据库、文件或内存中获取用户信息。
  7. 凭证比对
    • 如果使用缓存(CacheManager),会先尝试从缓存中读取认证信息;
    • 若无缓存,则调用 Realm 的 doGetAuthenticationInfo() 方法,返回 AuthenticationInfo
    • Authenticator 比较用户输入的凭证(如密码)与数据源中的凭证是否一致。
  8. 返回结果
    • 成功则设置 subject.authenticated = true
    • 失败则抛出异常,如 UnknownAccountException(账号不存在)、IncorrectCredentialsException(密码错误)等。

整个流程可简化为:Subject → SecurityManager → Authenticator → Realm → 数据源

二、权限认证流程(Permission Authorization)

权限认证用于判断用户是否有权执行某项操作,即回答”你能做什么?“。Shiro 支持细粒度的权限控制(如 user:deleteorder:view)。

  1. 用户发起权限校验:通过 subject.isPermitted("user:delete")subject.checkPermission("user:delete") 触发。
  2. 委托给 SecurityManager:Subject 将请求转发给 SecurityManager。
  3. Authorizer 执行授权:SecurityManager 调用内部的 Authorizer(默认为 ModularRealmAuthorizer)。
  4. Realm 获取权限数据:Authorizer 调用 Realm 的 getAuthorizationInfo() 方法,获取该用户拥有的所有权限集合(通常来自数据库的权限表)。
  5. 权限匹配
    • Shiro 支持通配符权限(如 user:* 匹配 user:adduser:delete);
    • Authorizer 判断用户权限集合中是否包含所请求的权限。
  6. 返回结果
    • isPermitted() 返回布尔值;
    • checkPermission() 若无权限则抛出 UnauthorizedException

典型应用场景:

if (subject.isPermitted("article:publish")) {
    // 允许发布文章
}

三、角色认证流程(Role Authorization)

角色认证是权限控制的一种高层抽象,用于判断用户是否属于某个角色(如”管理员”、“运营”),从而间接赋予一组权限。

  1. 用户发起角色校验:通过 subject.hasRole("admin")subject.checkRole("admin")
  2. 流程与权限类似:Subject → SecurityManager → Authorizer → Realm;Realm 从数据源加载用户的角色列表(如通过用户 ID 关联角色表)。
  3. 角色匹配:检查用户角色集合中是否包含指定角色。
  4. 返回结果
    • 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)UnknownAccountExceptionIncorrectCredentialsException用户表(用户名/密码)
权限认证你能做什么?isPermitted()checkPermission()UnauthorizedException权限表、角色-权限关联表
角色认证你是什么角色?hasRole()checkRole()UnauthorizedException用户-角色关联表