👋 大家好,我是 阿问学长
!专注于分享优质开源项目
解析、毕业设计项目指导
支持、幼小初高
的教辅资料
推荐等,欢迎关注交流!🚀
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的高级特性,包括:
- 监听器机制:登录、注销、踢人等事件的监听和处理
- 拦截器配置:全局和细粒度的权限拦截
- API权限校验:RESTful API、版本控制、第三方API的权限设计
- 自定义权限逻辑:复杂业务场景的权限校验实现
- 性能优化:权限缓存和查询优化
这些高级特性让Sa-Token在RuoYi-Vue-Plus中能够应对各种复杂的权限控制需求,为企业级应用提供了强大的安全保障。
至此,Sa-Token权限认证体系的深度解析就完成了。在下一个系列中,我们将探讨MyBatis-Plus数据持久层技术。