RuoYi-Vue-Plus深度解析:Sa-Token权限认证实战指南

RuoYi-Vue-Plus深度解析:Sa-Token权限认证实战指南

【免费下载链接】RuoYi-Vue-Plus 多租户后台管理系统 重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Warm-Flow工作流、SpringDoc、Hutool、OSS 定期同步 【免费下载链接】RuoYi-Vue-Plus 项目地址: https://gitcode.com/dromara/RuoYi-Vue-Plus

引言:权限认证的痛点与解决方案

在企业级应用开发中,权限认证一直是开发者的痛点之一。传统的权限认证方案往往存在配置复杂、扩展性差、安全性不足等问题。RuoYi-Vue-Plus作为一款优秀的多租户后台管理系统,深度集成了Sa-Token权限认证框架,为开发者提供了一套完整的权限认证解决方案。

通过本文,你将掌握:

  • ✅ Sa-Token在RuoYi-Vue-Plus中的核心配置
  • ✅ 多租户环境下的权限认证实战
  • ✅ 自定义权限验证的最佳实践
  • ✅ 异常处理和安全防护机制
  • ✅ 实际业务场景中的权限控制技巧

一、Sa-Token核心架构解析

1.1 整体架构设计

RuoYi-Vue-Plus的Sa-Token集成采用了分层架构设计:

mermaid

1.2 核心配置类详解

@AutoConfiguration
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
public class SaTokenConfig {

    @Bean
    public StpLogic getStpLogicJwt() {
        // Sa-Token 整合 jwt (简单模式)
        return new StpLogicJwtForSimple();
    }

    @Bean
    public StpInterface stpInterface() {
        return new SaPermissionImpl();
    }

    @Bean
    public SaTokenDao saTokenDao() {
        return new PlusSaTokenDao();
    }

    @Bean
    public SaTokenExceptionHandler saTokenExceptionHandler() {
        return new SaTokenExceptionHandler();
    }
}

二、多租户权限认证实战

2.1 登录认证流程

RuoYi-Vue-Plus支持多种登录方式,以下是密码登录的核心代码:

public class PasswordAuthStrategy implements IAuthStrategy {
    
    @Override
    public LoginVo authenticate(String... args) {
        // 验证用户名密码
        SysUser user = userService.selectUserByUserName(username);
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new ServiceException("用户不存在/密码错误");
        }
        
        // 创建登录用户信息
        LoginUser loginUser = buildLoginUser(user);
        
        // Sa-Token登录
        LoginHelper.login(loginUser, new SaLoginParameter()
            .setDevice(deviceType)
            .setTimeout(expireTime));
        
        // 返回登录结果
        LoginVo loginVo = new LoginVo();
        loginVo.setAccessToken(StpUtil.getTokenValue());
        loginVo.setExpireIn(StpUtil.getTokenTimeout());
        return loginVo;
    }
}

2.2 权限验证实现

核心权限验证类SaPermissionImpl实现了Sa-Token的权限接口:

public class SaPermissionImpl implements StpInterface {

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        LoginUser loginUser = LoginHelper.getLoginUser();
        if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) {
            // 从数据库查询权限
            PermissionService permissionService = getPermissionService();
            List<String> list = StringUtils.splitList(loginId.toString(), ":");
            return new ArrayList<>(permissionService.getMenuPermission(Long.parseLong(list.get(1))));
        }
        return new ArrayList<>(loginUser.getMenuPermission());
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 类似的角色权限获取逻辑
        return new ArrayList<>(loginUser.getRolePermission());
    }
}

三、登录助手工具类详解

