Sa-Token到底怎么鉴权的???(个人项目复盘)

📅 2026/7/1 2:27:17
Sa-Token到底怎么鉴权的???(个人项目复盘)
书接上回。请求 通过Gateway 中的两个过滤器SaReactorFilter、AddUserId2HeaderFilter通过了用户认证完成了权限校验将请求路由到了相应服务。会不会产生一个疑问uerId、用户权限到底是如何校验的到底是谁在鉴权加下来我们就详细的盘一盘 Satoken 到底再干什么~其实用户认证、权限校验完全是由 Sa-Token 框架内部完成的所以我们就来读一读源码叭用户认证public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 拦截地址 .addInclude(/**) /* 拦截全部path */ // 鉴权方法每次访问进入 .setAuth(obj - { log.info( SaReactorFilter, Path: {}, SaHolder.getRequest().getRequestPath()); // 登录校验 SaRouter.match(/**) // 拦截所有路由 .notMatch(/auth/login) // 排除登录接口 .notMatch(/auth/verification/code/send) // 排除验证码发送接口 .check(r - StpUtil.checkLogin()) // 校验是否登录 ; // 权限认证 -- 不同模块, 校验不同权限 SaRouter.match(/auth/logout, r - StpUtil.checkRole(common_user)); // 更多匹配 ... */ }) // 异常处理方法每次setAuth函数出现异常时进入 .setError(e - { // return SaResult.error(e.getMessage()); // 手动抛出异常抛给全局异常处理器 if (e instanceof NotLoginException) { throw new NotLoginException(e.getMessage(), null, null); } else if (e instanceof NotPermissionException || e instanceof NotRoleException) { throw new NotPermissionException(e.getMessage()); } else { throw new RuntimeException(e.getMessage()); } }) ; }前端发来请求该请求携带我们上节说的 用户登录 返回的token请求到达SaReactorFilter 拦截所有请求除白名单外先去 StpUtil.checkLogin()。欧克开始看源码交给大家一个 IDEA 的快捷键CTRL唱、跳、rap、篮球 B即可查看源码或者CTRL鼠标单击。public static void checkLogin() { stpLogic.checkLogin(); }继续查看stpLogic.checkLogin()后续连续的代码快都是继续查看的我就不说啦public void checkLogin() { this.getLoginId(); }public Object getLoginId() { if (this.isSwitch()) { return this.getSwitchLoginId(); } else { String tokenValue this.getTokenValue(true); if (SaFoxUtil.isEmpty(tokenValue)) { throw NotLoginException.newInstance(this.loginType, -1, 未能读取到有效 token, (String)null).setCode(11011); } else { String loginId this.getLoginIdNotHandle(tokenValue); if (SaFoxUtil.isEmpty(loginId)) { throw NotLoginException.newInstance(this.loginType, -2, token 无效, tokenValue).setCode(11012); } else if (loginId.equals(-3)) { throw NotLoginException.newInstance(this.loginType, -3, token 已过期, tokenValue).setCode(11013); } else if (loginId.equals(-4)) { throw NotLoginException.newInstance(this.loginType, -4, token 已被顶下线, tokenValue).setCode(11014); } else if (loginId.equals(-5)) { throw NotLoginException.newInstance(this.loginType, -5, token 已被踢下线, tokenValue).setCode(11015); } else { if (this.isOpenCheckActiveTimeout()) { this.checkActiveTimeout(tokenValue); if (this.getConfigOrGlobal().getAutoRenew()) { this.updateLastActiveToNow(tokenValue); } } return loginId; } } } }到此为止源码也就到头了可以看到起到作用的就是 getLoginId这段代码很好理解我们只需要关注第一个 else 的代码如果可以获取到 Token什么-3-4-5都是异常信息最后一个else 才是重点没有异常信息也就是登录成功返回 loginId 即 userId。至此登录认证也就完成没有抛出任何异常代码也就往下执行就到了角色校验。角色校验角色校验就要设计一些前者内容了我们要 在我们项目的 user 服务启动时将校色和权限的一一映射关系同步到 redis 中去方便 Sa-token 校验。public class PushRolePermissions2RedisRunner implements ApplicationRunner { Resource private RedisTemplateString, String redisTemplate; Resource private RoleDOMapper roleDOMapper; Resource private PermissionDOMapper permissionDOMapper; Resource private RolePermissionDOMapper rolePermissionDOMapper; // 权限同步标记 Key private static final String PUSH_PERMISSION_FLAG push.permission.flag; Override public void run(ApplicationArguments args) { log.info( 服务启动开始同步角色权限数据到 Redis 中...); try { // 是否能够同步数据: 原子操作只有在键 PUSH_PERMISSION_FLAG 不存在时才会设置该键的值为 1并设置过期时间为 1 天 boolean canPushed redisTemplate.opsForValue().setIfAbsent(PUSH_PERMISSION_FLAG, 1, 1, TimeUnit.DAYS); // 如果无法同步权限数据 if (!canPushed) { log.warn( 角色权限数据已经同步至 Redis 中不再同步...); return; } // 查询出所有角色 ListRoleDO roleDOS roleDOMapper.selectEnabledList(); if (CollUtil.isNotEmpty(roleDOS)) { // 拿到所有角色的 ID ListLong roleIds roleDOS.stream().map(RoleDO::getId).toList(); // 根据角色 ID, 批量查询出所有角色对应的权限 ListRolePermissionDO rolePermissionDOS rolePermissionDOMapper.selectByRoleIds(roleIds); // 按角色 ID 分组, 每个角色 ID 对应多个权限 ID MapLong, ListLong roleIdPermissionIdsMap rolePermissionDOS.stream().collect( Collectors.groupingBy(RolePermissionDO::getRoleId, Collectors.mapping(RolePermissionDO::getPermissionId, Collectors.toList())) ); // 查询 APP 端所有被启用的权限 ListPermissionDO permissionDOS permissionDOMapper.selectAppEnabledList(); // 权限 ID - 权限 DO MapLong, PermissionDO permissionIdDOMap permissionDOS.stream().collect( Collectors.toMap(PermissionDO::getId, permissionDO - permissionDO) ); // 组织 角色-权限 关系 MapString, ListString roleKeyPermissionsMap Maps.newHashMap(); // 循环所有角色 roleDOS.forEach(roleDO - { // 当前角色 ID Long roleId roleDO.getId(); // 当前角色 roleKey String roleKey roleDO.getRoleKey(); // 当前角色 ID 对应的权限 ID 集合 ListLong permissionIds roleIdPermissionIdsMap.get(roleId); if (CollUtil.isNotEmpty(permissionIds)) { ListString permissionKeys Lists.newArrayList(); permissionIds.forEach(permissionId - { // 根据权限 ID 获取具体的权限 DO 对象 PermissionDO permissionDO permissionIdDOMap.get(permissionId); permissionKeys.add(permissionDO.getPermissionKey()); }); roleKeyPermissionsMap.put(roleKey, permissionKeys); } }); // 同步至 Redis 中方便后续网关查询 Redis, 用于鉴权 roleKeyPermissionsMap.forEach((roleKey, permissions) - { String key RedisKeyConstants.buildRolePermissionsKey(roleKey); redisTemplate.opsForValue().set(key, JsonUtils.toJsonString(permissions)); }); } log.info( 服务启动成功同步角色权限数据到 Redis 中...); } catch (Exception e) { log.error( 同步角色权限数据到 Redis 中失败: , e); } } }该 角色—权限 的同步逻辑很简单我们 先把所有的 角色Id 转换成一个 list, 随后再通过 角色Id 获取出对应的 权限 集合随后再通过 Collectors.groupingBy将 角色对应的权限 写 Map 集合中然后 将角色—权限同步到 redis 中。然后开始我们的 源码追踪。SaRouter.match(/auth/logout, r - StpUtil.checkRole(common_user));public static void checkRole(String role) { stpLogic.checkRole(role); }public void checkRole(String role) { if (!this.hasRole(this.getLoginId(), role)) { throw (new NotRoleException(role, this.loginType)).setCode(11041); } }public boolean hasRole(Object loginId, String role) { return this.hasElement(this.getRoleList(loginId), role); }public boolean hasElement(ListString list, String element) { return (Boolean)SaStrategy.instance.hasElement.apply(list, element); }也就是说只要我们传入的 common_user 在 list 集合中就可以通过角色的校验。那么好问题来了这是什么时候校验的呢这就是困惑我很久的问题自从跟踪了源码您猜怎么着眼也不花了头也不晕了代码也看懂了。嗨嗨。这不就不得提到我们写到的 StpInterface 接口的实现类了。这个实现类就是在 hsaRole 的getRoleList。那么好这下是不是就通了。hasElement 返回true。Component Slf4j public class StpInterfaceImpl implements StpInterface { Resource private RedisTemplateString, String redisTemplate; Resource private ObjectMapper objectMapper; SneakyThrows Override public ListString getRoleList(Object loginId, String loginType) { log.info(## 获取用户角色列表, loginId: {}, loginId); // 构建 用户-角色 Redis Key String userRolesKey RedisKeyConstants.buildUserRoleKey(Long.valueOf(loginId.toString())); // 根据用户 ID 从 Redis 中获取该用户的角色集合 String useRolesValue redisTemplate.opsForValue().get(userRolesKey); if (StringUtils.isBlank(useRolesValue)) { return null; } // 将 JSON 字符串转换为 ListString 集合 return objectMapper.readValue(useRolesValue, new TypeReference() {}); } }至此角色校验也就通过了权限校验也是类似的可以自己追踪以下源码很有意思的。到现在用户认证没有异常信息角色认证也返回的true。SaReactorFilter 过滤器完美过滤完了请求中的信息就可 将请求发送到 AddUserId2HeaderFilter 中去了。完美完美吃完饭写就是不饿写的也有劲哈哈哈哈哈哈~第二次写文章如有不对的地方欢迎批评指正感谢可以看到这里哦~