在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕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 注解默认不会自动生效!必须满足以下条件之一:

  1. 集成 Spring AOP:通过 LifecycleBeanPostProcessorDefaultAdvisorAutoProxyCreator 启用注解支持。
  2. 手动注册 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 实现(如 RoleAnnotationHandlerPermissionAnnotationHandler)执行实际校验。

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));
                }
            }
        }
    }
}

逻辑清晰:

  1. 获取注解中的角色列表。
  2. 若只有一个角色,直接调用 checkRole
  3. 若有多个角色:
    • AND 模式:调用 checkRoles,要求全部拥有。
    • OR 模式:遍历检查,只要有一个角色存在即通过。

🔍 注意checkRolehasRole 的区别在于,前者在无权限时抛出异常,后者返回布尔值。

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);
    }
}

当目标方法被调用时,拦截器链会依次执行每个 AuthorizingAnnotationMethodInterceptorassertAuthorized 方法,完成权限校验。

2.4 执行流程图解

为了更直观地理解整个流程,我们用 Mermaid 绘制其执行序列:

Subject Role/PermissionAnnotationHandler AopAllianceAnnotationsAuthorizingMethodInterceptor AuthorizationAttributeSourceAdvisor Spring AOP Proxy Client Subject Role/PermissionAnnotationHandler AopAllianceAnnotationsAuthorizingMethodInterceptor AuthorizationAttributeSourceAdvisor Spring AOP Proxy Client alt [有权限] [无权限] 调用 deleteOrder() matches(method, clazz)? true (发现 @RequiresRoles) invoke(methodInvocation) assertAuthorized(annotation) checkRole("admin") true or throw Exception 继续 proceed() 返回结果 throw UnauthorizedException 抛出异常 异常

从图中可见,Shiro 注解的生效依赖于 Spring AOP 的代理机制,而具体的权限判断则由对应的 Handler 委托给 Subject 完成。

三、自定义 Shiro 注解的必要性与场景

尽管 Shiro 内置注解覆盖了大部分权限控制需求,但在实际项目中,我们常遇到以下场景:

  1. 动态权限校验:权限字符串不是硬编码,而是从数据库或配置中心动态获取。
  2. 复合条件判断:需要同时满足多个条件(如角色+部门+时间窗口)。
  3. 业务逻辑耦合:权限判断依赖于方法参数(如只能操作自己的订单)。
  4. 多租户隔离:不同租户的权限命名空间不同。

此时,自定义注解成为最佳解决方案。它不仅能保持代码的声明式风格,还能将复杂的校验逻辑封装复用。

四、自定义 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,但未进行权限校验。

排查步骤

  1. 检查是否配置了 AuthorizationAttributeSourceAdvisor
  2. 确认目标类是否被 Spring 管理(@Service@Component)。
  3. 验证方法是否为 public(Spring AOP 限制)。
  4. 检查代理类型:若使用接口,需确保调用的是代理对象而非原始对象。

6.2 权限校验顺序问题

现象:多个注解同时存在时,校验顺序不符合预期。

原因:Shiro 拦截器链按固定顺序执行(角色 → 权限 → 认证等)。

解决方案:避免在同一方法上堆砌多个注解,改用单一注解封装复合逻辑。

6.3 SpEL 表达式解析失败

现象@RequiresDynamicPermission 中的 SpEL 无法识别参数名。

原因:编译时未保留参数名(Java 8+ 需添加 -parameters 编译参数)。

解决方案

  • Maven 项目:在 maven-compiler-plugin 中配置 <parameters>true</parameters>
  • 或使用 @Param 注解显式指定参数名。

七、总结与展望

通过对 Shiro 注解源码的深入剖析,我们不仅理解了其“声明式权限控制”背后的实现机制,还掌握了自定义扩展的核心方法。无论是动态权限、多租户隔离,还是复合条件判断,都可以通过自定义 AnnotationHandlerMethodInterceptor 灵活实现。

Shiro 的设计哲学是“简单而强大”——它提供了清晰的扩展点,让开发者能在不破坏原有架构的前提下,应对各种复杂的安全需求。正如 Shiro 官方文档 所强调的:“Security is complex, but it shouldn’t be complicated.”

未来,随着微服务和云原生架构的普及,权限控制将面临更多挑战(如分布式授权、OAuth2 集成等)。虽然 Shiro 在这些领域不如 Spring Security 成熟,但其轻量级和灵活性仍使其在特定场景下具有不可替代的优势。掌握其底层原理,将帮助我们在技术选型和架构设计中做出更明智的决策。

💡 最后建议:在扩展 Shiro 时,始终遵循“最小权限原则”,避免过度复杂的自定义逻辑影响系统可维护性。安全框架的核心价值在于可靠性和可审计性,而非炫技。

通过本文的学习,相信你已具备深入定制 Shiro 注解的能力。不妨在下一个项目中尝试应用这些技巧,打造更安全、更灵活的权限控制系统!


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