若依框架中使用Shiro实现权限控制进阶技巧

2025-06发布5次浏览

若依框架是一个基于Spring Boot和Spring Cloud的快速开发平台,它内置了权限管理模块,并且使用Apache Shiro作为其权限控制的核心组件。本文将深入探讨如何在若依框架中使用Shiro实现更高级的权限控制技巧。


一、Shiro基础回顾

Shiro是Java领域中一个强大且灵活的安全框架,主要功能包括认证(Authentication)、授权(Authorization)、会话管理(Session Management)和加密(Cryptography)。在若依框架中,Shiro被用来处理用户登录、角色与权限的验证以及会话管理等核心安全需求。

Shiro核心概念

  1. Subject:代表当前用户。
  2. Realm:连接到数据源(如数据库),用于验证用户身份和加载权限信息。
  3. Filter Chain:定义URL与权限的映射关系。
  4. Session:支持无状态和有状态的会话管理。

二、若依框架中Shiro的配置与初始化

在若依框架中,Shiro的配置通过ShiroConfig类完成。以下是关键步骤:

  1. 定义Realm: 若依框架中使用UserRealm来加载用户信息和权限数据。该类实现了AuthorizingRealm接口,重写了doGetAuthenticationInfodoGetAuthorizationInfo方法。

    public class UserRealm extends AuthorizingRealm {
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            // 根据用户名查询角色和权限
            String username = (String) principals.getPrimaryPrincipal();
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            List<String> roles = userRoleService.getRolesByUsername(username);
            List<String> permissions = userPermissionService.getPermissionsByUsername(username);
            info.setRoles(new HashSet<>(roles));
            info.setStringPermissions(new HashSet<>(permissions));
            return info;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            // 验证用户登录信息
            String username = (String) token.getPrincipal();
            User user = userService.getUserByUsername(username);
            if (user == null) {
                throw new UnknownAccountException("用户不存在");
            }
            if (!user.isEnabled()) {
                throw new LockedAccountException("账户已被锁定");
            }
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
        }
    }
    
  2. 配置Filter Chain: 在ShiroConfig中定义URL与权限的映射关系。

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        chainDefinition.addPathDefinition("/login", "anon"); // 登录页面无需认证
        chainDefinition.addPathDefinition("/logout", "logout"); // 退出登录
        chainDefinition.addPathDefinition("/**", "authc"); // 其他页面需要认证
        return chainDefinition;
    }
    

三、进阶技巧:动态权限控制

在实际项目中,权限可能需要根据业务逻辑动态调整。以下是一些实现动态权限控制的技巧。

1. 动态加载权限

传统的Shiro权限控制通常是在应用启动时静态加载所有权限规则。但在若依框架中,可以通过数据库动态更新权限信息。

实现步骤:

  • 修改Realm:在doGetAuthorizationInfo方法中,从数据库实时获取用户的角色和权限。
  • 缓存机制:为了避免每次请求都查询数据库,可以引入缓存(如Redis)存储用户的权限信息。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    String username = (String) principals.getPrimaryPrincipal();
    // 查询缓存中的权限信息
    String cacheKey = "shiro:authorization:" + username;
    AuthorizationInfo cachedInfo = redisTemplate.opsForValue().get(cacheKey);
    if (cachedInfo != null) {
        return cachedInfo;
    }
    // 如果缓存中没有,则从数据库加载
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    List<String> roles = userRoleService.getRolesByUsername(username);
    List<String> permissions = userPermissionService.getPermissionsByUsername(username);
    info.setRoles(new HashSet<>(roles));
    info.setStringPermissions(new HashSet<>(permissions));
    // 将权限信息写入缓存
    redisTemplate.opsForValue().set(cacheKey, info, Duration.ofMinutes(30));
    return info;
}

2. 动态刷新权限

当用户角色或权限发生变化时,需要及时刷新Shiro的权限缓存。

实现步骤:

  • 调用SecurityUtils.getSubject().getSession().invalidate()清除当前用户的会话。
  • 或者调用AuthorizationAttributeSourceAdvisorrefreshAuthorizationCache()方法手动刷新缓存。
public void refreshAuthorizationCache(String username) {
    Subject currentUser = SecurityUtils.getSubject();
    if (currentUser.isAuthenticated()) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        List<String> roles = userRoleService.getRolesByUsername(username);
        List<String> permissions = userPermissionService.getPermissionsByUsername(username);
        info.setRoles(new HashSet<>(roles));
        info.setStringPermissions(new HashSet<>(permissions));
        ((DefaultSecurityManager) SecurityUtils.getSecurityManager()).clearCache(currentUser.getPrincipals());
    }
}

四、多租户权限控制

在多租户系统中,不同租户之间的权限需要隔离。若依框架可以通过以下方式实现多租户权限控制。

1. 数据隔离

为每个租户创建独立的数据库表或使用统一表并通过tenant_id字段区分数据。

2. 自定义Realm

UserRealm中加入租户ID的校验逻辑。

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    String username = (String) principals.getPrimaryPrincipal();
    String tenantId = getTenantIdFromContext(); // 从上下文中获取租户ID
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    List<String> roles = userRoleService.getRolesByUsernameAndTenant(username, tenantId);
    List<String> permissions = userPermissionService.getPermissionsByUsernameAndTenant(username, tenantId);
    info.setRoles(new HashSet<>(roles));
    info.setStringPermissions(new HashSet<>(permissions));
    return info;
}

五、优化与扩展

1. 引入JWT

为了支持无状态的分布式系统,可以结合JWT(JSON Web Token)替代传统的Session管理。

2. 权限粒度细化

通过自定义注解或AOP切面实现更细粒度的权限控制。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
    String value();
}

// 切面拦截器
@Aspect
@Component
public class PermissionInterceptor {
    @Around("@annotation(requiresPermission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable {
        Subject subject = SecurityUtils.getSubject();
        if (!subject.isPermitted(requiresPermission.value())) {
            throw new UnauthorizedException("无权限访问");
        }
        return joinPoint.proceed();
    }
}