3.1 LoginHelper核心方法

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoginHelper {
    
    // 关键常量定义
    public static final String LOGIN_USER_KEY = "loginUser";
    public static final String TENANT_KEY = "tenantId";
    public static final String USER_KEY = "userId";
    
    // 登录方法
    public static void login(LoginUser loginUser, SaLoginParameter model) {
        model = ObjectUtil.defaultIfNull(model, new SaLoginParameter());
        StpUtil.login(loginUser.getLoginId(),
            model.setExtra(TENANT_KEY, loginUser.getTenantId())
                .setExtra(USER_KEY, loginUser.getUserId())
                .setExtra(USER_NAME_KEY, loginUser.getUsername())
        );
        StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
    }
    
    // 获取当前用户信息
    public static <T extends LoginUser> T getLoginUser() {
        SaSession session = StpUtil.getTokenSession();
        if (ObjectUtil.isNull(session)) {
            return null;
        }
        return (T) session.get(LOGIN_USER_KEY);
    }
    
    // 获取用户ID
    public static Long getUserId() {
        return Convert.toLong(StpUtil.getExtra(USER_KEY));
    }
    
    // 检查登录状态
    public static boolean isLogin() {
        try {
            return getLoginUser() != null;
        } catch (Exception e) {
            return false;
        }
    }
}

3.2 业务场景中的使用示例

// 在Controller中获取当前用户信息
@RestController
@RequestMapping("/system/user")
public class UserController {
    
    @GetMapping("/profile")
    public R<LoginUser> getProfile() {
        LoginUser loginUser = LoginHelper.getLoginUser();
        if (loginUser == null) {
            throw new ServiceException("用户未登录");
        }
        return R.ok(loginUser);
    }
    
    @PostMapping("/update")
    @SaCheckLogin  // 需要登录验证
    public R<Void> updateUser(@RequestBody UserUpdateDTO dto) {
        Long currentUserId = LoginHelper.getUserId();
        // 业务逻辑处理
        userService.updateUser(currentUserId, dto);
        return R.ok();
    }
}

四、多设备类型支持

4.1 设备类型管理

RuoYi-Vue-Plus支持多种设备类型的权限控制:

public enum DeviceType {
    PC("pc", "电脑端"),
    APP("app", "移动端"),
    WECHAT("wechat", "微信端"),
    XCX("xcx", "小程序");
    
    private final String code;
    private final String info;
    
    DeviceType(String code, String info) {
        this.code = code;
        this.info = info;
    }
    
    public String getCode() {
        return code;
    }
}

4.2 多设备登录实现

// 根据不同设备类型进行登录
public void loginByDevice(LoginUser loginUser, String deviceType) {
    SaLoginParameter parameter = new SaLoginParameter()
        .setDevice(deviceType)
        .setTimeout(3600); // 1小时有效期
    
    LoginHelper.login(loginUser, parameter);
}

// 检查特定设备的登录状态
public boolean isLoginOnDevice(String deviceType) {
    return StpUtil.isLogin() && 
           deviceType.equals(StpUtil.getLoginDevice());
}

五、权限控制最佳实践

5.1 接口权限控制

在Controller层使用注解进行权限控制:

@RestController
@RequestMapping("/system")
public class SystemController {
    
    // 需要用户登录
    @SaCheckLogin
    @GetMapping("/info")
    public R<SystemInfo> getSystemInfo() {
        return R.ok(systemService.getSystemInfo());
    }
    
    // 需要特定权限
    @SaCheckPermission("system:user:list")
    @GetMapping("/users")
    public R<List<UserVO>> getUserList() {
        return R.ok(userService.getUserList());
    }
    
    // 需要管理员角色
    @SaCheckRole("admin")
    @PostMapping("/config")
    public R<Void> updateConfig(@RequestBody ConfigDTO config) {
        systemService.updateConfig(config);
        return R.ok();
    }
}

5.2 业务层权限验证

@Service
public class UserServiceImpl implements UserService {
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateUser(Long userId, UserUpdateDTO dto) {
        // 获取当前登录用户
        LoginUser currentUser = LoginHelper.getLoginUser();
        
        // 权限验证:只能修改自己的信息或拥有管理员权限
        if (!userId.equals(currentUser.getUserId()) && 
            !currentUser.getRolePermission().contains("admin")) {
            throw new ServiceException("无权限修改其他用户信息");
        }
        
        // 业务逻辑处理
        SysUser user = userMapper.selectById(userId);
        if (user == null) {
            throw new ServiceException("用户不存在");
        }
        
        // 更新操作
        user.setEmail(dto.getEmail());
        user.setPhone(dto.getPhone());
        userMapper.updateById(user);
    }
}

