Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.4 Sa-Token高级特性实现

发布于:2025-09-01 ⋅ 阅读:(42) ⋅ 点赞:(0)

👋 大家好,我是 阿问学长!专注于分享优质开源项目解析、毕业设计项目指导支持、幼小初高教辅资料推荐等,欢迎关注交流!🚀

Sa-Token高级特性实现

前言

在前面的文章中,我们学习了Sa-Token的基础使用和权限注解。本文将深入探讨Sa-Token的高级特性,包括监听器机制、拦截器配置、自定义权限验证逻辑等,这些特性让Sa-Token在RuoYi-Vue-Plus中能够实现更加复杂和灵活的权限控制。

SaTokenListener监听器机制

监听器概述

Sa-Token提供了丰富的监听器机制,可以监听登录、注销、踢人、权限校验等各种事件,让开发者能够在这些关键节点执行自定义逻辑。

核心监听器事件

@Component
public class SaTokenListenerImpl implements SaTokenListener {
    
    @Autowired
    private ISysLogininforService logininforService;
    
    @Autowired
    private ISysUserOnlineService userOnlineService;
    
    @Autowired
    private RedisUtils redisUtils;
    
    /**
     * 每次登录时触发
     */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        log.info("用户登录成功: loginId={}, tokenValue={}, device={}", 
                loginId, tokenValue, loginModel.getDevice());
        
        // 记录登录日志
        recordLoginLog(loginId, tokenValue, loginModel, true);
        
        // 更新在线用户信息
        updateOnlineUser(loginId, tokenValue, loginModel);
        
        // 发送登录通知
        sendLoginNotification(loginId, loginModel);
        
