Shiro - 掌握 Shiro 注解的源码解析与自定义扩展

👋 大家好,欢迎来到我的技术博客!
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕Shiro这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
Shiro - 掌握 Shiro 注解的源码解析与自定义扩展
在现代 Java Web 应用开发中,权限控制是保障系统安全的核心环节。Apache Shiro 作为一款轻量级、功能强大的安全框架,凭借其简洁的 API 和灵活的架构,被广泛应用于各类企业级项目中。其中,Shiro 提供的注解机制极大地简化了权限校验逻辑,使开发者能够以声明式的方式实现细粒度的访问控制。
然而,许多开发者仅停留在使用 @RequiresRoles、@RequiresPermissions 等基础注解的层面,对其实现原理知之甚少。当业务需求超出框架默认能力时(例如需要支持动态权限、多租户隔离或基于上下文的复合判断),往往束手无策。本文将深入 Shiro 注解的源码实现,剖析其工作机制,并通过实际案例演示如何自定义扩展注解,以满足复杂业务场景下的安全控制需求。
📌 提示:本文假设读者已具备 Shiro 基础知识,了解 Subject、Realm、SecurityManager 等核心概念。若尚未掌握,建议先阅读 Apache Shiro 官方文档。
一、Shiro 注解体系概览
Shiro 提供了一套完整的注解集合,用于在方法级别进行权限、角色和身份验证的声明式控制。这些注解主要位于 org.apache.shiro.authz.annotation 包下,常见的包括:
@RequiresAuthentication:要求当前用户已通过身份认证(即subject.isAuthenticated()为true)。@RequiresGuest:要求当前用户是“访客”(未认证且未记住登录状态)。@RequiresUser:要求当前用户是“已知用户”(已认证或通过“记住我”功能登录)。@RequiresRoles:要求当前用户拥有指定的一个或多个角色。@RequiresPermissions:要求当前用户拥有指定的一个或多个权限。
这些注解的共同特点是:它们本身不包含任何逻辑,而是作为元数据被 AOP 框架(如 Spring AOP)拦截并处理。Shiro 通过 AuthorizingAnnotationMethodInterceptor 及其子类实现了具体的校验逻辑。
1.1 注解的使用示例
以下是一个典型的 Shiro 注解使用场景:
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.authz.annotation.RequiresPermissions;
@Service
public class OrderService {
@RequiresRoles("admin")
public void deleteOrder(Long orderId) {
// 仅管理员可删除订单
}
@RequiresPermissions("order:edit")
public void updateOrder(Order order) {
// 需要 order:edit 权限才能编辑订单
}
}
当调用 deleteOrder 方法时,Shiro 会自动检查当前用户是否拥有 admin 角色;若无,则抛出 UnauthorizedException 异常,阻止方法执行。
1.2 注解生效的前提条件
值得注意的是,Shiro 注解默认不会自动生效!必须满足以下条件之一:
- 集成 Spring AOP:通过
LifecycleBeanPostProcessor和DefaultAdvisorAutoProxyCreator启用注解支持。 - 手动注册 AOP 拦截器:在非 Spring 环境中,需自行配置方法拦截器链。
在 Spring Boot 项目中,通常通过如下配置启用 Shiro 注解:
@Configuration
public class ShiroConfig {
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true); // 使用 CGLIB 代理
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
上述配置中,AuthorizationAttributeSourceAdvisor 是关键——它负责扫描带有 Shiro 注解的方法,并为其织入权限校验逻辑。
二、Shiro 注解源码深度解析
要真正掌握 Shiro 注解,必须深入其源码实现。我们将从注解定义、拦截器机制到 AOP 集成三个层面进行剖析。
2.1 注解定义与元数据
以 @RequiresRoles 为例,其源码如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {
String[] value(); // 必须的角色列表
Logical logical() default Logical.AND; // 逻辑关系:AND 或 OR
}
value():指定所需角色,支持多个。logical():定义多个角色之间的逻辑关系。Logical.AND表示必须同时拥有所有角色,Logical.OR表示拥有任一角色即可。
类似地,@RequiresPermissions 也包含相同的 logical 属性,用于控制权限的组合逻辑。
2.2 拦截器机制:AuthorizingAnnotationMethodInterceptor
Shiro 的核心在于 AuthorizingAnnotationMethodInterceptor 抽象类。它是所有注解拦截器的基类,定义了通用的拦截流程:
public abstract class AuthorizingAnnotationMethodInterceptor
extends AnnotationMethodInterceptor {
public AuthorizingAnnotationMethodInterceptor(AnnotationHandler handler) {
super(handler);
}
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
try {
((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(mi);
} catch (AuthorizationException ae) {
throw ae;
} catch (Exception e) {
throw new AuthorizationException("Unexpected error", e);
}
}
}
关键方法 assertAuthorized 委托给具体的 AuthorizingAnnotationHandler 实现(如 RoleAnnotationHandler、PermissionAnnotationHandler)执行实际校验。
2.2.1 RoleAnnotationHandler 源码分析
@RequiresRoles 对应的处理器是 RoleAnnotationHandler:
public class RoleAnnotationHandler extends AuthorizingAnnotationHandler {
public RoleAnnotationHandler() {
super(RequiresRoles.class);
}
public void assertAuthorized(Annotation a) throws AuthorizationException {
if (!(a instanceof RequiresRoles)) return;
RequiresRoles rrAnnotation = (RequiresRoles) a;
String[] roles = rrAnnotation.value();
if (roles.length == 1) {
getSubject().checkRole(roles[0]);
} else if (roles.length > 1) {
if (rrAnnotation.logical() == Logical.AND) {
getSubject().checkRoles(Arrays.asList(roles));
} else {
boolean hasAtLeastOneRole = false;
for (String role : roles) {
if (getSubject().hasRole(role)) {
hasAtLeastOneRole = true;
break;
}
}
if (!hasAtLeastOneRole) {
throw new UnauthorizedException("Requires at least one role: " + Arrays.toString(roles));
}
}
}
}
}
逻辑清晰:
- 获取注解中的角色列表。
- 若只有一个角色,直接调用
checkRole。 - 若有多个角色:
AND模式:调用checkRoles,要求全部拥有。OR模式:遍历检查,只要有一个角色存在即通过。
🔍 注意:
checkRole和hasRole的区别在于,前者在无权限时抛出异常,后者返回布尔值。
2.2.2 PermissionAnnotationHandler 源码分析
@RequiresPermissions 的处理逻辑与角色类似,但调用的是权限相关方法:
public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler {
public PermissionAnnotationHandler() {
super(RequiresPermissions.class);
}
public void assertAuthorized(Annotation a) throws AuthorizationException {
if (!(a instanceof RequiresPermissions)) return;
RequiresPermissions rpAnnotation = (RequiresPermissions) a;
String[] perms = rpAnnotation.value();
if (perms.length == 1) {
getSubject().checkPermission(perms[0]);
} else {
if (rpAnnotation.logical() == Logical.AND) {
getSubject().checkPermissions(perms);
} else {
boolean hasAtLeastOnePerm = false;
for (String perm : perms) {
if (getSubject().isPermitted(perm)) {
hasAtLeastOnePerm = true;
break;
}
}
if (!hasAtLeastOnePerm) {
throw new UnauthorizedException("Requires at least one permission: " + Arrays.toString(perms));
}
}
}
}
}
2.3 AOP 集成:AuthorizationAttributeSourceAdvisor
在 Spring 环境中,AuthorizationAttributeSourceAdvisor 是连接 Shiro 注解与 AOP 代理的桥梁。其核心逻辑如下:
public class AuthorizationAttributeSourceAdvisor
extends StaticMethodMatcherPointcutAdvisor {
private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
new Class[] {
RequiresPermissions.class, RequiresRoles.class,
RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
};
public AuthorizationAttributeSourceAdvisor() {
setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
}
public boolean matches(Method method, Class targetClass) {
// 检查方法或类上是否存在 Shiro 注解
for (Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES) {
if (method.isAnnotationPresent(annClass) ||
targetClass.isAnnotationPresent(annClass)) {
return true;
}
}
return false;
}
}
matches方法决定哪些方法需要被拦截。setAdvice设置了具体的拦截器实现:AopAllianceAnnotationsAuthorizingMethodInterceptor。
该拦截器内部维护了一个 List<AuthorizingAnnotationMethodInterceptor>,包含所有内置注解的处理器:
public class AopAllianceAnnotationsAuthorizingMethodInterceptor
extends AnnotationsAuthorizingMethodInterceptor {
public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
List<AuthorizingAnnotationMethodInterceptor> interceptors =
new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
interceptors.add(new RoleAnnotationMethodInterceptor());
interceptors.add(new PermissionAnnotationMethodInterceptor());
interceptors.add(new AuthenticatedAnnotationMethodInterceptor());
interceptors.add(new UserAnnotationMethodInterceptor());
interceptors.add(new GuestAnnotationMethodInterceptor());
setMethodInterceptors(interceptors);
}
}
当目标方法被调用时,拦截器链会依次执行每个 AuthorizingAnnotationMethodInterceptor 的 assertAuthorized 方法,完成权限校验。
2.4 执行流程图解
为了更直观地理解整个流程,我们用 Mermaid 绘制其执行序列:
从图中可见,Shiro 注解的生效依赖于 Spring AOP 的代理机制,而具体的权限判断则由对应的 Handler 委托给 Subject 完成。
三、自定义 Shiro 注解的必要性与场景
尽管 Shiro 内置注解覆盖了大部分权限控制需求,但在实际项目中,我们常遇到以下场景:
- 动态权限校验:权限字符串不是硬编码,而是从数据库或配置中心动态获取。
- 复合条件判断:需要同时满足多个条件(如角色+部门+时间窗口)。
- 业务逻辑耦合:权限判断依赖于方法参数(如只能操作自己的订单)。
- 多租户隔离:不同租户的权限命名空间不同。
此时,自定义注解成为最佳解决方案。它不仅能保持代码的声明式风格,还能将复杂的校验逻辑封装复用。
四、自定义 Shiro 注解实战
接下来,我们将通过两个典型案例,演示如何扩展 Shiro 注解。
4.1 案例一:动态权限注解 @RequiresDynamicPermission
4.1.1 需求描述
假设系统中存在动态资源,如 /api/order/{orderId},要求用户必须拥有 order:read:{orderId} 权限才能访问。由于 orderId 是运行时参数,无法在注解中静态指定。
4.1.2 自定义注解定义
首先定义注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresDynamicPermission {
// 权限模板,支持 SpEL 表达式
String value();
// 逻辑关系
Logical logical() default Logical.AND;
}
value() 支持 Spring Expression Language (SpEL),例如 "order:read:" + #orderId。
4.1.3 自定义 AnnotationHandler
实现 AuthorizingAnnotationHandler 的子类:
public class DynamicPermissionAnnotationHandler
extends AuthorizingAnnotationHandler {
private final ExpressionParser parser = new SpelExpressionParser();
private final LocalVariableTableParameterNameDiscoverer discoverer =
new LocalVariableTableParameterNameDiscoverer();
public DynamicPermissionAnnotationHandler() {
super(RequiresDynamicPermission.class);
}
@Override
public void assertAuthorized(Annotation annotation) throws AuthorizationException {
if (!(annotation instanceof RequiresDynamicPermission)) {
return;
}
RequiresDynamicPermission dp = (RequiresDynamicPermission) annotation;
Method method = getMethod(); // 从 MethodInvocation 获取
Object[] args = getArguments(); // 方法参数
// 解析 SpEL 表达式
String[] permissionTemplates = dp.value();
String[] actualPermissions = new String[permissionTemplates.length];
EvaluationContext context = buildEvaluationContext(method, args);
for (int i = 0; i < permissionTemplates.length; i++) {
Expression expr = parser.parseExpression(permissionTemplates[i]);
actualPermissions[i] = expr.getValue(context, String.class);
}
// 执行权限校验
Subject subject = getSubject();
if (actualPermissions.length == 1) {
subject.checkPermission(actualPermissions[0]);
} else {
if (dp.logical() == Logical.AND) {
subject.checkPermissions(actualPermissions);
} else {
boolean hasAny = false;
for (String perm : actualPermissions) {
if (subject.isPermitted(perm)) {
hasAny = true;
break;
}
}
if (!hasAny) {
throw new UnauthorizedException(
"Requires at least one dynamic permission: " +
Arrays.toString(actualPermissions));
}
}
}
}
private EvaluationContext buildEvaluationContext(Method method, Object[] args) {
String[] paramNames = discoverer.getParameterNames(method);
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
return context;
}
}
关键点:
- 使用
SpelExpressionParser解析权限模板。 - 通过
LocalVariableTableParameterNameDiscoverer获取方法参数名,构建 SpEL 上下文。 - 将解析后的实际权限字符串传递给
Subject进行校验。
4.1.4 自定义 MethodInterceptor
创建对应的拦截器:
public class DynamicPermissionAnnotationMethodInterceptor
extends AuthorizingAnnotationMethodInterceptor {
public DynamicPermissionAnnotationMethodInterceptor() {
super(new DynamicPermissionAnnotationHandler());
}
}
4.1.5 集成到 Spring AOP
修改 ShiroConfig,将自定义拦截器加入 Advisor:
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor =
new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
// 创建自定义拦截器链
List<AuthorizingAnnotationMethodInterceptor> interceptors =
new ArrayList<>();
interceptors.add(new RoleAnnotationMethodInterceptor());
interceptors.add(new PermissionAnnotationMethodInterceptor());
interceptors.add(new DynamicPermissionAnnotationMethodInterceptor()); // 新增
// ... 其他内置拦截器
AopAllianceAnnotationsAuthorizingMethodInterceptor customInterceptor =
new AopAllianceAnnotationsAuthorizingMethodInterceptor() {
{
setMethodInterceptors(interceptors);
}
};
advisor.setAdvice(customInterceptor);
return advisor;
}
4.1.6 使用示例
@Service
public class OrderService {
@RequiresDynamicPermission("order:read:" + #orderId)
public Order getOrder(Long orderId) {
return orderDao.findById(orderId);
}
@RequiresDynamicPermission({"order:edit:" + #order.id, "dept:manage:" + #order.deptId})
public void updateOrder(Order order) {
orderDao.update(order);
}
}
当调用 getOrder(123L) 时,实际校验的权限为 order:read:123,实现了动态权限控制。
4.2 案例二:多租户角色注解 @RequiresTenantRole
4.2.1 需求描述
在 SaaS 系统中,不同租户(Tenant)的角色是隔离的。例如,租户 A 的 admin 角色与租户 B 的 admin 角色互不影响。我们需要一个注解,能自动将当前租户 ID 拼接到角色名前。
4.2.2 自定义注解定义
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresTenantRole {
String[] value();
Logical logical() default Logical.AND;
}
4.2.3 自定义 AnnotationHandler
public class TenantRoleAnnotationHandler extends AuthorizingAnnotationHandler {
public TenantRoleAnnotationHandler() {
super(RequiresTenantRole.class);
}
@Override
public void assertAuthorized(Annotation annotation) throws AuthorizationException {
if (!(annotation instanceof RequiresTenantRole)) {
return;
}
RequiresTenantRole tr = (RequiresTenantRole) annotation;
String[] roles = tr.value();
String currentTenantId = getCurrentTenantId(); // 从上下文获取租户ID
// 构建带租户前缀的角色名
String[] tenantRoles = new String[roles.length];
for (int i = 0; i < roles.length; i++) {
tenantRoles[i] = currentTenantId + ":" + roles[i];
}
Subject subject = getSubject();
if (tenantRoles.length == 1) {
subject.checkRole(tenantRoles[0]);
} else {
if (tr.logical() == Logical.AND) {
subject.checkRoles(Arrays.asList(tenantRoles));
} else {
boolean hasAny = false;
for (String role : tenantRoles) {
if (subject.hasRole(role)) {
hasAny = true;
break;
}
}
if (!hasAny) {
throw new UnauthorizedException(
"Requires at least one tenant role: " +
Arrays.toString(tenantRoles));
}
}
}
}
private String getCurrentTenantId() {
// 从 ThreadLocal、Request Header 或 SecurityContext 获取
// 示例:return TenantContext.getCurrentTenantId();
return "tenant-001"; // 简化示例
}
}
4.2.4 使用示例
@Service
public class ReportService {
@RequiresTenantRole("report_viewer")
public List<Report> getReports() {
// 实际校验角色:tenant-001:report_viewer
return reportDao.findByTenant(TenantContext.getCurrentTenantId());
}
}
通过这种方式,无需在每个方法中手动拼接租户 ID,权限控制逻辑更加清晰。
五、高级技巧与最佳实践
5.1 处理异常与自定义响应
Shiro 默认抛出 AuthorizationException,但在 Web 应用中,我们通常希望返回 JSON 格式的错误信息。可通过全局异常处理器实现:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<ErrorResponse> handleAuthException(AuthorizationException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("NO_PERMISSION", ex.getMessage()));
}
}
5.2 性能优化:缓存权限校验结果
对于高频调用的接口,重复的权限校验可能成为性能瓶颈。可结合缓存(如 Caffeine)优化:
public class CachedPermissionAnnotationHandler
extends PermissionAnnotationHandler {
private final Cache<String, Boolean> permissionCache =
Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
@Override
public void assertAuthorized(Annotation a) throws AuthorizationException {
// ... 解析权限
String cacheKey = buildCacheKey(subject.getPrincipal(), permission);
Boolean permitted = permissionCache.get(cacheKey, k ->
subject.isPermitted(permission));
if (!permitted) {
throw new UnauthorizedException("Permission denied");
}
}
}
⚠️ 注意:缓存需考虑权限变更时的失效策略,避免安全漏洞。
5.3 与 Spring Security 注解共存
在混合使用 Shiro 和 Spring Security 的项目中,需注意两者 AOP 代理的冲突。建议:
- 统一使用一种安全框架。
- 若必须共存,明确划分职责(如 Shiro 处理业务权限,Spring Security 处理认证)。
5.4 单元测试自定义注解
为确保自定义注解逻辑正确,应编写单元测试:
@Test
public void testDynamicPermissionGranted() {
// 模拟 Subject
Subject subject = mock(Subject.class);
when(subject.isPermitted("order:read:123")).thenReturn(true);
ThreadContext.bind(subject);
// 调用被注解方法
OrderService service = new OrderService();
assertDoesNotThrow(() -> service.getOrder(123L));
}
使用 ThreadContext.bind(subject) 可在测试中设置当前 Subject。
六、常见问题与解决方案
6.1 注解不生效
现象:添加了 @RequiresRoles,但未进行权限校验。
排查步骤:
- 检查是否配置了
AuthorizationAttributeSourceAdvisor。 - 确认目标类是否被 Spring 管理(
@Service、@Component)。 - 验证方法是否为
public(Spring AOP 限制)。 - 检查代理类型:若使用接口,需确保调用的是代理对象而非原始对象。
6.2 权限校验顺序问题
现象:多个注解同时存在时,校验顺序不符合预期。
原因:Shiro 拦截器链按固定顺序执行(角色 → 权限 → 认证等)。
解决方案:避免在同一方法上堆砌多个注解,改用单一注解封装复合逻辑。
6.3 SpEL 表达式解析失败
现象:@RequiresDynamicPermission 中的 SpEL 无法识别参数名。
原因:编译时未保留参数名(Java 8+ 需添加 -parameters 编译参数)。
解决方案:
- Maven 项目:在
maven-compiler-plugin中配置<parameters>true</parameters>。 - 或使用
@Param注解显式指定参数名。
七、总结与展望
通过对 Shiro 注解源码的深入剖析,我们不仅理解了其“声明式权限控制”背后的实现机制,还掌握了自定义扩展的核心方法。无论是动态权限、多租户隔离,还是复合条件判断,都可以通过自定义 AnnotationHandler 和 MethodInterceptor 灵活实现。
Shiro 的设计哲学是“简单而强大”——它提供了清晰的扩展点,让开发者能在不破坏原有架构的前提下,应对各种复杂的安全需求。正如 Shiro 官方文档 所强调的:“Security is complex, but it shouldn’t be complicated.”
未来,随着微服务和云原生架构的普及,权限控制将面临更多挑战(如分布式授权、OAuth2 集成等)。虽然 Shiro 在这些领域不如 Spring Security 成熟,但其轻量级和灵活性仍使其在特定场景下具有不可替代的优势。掌握其底层原理,将帮助我们在技术选型和架构设计中做出更明智的决策。
💡 最后建议:在扩展 Shiro 时,始终遵循“最小权限原则”,避免过度复杂的自定义逻辑影响系统可维护性。安全框架的核心价值在于可靠性和可审计性,而非炫技。
通过本文的学习,相信你已具备深入定制 Shiro 注解的能力。不妨在下一个项目中尝试应用这些技巧,打造更安全、更灵活的权限控制系统!
🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨
更多推荐


所有评论(0)