六、异常处理与安全防护

6.1 统一异常处理

public class SaTokenExceptionHandler {
    
    @ExceptionHandler(NotLoginException.class)
    public R<Void> handleNotLoginException(NotLoginException e) {
        String message;
        switch (e.getType()) {
            case NotLoginException.NOT_TOKEN:
                message = "未提供token";
                break;
            case NotLoginException.INVALID_TOKEN:
                message = "token无效";
                break;
            case NotLoginException.TOKEN_TIMEOUT:
                message = "token已过期";
                break;
            case NotLoginException.BE_REPLACED:
                message = "token已被顶下线";
                break;
            case NotLoginException.KICK_OUT:
                message = "token已被踢下线";
                break;
            default:
                message = "当前会话未登录";
        }
        return R.fail(HttpStatus.UNAUTHORIZED, message);
    }
    
    @ExceptionHandler(NotPermissionException.class)
    public R<Void> handleNotPermissionException(NotPermissionException e) {
        return R.fail(HttpStatus.FORBIDDEN, "无权限访问:" + e.getPermission());
    }
    
    @ExceptionHandler(NotRoleException.class)
    public R<Void> handleNotRoleException(NotRoleException e) {
        return R.fail(HttpStatus.FORBIDDEN, "无角色权限:" + e.getRole());
    }
}

6.2 安全防护措施

// 并发登录控制
public class ConcurrentLoginControl {
    
    private static final int MAX_CONCURRENT_LOGINS = 3;
    
    public void checkConcurrentLogin(String username) {
        int loginCount = StpUtil.getLoginCount(username);
        if (loginCount >= MAX_CONCURRENT_LOGINS) {
            // 踢掉最早的登录会话
            StpUtil.kickout(StpUtil.getLoginId(username, 0));
        }
    }
}

// 敏感操作验证
public class SecurityValidator {
    
    public void validateSensitiveOperation(Long userId, String operation) {
        LoginUser currentUser = LoginHelper.getLoginUser();
        
        // 验证操作权限
        if (!hasPermission(currentUser, operation)) {
            throw new ServiceException("无权限执行此操作");
        }
        
        // 记录操作日志
        auditLogService.logSensitiveOperation(userId, operation);
    }
    
    private boolean hasPermission(LoginUser user, String operation) {
        return user.getMenuPermission().contains(operation) ||
               user.getRolePermission().contains("admin");
    }
}

七、实战案例:多租户权限系统

7.1 租户隔离实现

// 租户权限过滤器
@Component
public class TenantFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String tenantId = httpRequest.getHeader("X-Tenant-Id");
        
        if (StringUtils.isNotBlank(tenantId)) {
            // 设置当前租户上下文
            TenantContext.setCurrentTenant(tenantId);
            
            // 验证租户权限
            if (!tenantService.isValidTenant(tenantId)) {
                throw new ServiceException("租户不存在或已禁用");
            }
        }
        
        try {
            chain.doFilter(request, response);
        } finally {
            TenantContext.clear();
        }
    }
}

// 租户上下文管理
public class TenantContext {
    private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
    
    public static void setCurrentTenant(String tenantId) {
        currentTenant.set(tenantId);
    }
    
    public static String getCurrentTenant() {
        return currentTenant.get();
    }
    
    public static void clear() {
        currentTenant.remove();
    }
}

7.2 跨租户数据权限控制

// 数据权限拦截器
@Interceptor
public class DataPermissionInterceptor implements InnerInterceptor {
    
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, 
                          RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        
        // 获取当前用户和租户信息
        LoginUser loginUser = LoginHelper.getLoginUser();
        String currentTenant = TenantContext.getCurrentTenant();
        