        // 清除登录失败次数
        clearLoginFailureCount(loginId);
    }
    
    /**
     * 每次注销时触发
     */
    @Override
    public void doLogout(String loginType, Object loginId, String tokenValue) {
        log.info("用户注销: loginId={}, tokenValue={}", loginId, tokenValue);
        
        // 记录注销日志
        recordLogoutLog(loginId, tokenValue);
        
        // 移除在线用户信息
        removeOnlineUser(tokenValue);
        
        // 清理用户缓存
        clearUserCache(loginId);
    }
    
    /**
     * 每次被踢下线时触发
     */
    @Override
    public void doKickout(String loginType, Object loginId, String tokenValue) {
        log.info("用户被踢下线: loginId={}, tokenValue={}", loginId, tokenValue);
        
        // 记录踢出日志
        recordKickoutLog(loginId, tokenValue);
        
        // 发送踢出通知
        sendKickoutNotification(loginId);
        
        // 移除在线用户信息
        removeOnlineUser(tokenValue);
    }
    
    /**
     * 每次Token续期时触发
     */
    @Override
    public void doRenew(String loginType, Object loginId, String tokenValue, long timeout) {
        log.debug("Token续期: loginId={}, tokenValue={}, timeout={}", loginId, tokenValue, timeout);
        
        // 更新在线用户活跃时间
        updateUserActiveTime(loginId, tokenValue);
    }
    
    /**
     * 每次创建Session时触发
     */
    @Override
    public void doCreateSession(String id) {
        log.debug("创建Session: sessionId={}", id);
    }
    
    /**
     * 每次注销Session时触发
     */
    @Override
    public void doLogoutSession(String id) {
        log.debug("注销Session: sessionId={}", id);
    }
    
    /**
     * 记录登录日志
     */
    private void recordLoginLog(Object loginId, String tokenValue, SaLoginModel loginModel, boolean success) {
        try {
            SysLogininfor logininfor = new SysLogininfor();
            
            // 获取用户信息
            SysUser user = userService.selectUserById(Long.valueOf(loginId.toString()));
            if (user != null) {
                logininfor.setUserName(user.getUserName());
            }
            
            // 设置登录信息
            logininfor.setIpaddr(ServletUtils.getClientIP());
            logininfor.setLoginLocation(AddressUtils.getRealAddressByIP(logininfor.getIpaddr()));
            logininfor.setBrowser(ServletUtils.getBrowser());
            logininfor.setOs(ServletUtils.getOs());
            logininfor.setLoginTime(DateUtils.getNowDate());
            logininfor.setStatus(success ? Constants.LOGIN_SUCCESS : Constants.LOGIN_FAIL);
            logininfor.setMsg(success ? "登录成功" : "登录失败");
            
            // 设备信息
            if (loginModel != null) {
                logininfor.setDevice(loginModel.getDevice());
            }
            
            // 异步保存日志
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(logininfor));
            
        } catch (Exception e) {
            log.error("记录登录日志失败", e);
        }
    }
    
    /**
     * 更新在线用户信息
     */
    private void updateOnlineUser(Object loginId, String tokenValue, SaLoginModel loginModel) {
        try {
            SysUserOnline userOnline = new SysUserOnline();
            userOnline.setTokenId(tokenValue);
            userOnline.setUserId(Long.valueOf(loginId.toString()));
            
            // 获取用户信息
            SysUser user = userService.selectUserById(userOnline.getUserId());
            if (user != null) {
                userOnline.setUserName(user.getUserName());
                userOnline.setDeptName(user.getDept() != null ? user.getDept().getDeptName() : "");
            }
            
            // 设置登录信息
            userOnline.setIpaddr(ServletUtils.getClientIP());
            userOnline.setLoginLocation(AddressUtils.getRealAddressByIP(userOnline.getIpaddr()));
            userOnline.setBrowser(ServletUtils.getBrowser());
            userOnline.setOs(ServletUtils.getOs());
            userOnline.setLoginTime(DateUtils.getNowDate());
            
            if (loginModel != null) {
                userOnline.setDevice(loginModel.getDevice());
            }
            
            // 保存在线用户信息
            userOnlineService.saveOnline(userOnline);
            
        } catch (Exception e) {
            log.error("更新在线用户信息失败", e);
        }
    }
    
    /**
     * 发送登录通知
     */
    private void sendLoginNotification(Object loginId, SaLoginModel loginModel) {
        try {
            // 获取用户信息
            SysUser user = userService.selectUserById(Long.valueOf(loginId.toString()));
            if (user == null) {
                return;
            }
            
            // 构建通知消息
            LoginNotificationDto notification = new LoginNotificationDto();
            notification.setUserId(user.getUserId());
            notification.setUserName(user.getUserName());
            notification.setLoginTime(new Date());
            notification.setIpAddress(ServletUtils.getClientIP());
            notification.setDevice(loginModel != null ? loginModel.getDevice() : "unknown");
            
            // 发送WebSocket通知
            webSocketService.sendToUser(user.getUserId(), "login_notification", notification);
            
            // 发送邮件通知(如果启用)
            if (user.getEmail() != null && systemConfigService.isEmailNotificationEnabled()) {
                emailService.sendLoginNotification(user.getEmail(), notification);
            }
            
        } catch (Exception e) {
            log.error("发送登录通知失败", e);
        }
    }
}

自定义事件监听

/**
 * 自定义权限校验监听器
 */
@Component
public class PermissionCheckListener {
    
    @Autowired
    private ISysOperLogService operLogService;
    
    /**
     * 权限校验成功事件
     */
    @EventListener
    public void handlePermissionCheckSuccess(PermissionCheckSuccessEvent event) {
        // 记录权限使用日志
        recordPermissionUsage(event.getLoginId(), event.getPermission(), true);
    }
    
    /**
     * 权限校验失败事件
     */
    @EventListener
    public void handlePermissionCheckFailure(PermissionCheckFailureEvent event) {
        // 记录权限拒绝日志
        recordPermissionUsage(event.getLoginId(), event.getPermission(), false);
        
        // 检查是否需要告警
        checkSecurityAlert(event);
    }
    
    /**
     * 记录权限使用情况
     */
    private void recordPermissionUsage(Object loginId, String permission, boolean success) {
        try {
            SysOperLog operLog = new SysOperLog();
            operLog.setOperName(getUserName(loginId));
            operLog.setOperIp(ServletUtils.getClientIP());
            operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
            operLog.setMethod("PERMISSION_CHECK");
            operLog.setRequestMethod("CHECK");
            operLog.setOperUrl(permission);
            operLog.setStatus(success ? Constants.SUCCESS : Constants.FAIL);
            operLog.setOperTime(DateUtils.getNowDate());
            
            // 异步保存日志
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
            
        } catch (Exception e) {
            log.error("记录权限使用日志失败", e);
        }
    }
    
    /**
     * 安全告警检查
     */
    private void checkSecurityAlert(PermissionCheckFailureEvent event) {
        String key = "permission_failure:" + event.getLoginId();
        Integer count = redisUtils.getCacheObject(key);
        count = count == null ? 1 : count + 1;
        
        // 设置过期时间为1小时
        redisUtils.setCacheObject(key, count, Duration.ofHours(1));
        
        // 如果1小时内权限校验失败超过10次,发送告警
        if (count >= 10) {
            securityAlertService.sendPermissionFailureAlert(event.getLoginId(), count);
        }
    }
}

SaCheckInterceptor拦截器配置

全局拦截器配置

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
    
    /**
     * 注册Sa-Token拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册Sa-Token拦截器,校验规则为StpUtil.checkLogin()登录校验
        registry.addInterceptor(new SaInterceptor(handle -> {
            
            // 指定一条match规则
            SaRouter.match("/**")    // 拦截所有路由
                .notMatch("/auth/login")    // 排除登录接口
                .notMatch("/auth/logout")   // 排除注销接口
                .notMatch("/public/**")     // 排除公开接口
                .notMatch("/captcha/**")    // 排除验证码接口
                .notMatch("/error")         // 排除错误页面
                .notMatch("/favicon.ico")   // 排除图标
                .notMatch("/actuator/**")   // 排除监控端点
                .check(r -> StpUtil.checkLogin());  // 执行登录校验
                
        })).addPathPatterns("/**");
        
        // 注册权限校验拦截器
        registry.addInterceptor(new SaInterceptor(handle -> {
            
            // API权限校验
            SaRouter.match("/api/**", r -> {
                // 检查API访问权限
                StpUtil.checkPermission("api:access");
            });
            
            // 管理员接口权限校验
            SaRouter.match("/admin/**", r -> {
                StpUtil.checkRole("admin");
            });
            
            // 系统管理权限校验
            SaRouter.match("/system/**", r -> {
                String uri = SaHolder.getRequest().getRequestURI();
                String permission = convertUriToPermission(uri);
                if (StringUtils.isNotBlank(permission)) {
                    StpUtil.checkPermission(permission);
                }
            });
            
        })).addPathPatterns("/**");
    }
    
    /**
     * 将URI转换为权限标识
     */
    private String convertUriToPermission(String uri) {
        // 移除前缀
        uri = uri.replaceFirst("/system", "");
        
        // 转换为权限格式
        if (uri.startsWith("/user")) {
            return "system:user:list";
        } else if (uri.startsWith("/role")) {
            return "system:role:list";
        } else if (uri.startsWith("/menu")) {
            return "system:menu:list";
        } else if (uri.startsWith("/dept")) {
            return "system:dept:list";
        }
        
        return null;
    }
}

细粒度路由拦截

@Configuration
public class AdvancedSaTokenConfig {
    
    /**
     * 高级拦截器配置
     */
    @Bean
    public SaInterceptor getSaInterceptor() {
        return new SaInterceptor(handle -> {
            
            // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
            SaRouter.match("/**", "/auth/login", r -> StpUtil.checkLogin());
            
            // 角色校验 -- 拦截以 admin 开头的路由,必须具备admin角色或者super-admin角色才可以通过认证
            SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
            
            // 权限校验 -- 不同模块, 校验不同权限
            SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
            SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
            SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
            SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
            SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
            SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
            
            // 甚至你可以随意的写一个打印语句
            SaRouter.match("/**", r -> System.out.println("----啦啦啦----"));
            
            // 连缀写法
            SaRouter.match("/admin/**").check(r -> StpUtil.checkPermission("admin"));
            
        });
    }
}

动态权限拦截

@Component
public class DynamicPermissionInterceptor implements HandlerInterceptor {
    
    @Autowired
    private IPermissionService permissionService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        // 跳过非Controller方法
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        
        // 检查是否有忽略注解
        if (handlerMethod.hasMethodAnnotation(SaIgnore.class)) {
            return true;
        }
        
        // 检查登录状态
        if (!StpUtil.isLogin()) {
            throw new NotLoginException("用户未登录", StpUtil.getLoginType());
        }
        