        if (loginUser != null && currentTenant != null) {
            // 构建数据权限SQL
            String dataPermissionSql = buildDataPermissionSql(loginUser, currentTenant);
            
            // 修改原始SQL,添加数据权限条件
            String originalSql = boundSql.getSql();
            String newSql = originalSql + " AND " + dataPermissionSql;
            
            // 反射修改BoundSql中的SQL
            ReflectUtil.setFieldValue(boundSql, "sql", newSql);
        }
    }
    
    private String buildDataPermissionSql(LoginUser user, String tenantId) {
        StringBuilder sqlBuilder = new StringBuilder();
        
        // 根据用户角色构建不同的数据权限
        if (user.getRolePermission().contains("admin")) {
            // 管理员可以看到所有数据
            sqlBuilder.append("tenant_id = '").append(tenantId).append("'");
        } else if (user.getRolePermission().contains("dept_manager")) {
            // 部门经理只能看到本部门数据
            sqlBuilder.append("tenant_id = '").append(tenantId)
                     .append("' AND dept_id = ").append(user.getDeptId());
        } else {
            // 普通员工只能看到自己的数据
            sqlBuilder.append("tenant_id = '").append(tenantId)
                     .append("' AND create_by = ").append(user.getUserId());
        }
        
        return sqlBuilder.toString();
    }
}

八、性能优化与最佳实践

8.1 权限缓存策略

// 基于Redis的权限缓存
@Component
public class PermissionCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String PERMISSION_KEY_PREFIX = "permission:";
    private static final String ROLE_KEY_PREFIX = "role:";
    private static final long CACHE_TIMEOUT = 30 * 60; // 30分钟
    
    public List<String> getCachedPermissions(Long userId) {
        String key = PERMISSION_KEY_PREFIX + userId;
        Object cached = redisTemplate.opsForValue().get(key);
        
        if (cached != null) {
            return (List<String>) cached;
        }
        
        // 从数据库查询并缓存
        List<String> permissions = permissionMapper.selectPermissionsByUserId(userId);
        redisTemplate.opsForValue().set(key, permissions, CACHE_TIMEOUT, TimeUnit.SECONDS);
        
        return permissions;
    }
    
    public void evictPermissionCache(Long userId) {
        String key = PERMISSION_KEY_PREFIX + userId;
        redisTemplate.delete(key);
    }
}

8.2 批量权限验证优化

// 批量权限验证服务
@Service
public class BatchPermissionService {
    
    @Autowired
    private PermissionService permissionService;
    
    /**
     * 批量验证权限,减少数据库查询次数
     */
    public Map<Long, Boolean> batchCheckPermissions(List<Long> userIds, String permission) {
        Map<Long, Boolean> result = new HashMap<>();
        
        // 批量获取用户权限
        Map<Long, List<String>> userPermissions = permissionService.batchGetPermissions(userIds);
        
        for (Long userId : userIds) {
            List<String> permissions = userPermissions.get(userId);
            result.put(userId, permissions != null && permissions.contains(permission));
        }
        
        return result;
    }
    
    /**
     * 预加载权限数据到缓存
     */
    @Async
    public void preloadPermissions(List<Long> userIds) {
        permissionService.batchGetPermissions(userIds);
    }
}

总结

RuoYi-Vue-Plus通过深度集成Sa-Token,提供了一套完整、灵活、高效的权限认证解决方案。本文从核心架构、多租户支持、权限控制、异常处理、性能优化等多个维度进行了详细解析,并提供了丰富的实战案例。

关键要点总结:

  1. 架构清晰:采用分层设计,职责分明,易于扩展和维护
  2. 多租户支持:完善的租户隔离和数据权限控制机制
  3. 灵活配置:支持多种设备类型和登录方式的权限控制
  4. 安全可靠:完善的异常处理和安全防护机制
  5. 性能优化:合理的缓存策略和批量处理优化

通过掌握这些技术要点,开发者可以快速构建安全可靠的企业级权限系统,满足各种复杂的业务场景需求。

【免费下载链接】RuoYi-Vue-Plus 多租户后台管理系统 重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Warm-Flow工作流、SpringDoc、Hutool、OSS 定期同步 【免费下载链接】RuoYi-Vue-Plus 项目地址: https://gitcode.com/dromara/RuoYi-Vue-Plus

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值