        // 动态权限校验
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        
        // 获取当前用户ID
        Object loginId = StpUtil.getLoginId();
        
        // 检查动态权限
        if (!permissionService.hasPermission(Long.valueOf(loginId.toString()), requestURI, method)) {
            throw new NotPermissionException("权限不足");
        }
        
        return true;
    }
}

API权限校验最佳实践

RESTful API权限设计

@RestController
@RequestMapping("/api/v1")
public class ApiController {
    
    /**
     * RESTful风格的权限校验
     */
    @GetMapping("/users")
    @SaCheckPermission("user:query")
    public R<List<UserVo>> getUsers() {
        return R.ok(userService.selectUserList());
    }
    
    @PostMapping("/users")
    @SaCheckPermission("user:add")
    public R<Void> createUser(@RequestBody UserDto userDto) {
        return toAjax(userService.insertUser(userDto));
    }
    
    @PutMapping("/users/{id}")
    @SaCheckPermission("user:edit")
    public R<Void> updateUser(@PathVariable Long id, @RequestBody UserDto userDto) {
        return toAjax(userService.updateUser(id, userDto));
    }
    
    @DeleteMapping("/users/{id}")
    @SaCheckPermission("user:remove")
    public R<Void> deleteUser(@PathVariable Long id) {
        return toAjax(userService.deleteUser(id));
    }
}

API版本权限控制

@RestController
@RequestMapping("/api")
public class VersionedApiController {
    
    /**
     * V1版本API - 基础权限
     */
    @GetMapping("/v1/data")
    @SaCheckPermission("api:v1:data")
    public R<DataVo> getDataV1() {
        return R.ok(dataService.getBasicData());
    }
    
    /**
     * V2版本API - 高级权限
     */
    @GetMapping("/v2/data")
    @SaCheckPermission("api:v2:data")
    public R<AdvancedDataVo> getDataV2() {
        return R.ok(dataService.getAdvancedData());
    }
    
    /**
     * 实验性API - 特殊权限
     */
    @GetMapping("/experimental/data")
    @SaCheckPermission("api:experimental:access")
    @SaCheckRole("developer")
    public R<ExperimentalDataVo> getExperimentalData() {
        return R.ok(dataService.getExperimentalData());
    }
}

第三方API权限控制

@RestController
@RequestMapping("/open-api")
public class OpenApiController {
    
    @Autowired
    private ApiKeyService apiKeyService;
    
    /**
     * 第三方API - 使用API Key认证
     */
    @PostMapping("/webhook")
    public R<Void> webhook(@RequestHeader("X-API-Key") String apiKey, 
                          @RequestBody WebhookDto webhook) {
        
        // 验证API Key
        if (!apiKeyService.validateApiKey(apiKey)) {
            throw new ServiceException("Invalid API Key");
        }
        
        // 检查API Key权限
        if (!apiKeyService.hasPermission(apiKey, "webhook:receive")) {
            throw new ServiceException("Permission denied");
        }
        
        return toAjax(webhookService.process(webhook));
    }
    
    /**
     * OAuth2保护的API
     */
    @GetMapping("/protected/resource")
    @SaCheckLogin
    public R<ResourceVo> getProtectedResource(@RequestHeader("Authorization") String token) {
        
        // 验证OAuth2 Token
        if (!oauthService.validateToken(token)) {
            throw new ServiceException("Invalid OAuth2 Token");
        }
        
        // 获取Token对应的权限范围
        List<String> scopes = oauthService.getTokenScopes(token);
        
        // 检查权限范围
        if (!scopes.contains("read:resource")) {
            throw new ServiceException("Insufficient scope");
        }
        
        return R.ok(resourceService.getResource());
    }
}

自定义权限验证逻辑

复杂业务权限校验

@Service
public class BusinessPermissionService {
    
    /**
     * 检查数据所有权
     */
    public boolean checkDataOwnership(Long userId, Long dataId, String dataType) {
        switch (dataType) {
            case "order":
                return orderService.isOrderOwner(userId, dataId);
            case "document":
                return documentService.isDocumentOwner(userId, dataId);
            case "project":
                return projectService.isProjectMember(userId, dataId);
            default:
                return false;
        }
    }
    
    /**
     * 检查部门数据权限
     */
    public boolean checkDeptDataPermission(Long userId, Long deptId) {
        SysUser user = userService.selectUserById(userId);
        if (user == null) {
            return false;
        }
        
        // 获取用户数据权限范围
        String dataScope = user.getRole().getDataScope();
        
        switch (dataScope) {
            case DataScopeEnum.DATA_SCOPE_ALL:
                return true;
            case DataScopeEnum.DATA_SCOPE_CUSTOM:
                return roleService.checkDeptDataScope(user.getRoleId(), deptId);
            case DataScopeEnum.DATA_SCOPE_DEPT:
                return user.getDeptId().equals(deptId);
            case DataScopeEnum.DATA_SCOPE_DEPT_AND_CHILD:
                return deptService.isChildDept(user.getDeptId(), deptId);
            case DataScopeEnum.DATA_SCOPE_SELF:
                return false; // 仅本人数据,不包含部门数据
            default:
                return false;
        }
    }
    
    /**
     * 检查时间段权限
     */
    public boolean checkTimePermission(Long userId, String permission) {
        // 获取用户的时间权限配置
        UserTimePermission timePermission = userService.getUserTimePermission(userId, permission);
        
        if (timePermission == null) {
            return true; // 没有时间限制
        }
        
        LocalTime now = LocalTime.now();
        DayOfWeek today = LocalDate.now().getDayOfWeek();
        
        // 检查是否在允许的时间段内
        return timePermission.isAllowedTime(today, now);
    }
    
    /**
     * 检查IP权限
     */
    public boolean checkIpPermission(Long userId, String permission) {
        String clientIp = ServletUtils.getClientIP();
        
        // 获取用户的IP权限配置
        List<String> allowedIps = userService.getAllowedIps(userId, permission);
        
        if (CollectionUtils.isEmpty(allowedIps)) {
            return true; // 没有IP限制
        }
        
        // 检查IP是否在允许列表中
        return allowedIps.stream().anyMatch(allowedIp -> IpUtils.matches(clientIp, allowedIp));
    }
}

权限缓存优化

@Service
public class PermissionCacheService {
    
    @Autowired
    private RedisUtils redisUtils;
    
    private static final String PERMISSION_CACHE_KEY = "permission:cache:";
    private static final Duration CACHE_DURATION = Duration.ofMinutes(30);
    
    /**
     * 获取用户权限列表(带缓存)
     */
    public List<String> getUserPermissions(Long userId) {
        String cacheKey = PERMISSION_CACHE_KEY + "user:" + userId;
        
        // 先从缓存获取
        List<String> permissions = redisUtils.getCacheObject(cacheKey);
        if (permissions != null) {
            return permissions;
        }
        
        // 缓存未命中,从数据库查询
        permissions = menuService.selectMenuPermsByUserId(userId);
        
        // 存入缓存
        redisUtils.setCacheObject(cacheKey, permissions, CACHE_DURATION);
        
        return permissions;
    }
    
    /**
     * 清除用户权限缓存
     */
    public void clearUserPermissionCache(Long userId) {
        String cacheKey = PERMISSION_CACHE_KEY + "user:" + userId;
        redisUtils.deleteObject(cacheKey);
    }
    
    /**
     * 批量清除权限缓存
     */
    public void clearPermissionCache(String pattern) {
        Set<String> keys = redisUtils.keys(PERMISSION_CACHE_KEY + pattern);
        if (!keys.isEmpty()) {
            redisUtils.deleteObject(keys);
        }
    }
}

总结

本文深入介绍了Sa-Token的高级特性,包括:

  1. 监听器机制:登录、注销、踢人等事件的监听和处理
  2. 拦截器配置:全局和细粒度的权限拦截
  3. API权限校验:RESTful API、版本控制、第三方API的权限设计
  4. 自定义权限逻辑:复杂业务场景的权限校验实现
  5. 性能优化:权限缓存和查询优化

这些高级特性让Sa-Token在RuoYi-Vue-Plus中能够应对各种复杂的权限控制需求,为企业级应用提供了强大的安全保障。

至此,Sa-Token权限认证体系的深度解析就完成了。在下一个系列中,我们将探讨MyBatis-Plus数据持久层技术。

参考资料


网站公告

今日签到

点亮在社区的每一天
去签到