
其实我早想把若依这套权限认证给换掉了。若依自身的权限模型确实完整但Spring Security JWT 这套组合用在中小型项目里配置重、概念多、调试麻烦光是一个“自定义过滤器链”就够新手研究大半天。sa-token 走的是另一种思路轻量、开箱即用、API 简洁登录、鉴权、踢人下线、账号封禁这些高频能力直接封装好了十几分钟就能接进来。把若依的认证底座从 Spring Security 换成 sa-token 之后最大的感受就是代码量变少了出问题也好定位了。这篇博文就围绕“若依框架集成 sa-token”这件事把整个替换思路、核心代码改动、前后端联调细节和常见坑位都过一遍。内容面向刚开始接触若依二次开发的 Java 后端也适合想把老项目认证体系换轻的人参考。1. 替换前先搞清楚若依的认证链和 sa-token 的差异1.1 若依默认的 Spring Security JWT 是怎么工作的RuoYi-Vue 这个前后端分离版本默认的登录认证链路大概是这样的用户提交用户名密码后端用 AuthenticationManager 走一遍 Spring Security 的认证流程认证通过后生成一个 LoginUser 对象里面塞用户信息、权限集合、角色集合然后通过 TokenService 创建一个 JWT token 返回给前端。前端把 token 存起来后续每次请求在 Header 里带Authorization: Bearer xxxxx后端有个 OncePerRequestFilter 把这个 token 解析出来再通过 SecurityContextHolder 把用户信息塞进 SecurityContext供后续业务代码获取当前登录人。这套链路的问题在于Spring Security 的过滤器链本身是全局性的一旦接入所有请求都要经过它的过滤器链如果你想对某些接口放行、某些接口必须登录就得写 SecurityConfig 里的 authorizeRequests 规则。若依在这上面封装了一层但底层还是那套东西。对于只做管理系统的人来说Spring Security 提供的大部分能力根本没用到却要为它的复杂配置买单。1.2 sa-token 到底解决了什么问题sa-token 是一个轻量的 Java 权限认证框架核心就两个词登录、鉴权。它默认把 token 存在内存或 Redis 中通过一个拦截器或者注解就能实现对接口的保护。对比 Spring Securitysa-token 不需要你理解过滤器链、认证管理器、授权管理器这些概念只需要记住几个 APIStpUtil.login(uid)是登录StpUtil.checkLogin()是校验登录SaCheckPermission(xxx)是权限校验。这种模型对业务开发人员非常友好出了问题也容易定位。从我的实际感受看sa-token 对若依这种“管理后台 RBAC 权限模型”的项目来说是真的合适。若依已经做好了用户、角色、菜单权限的维护sa-token 只负责“你登录了没有”“你够不够权限调用这个接口”这两个核心动作。两边刚好能对齐。1.3 替换之前必须确认的三个问题在动手之前有几个问题需要先想清楚不然后面会反复返工。第一个是版本兼容。若依的版本很多RuoYi-Vue前后端分离、RuoYi-Vue-Plus增强版、RuoYi-Cloud微服务版它们对安全框架的使用方式有差异。这篇博文默认针对 RuoYi-Vue 传统分离版讲解其他版本集成思路类似但配置类路径和依赖位置可能不同要灵活调整。第二个是认证方式的选择。sa-token 默认的 token 风格是 uuid 字符串保存在 Redis 或内存中如果你希望保持无状态 JWT 方式sa-token 也支持但配置和代码会多一层。我建议直接把 sa-token 的默认模式就行token 存 Redis配合若依自带的 Redis 缓存很自然。第三个是前端改动范围。若依前端的请求工具里默认在请求头加了Authorization: Bearer token的拼接逻辑。sa-token 默认的请求头是satoken如果你不想改前端太多代码可以通过配置把 token-name 设置为Authorization同时去掉前端代码里的Bearer前缀。这两个地方必须对齐否则会出现“登录成功但接口全部 401”的尴尬情况。2. 依赖调整与基础配置把 Security 换掉把 sa-token 请进来2.1 排除 Spring Security 依赖若依的父工程是 RuoYiApplication里面通过 ruoyi-framework 模块引入了 Spring Security。在 ruoyi-framework 模块的 pom.xml 中找到这段依赖直接把spring-boot-starter-security换成 sa-token 的依赖。我改造时的做法是!-- ruoyi-framework/pom.xml -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency把这一段移除或者注释掉然后添加!-- sa-token 核心依赖 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.34.0/version /dependency !-- sa-token 整合 Redis使用 Jackjson 序列化方式 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-redis-jackson/artifactId version1.34.0/version /dependency这个sa-token-redis-jackson依赖很关键。若依本身用了 Redis 缓存登录用户信息sa-token 默认把 token 会话放到内存但生产环境必须多实例部署所以需要把会话放到 Redis 中共享。sa-token-redis-jackson会把 sa-token 的会话数据通过 Jackson 序列化后写入 Redis和若依的缓存机制能共存。注意sa-token 的 Redis 集成依赖会传递依赖一个commons-pool2如果你原来没有这个依赖记得一起加上否则连接池初始化会报错。可以直接拿若依原有 pom 里的 dependencies 对照检查。2.2 处理若依原本的 Security 配置类和工具类把 Spring Security 依赖移除之后项目里凡是引用了 Security 相关包的类都会编译报错。若依工程里主要有这几个地方ruoyi-framework下的SecurityConfig整个删掉或注释掉ruoyi-common下的SecurityUtils依赖SecurityContextHolder这个类提供获取当前登录用户、用户ID、用户名称的静态方法需要重写ruoyi-framework下的TokenService依赖 JWT 生成 token登录后要被 sa-token 替代若依各个 Controller 上的PreAuthorize(ss.hasPermi(xxx))注解Spring Security 的权限注解需要替换成 sa-token 的SaCheckPermission建议处理顺序是先删 SecurityConfig再改 SecurityUtils然后改登录逻辑最后处理注解。这样每改一步都能让项目重新编译通过不会一堆错误堆在一起无从下手。2.3 sa-token 核心配置项说明在若依的application.yml中新增 sa-token 配置块。我常用的配置是# sa-token 配置 sa-token: # token 名称同时也是 cookie 名称 token-name: Authorization # token 有效期单位秒24小时 timeout: 86400 # token 最低活跃频率-1 代表不限制 active-timeout: -1 # 是否允许同一账号并发登录 is-concurrent: true # 多人登录同一账号时是否共用一个 token is-share: false # token 风格 token-style: uuid # 是否输出操作日志 is-log: false重点说两个配置token-name: Authorization是为了兼容若依前端已有的请求头格式。sa-token 默认从请求头satoken中读取 token但是若依前端的request.js里面写的是Authorization不改的话前后端就对不上。这里改成Authorization之后前端不用动请求拦截器。is-concurrent和is-share这两个配置也推荐搞清楚。is-concurrent: true表示允许同一个账号多处登录is-share: false表示每次登录都会生成一个新 token而不是大家共用一个 token。如果你希望实现“一个账号只能在一处登录后来登录的人把前面的人挤下线”可以设置is-concurrent: false这样后登录的会顶掉之前的会话。这两个参数在实际项目中很常用但若依默认的 JWT 方案里想实现这个能力得自己写逻辑sa-token 一行配置就搞定了。2.4 把 UserDetailsService 从 Security 语境中剥离若依原本用户登录校验依赖 Spring Security 的UserDetailsService和UserDetails接口。改造之后这层已经不需要了但若依的业务代码里大量使用了LoginUser这个类来承载用户信息。保留LoginUser类只是让它不再实现UserDetails接口变成一个普通的 POJO只保存必要字段用户信息、权限集合、角色集合。改造后的LoginUser可以简化为public class LoginUser { private Long userId; private SysUser user; private SetString permissions; private SetString roles; }这样改动影响最小登录逻辑里构造这个对象后通过 sa-token 的Session保存起来。3. 核心代码改造登录、拦截、权限注解、用户上下文3.1 登录接口改造成 sa-token 方式若依原本的登录逻辑在SysLoginService里核心代码是loginUser userDetailsService.loadUserByUsername(username)然后校验密码最后通过tokenService.createToken(loginUser)签发 token。改成 sa-token 之后流程变成// 1. 查询用户 SysUser user userMapper.selectUserByUserName(username); // 2. 校验密码这里直接用若依的 PasswordEncoder 或手动比对 if (!SecurityUtils.matchesPassword(password, user.getPassword())) { throw new ServiceException(用户不存在或密码错误); } // 3. 构造登录用户对象若依的 LoginUser LoginUser loginUser new LoginUser(); loginUser.setUserId(user.getUserId()); loginUser.setUser(user); loginUser.setPermissions(menuService.selectMenuPermsByUserId(user.getUserId())); loginUser.setRoles(roleService.selectRoleKeysByUserId(user.getUserId())); // 4. sa-token 登录 StpUtil.login(user.getUserId()); // 5. 把 loginUser 保存到会话中后续随时取用 StpUtil.getSession().set(loginUser, loginUser); // 6. 返回 token 给前端 String token StpUtil.getTokenValue();这里有个问题若依原本的密码校验是passwordEncoder.matches(rawPassword, encodedPassword)里面用了 BCrypt。改造后不能直接用StpUtil.login自带的密码校验能力因为 sa-token 不关心你的密码是什么格式它只管登录会话。所以密码还是走若依原来的校验逻辑校验通过后再调用StpUtil.login建立会话。登录成功后前端会拿到这个 token后续请求带上它即可。sa-token 在 Redis 中存 session下次请求时根据 token 找到 session从而知道当前用户是谁。3.2 注册 SaInterceptor 实现路由拦截若依原本通过 Spring Security 的SecurityConfig来配置哪些接口需要登录、哪些接口匿名访问。改造之后这项职责交给 sa-token 的拦截器。新建一个SaTokenConfigure配置类Configuration public class SaTokenConfigure implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - { // 所有请求默认都要登录除了下面放行的路径 StpUtil.checkLogin(); })) .addPathPatterns(/**) .excludePathPatterns( /login, /captchaImage, /register, /error, /doc.html, /swagger-resources/**, /webjars/**, /v3/api-docs/** ); } }如果项目里有的接口需要登录、有的接口不需要建议采用“全局拦截 白名单排除”的方式维护起来最方便。若依原来的/login、/captchaImage这些匿名接口要加进白名单否则一启动就要登录登录接口被拦截就循环重定向了。SaInterceptor里面的 Lambda 表达式是 sa-token 1.30 之后的新写法如果你用的版本比较老可能是一个SaInterceptor()然后单独写 HandlerInterceptor 实现类两种方式效果一样按自己版本选即可。提示SaInterceptor默认只校验登录不包括权限校验。权限校验要么在每个 Controller 方法上加SaCheckPermission注解要么在拦截器里手动调用StpUtil.checkPermission(...)。用注解的方式更贴合若依原本的写法后续排查也直观。3.3 替换权限注解从 PreAuthorize 到 SaCheckPermission若依页面上的按钮、接口里的权限控制大量使用了 Spring Security 的注解PreAuthorize(ss.hasPermi(system:user:list)) GetMapping(/list) public TableDataInfo list(SysUser user) { ... }替换成 sa-token 的注解就是SaCheckPermission(system:user:list) GetMapping(/list) public TableDataInfo list(SysUser user) { ... }这个改动最直接把PreAuthorize(ss.hasPermi(...))的前半个句子删掉换成SaCheckPermission(...)。我在实际操作中主要用正则替换处理批量改完再逐个检查几百个接口也能很快搞定。需要注意SaCheckPermission要生效必须告诉 sa-token 当前登录用户有哪些权限。sa-token 通过StpInterface接口来获取权限集合和角色集合如果你不实现这个接口那么所有接口都校验不过会一直报“无权限”。所以需要写一个StpInterfaceImpl实现类核心代码如下Component public class StpInterfaceImpl implements StpInterface { Autowired private ISysMenuService menuService; Override public ListString getPermissionList(Object loginId, String loginType) { // 通过 loginId 查询用户权限这里可以直接调用若依菜单服务 SysUser user new SysUser(); user.setUserId(Long.valueOf(loginId.toString())); return menuService.selectMenuPermsByUserId(user.getUserId()); } Override public ListString getRoleList(Object loginId, String loginType) { // 查询用户角色标识例如 admin return new ArrayList(); } }getPermissionList返回的是权限标识的集合比如system:user:list、system:user:query这些正好对应若依菜单表里的 perms 字段。getRoleList返回的是角色标识比如admin如果你项目里只用权限判断角色集合可以返回空列表不影响使用。这个StpInterfaceImpl是整个权限校验的桥梁没有它SaCheckPermission完全失效所以务必确认写对。3.4 获取当前登录用户的新方式若依原本业务代码里获取当前登录用户都是这么写的SysUser user SecurityUtils.getLoginUser().getUser();改造之后SecurityUtils里的SecurityContextHolder已经没有内容了必须换成 sa-token 的方式。我重新封装了一个工具类LoginHelperpublic class LoginHelper { public static SysUser getUser() { LoginUser loginUser (LoginUser) StpUtil.getSession().get(loginUser); return loginUser.getUser(); } public static Long getUserId() { return getUser().getUserId(); } public static String getUsername() { return getUser().getUserName(); } }在业务代码中把SecurityUtils.getLoginUser().getUser()全部替换为LoginHelper.getUser()即可。这个工具类的核心逻辑就是sa-token 根据请求头里的 token 找到 Redis 中的 session再取出我们登录时塞进去的loginUser。这里有一个性能上的经验如果你项目里的接口调用量很大频繁从 session 里取loginUser也是 Redis 操作。如果每次请求都要用用户信息建议用若依的ThreadLocal缓存机制或者把loginUser放到 sa-token 的请求级临时上下文里。不过一般管理系统没那么极端直接从 session 取问题不大。4. 前后端联调与细节问题token 头、CORS、白名单一个都不能少4.1 token 请求头到底该叫 Authorization 还是 satoken这是集成 sa-token 到若依之后最常遇到的坑没有之一。sa-token 默认从请求头satoken里读 token如果你在配置里设置了token-name: satoken那前端必须在请求头里加satoken: xxxx。若依的前端代码默认是Authorization: Bearer xxxx所以必须二选一对齐。我的建议是配置token-name: Authorization然后修改若依前端request.js去掉 token 前面的Bearer前缀。原因是很多若依二次开发的项目里开发者已经习惯 Authorization 这个头名称后面对接其他系统也更通用。前端改动点在utils/request.js里// 修改前 config.headers[Authorization] Bearer getToken() // 修改后 config.headers[Authorization] getToken()登录方法里返回的 token 是 sa-token 自己生成的 uuid 字符串没有Bearer前缀所以前端不要画蛇添足。如果你不想改前端另一个办法是把 sa-token 的token-name配置成Authorization然后自定义 token 前缀让 sa-token 在解析时自动识别带Bearer的 token。但是这样需要写额外的 SaTokenAction 实现类不如前端改一行来得快。4.2 跨域配置别和 Security 时代混淆若依原本的跨域配置有两种写法一种是 Spring Security 的SecurityConfig里加cors()另一种是项目里单独的CorsConfig实现了WebMvcConfigurer。Spring Security 移除之后原来依赖于 Security 的跨域配置就失效了你需要确认项目里是否有一个独立的CorsConfig。如果你发现删除 SecurityConfig 之后前端请求出现跨域错误那就重新写一个独立跨域配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这个配置是基于 Spring MVC 的不依赖 Security所以无论你用什么认证框架都有效。注意allowedOriginPatterns(*)和allowCredentials(true)同时设置时不能用allowedOrigins(*)否则浏览器会拦截。4.3 登录接口被拦截的排查思路很多朋友改完以后发现连登录接口都返回“未登录”。这种情况基本是拦截器配置的问题核心排查步骤有三步第一步确认登录接口的路径是否在excludePathPatterns白名单里。若依登录接口是POST /login如果你的项目加了 context-path比如http://localhost:8080/ruoyi/login那么白名单里也应该是完整路径。第二步确认SaInterceptor里的StpUtil.checkLogin()是不是全局拦截了所有请求然后白名单没写对。可以暂时先打印拦截路径看看实际请求的 url 是什么。第三步检查是否有多个拦截器。若依某些版本里自带了一个 HandlerInterceptor 处理页面访问权限如果你没有删除它它也会拦截请求但报错信息和 sa-token 又不一样容易混淆。4.4 ruoyi satoken 签名问题到底是怎么回事搜“ruoyi satoken 签名”这个关键词的朋友大概率是被 JWT 的“签名”概念带过来的。若依原本的 JWT token 里有签名算法token 生成后需要验签所以都知道“签名”这回事。但 sa-token 默认并没有签名机制它就是一个随机 uuid 字符串对应 Redis 里的一份 session没有 JWT 那种“签名”概念。如果你用 sa-token 时对“签名”有要求比如希望 token 里带用户信息、且防篡改那可以开启 sa-token 的 JWT 模式引入sa-token-jwt依赖然后在登录后调用StpUtil.login时传入额外参数。但在我看来非必要不要上 JWT。sa-token 默认的模式性能更好也更简单UUID token 存在 Redis 里天然支持踢人下线、会话管理这些功能换成 JWT 之后这些能力反而会受限。所以“签名”这个需求要拆开看如果只是内部系统默认 uuid 模式完全够用。5. 常见问题排查与避坑技巧5.1 IDEA 导入若依项目报 error adding module to project: null这个错误很多人在从 Git 拉取若依代码到 IDEA 时会遇到。它本身和 sa-token 集成没关系属于 IDEA 的 Maven 模块导入 bug但也是刚准备改造 Tomcat 时容易绊一脚的报错。解决方式按下面顺序排查把 IDEA 的File - Invalidate Caches / Restart先来一遍清掉索引缓存在 Maven 面板里点刷新按钮看是不是依赖下载了一半导致模块解析失败确认项目的.idea目录和.iml文件是从 Git 带过来的如果是最好删除后让 IDEA 重新识别如果是 JDK 版本不匹配造成的检查 Project Structure 里的 SDK 是否为 JDK 8 或项目要求的版本若依老版本要求 JDK 8不能设置成 17否则 Maven 编译也会有一堆想不到的报错。用 Git 拉下来、在 IDEA 里打开、然后把当前分支切到主分支再重试这个操作本身不会造成模块问题但反复切换分支时 IDEA 索引容易兜不住报错后先关掉自动导入等模块稳定后再打开。5.2 登录成功后访问接口仍然 401出现这个情况一般不是登录没成功而是请求没带 token 或者 token 头名字不一致。比如你前端请求头带的是Authorization后端token-name配置的却是satoken那 sa-token 自然找不到 token。另一个可能性是前端 Vue 项目里getToken()存的值是带Bearer前缀的旧 token登录成功后新的 token 没覆盖上去或者 localStorage 里存了多个 key。排查方法很快用 Postman 先手动登录拿到 token再在请求头里加上Authorization: token值如果通了说明后端没问题问题在前端如果仍然 401就检查后端的 token-name 配置和拦截器白名单。5.3 接口权限全部校验失败SaCheckPermission 不生效这个基本可以确定是StpInterface没有实现或者没有加入 Spring 容器。你可以先确认注入的 Service 是否正常如果menuService为 null大概率是扫描包没覆盖到。另一个隐蔽原因是StpInterfaceImpl里调用的权限查询方法返回的是SetString而接口要求的是ListString转换没写对导致返回空集合。建议在getPermissionList里临时打印一下查询结果确认当前登录用户确实取到了权限标识。比如ListString perms menuService.selectMenuPermsByUserId(user.getUserId()); System.out.println(当前用户权限 perms);输出如果是[system:user:list, system:role:list]这样的格式说明 StpInterface 本身没问题再去查注解路径是否正确。5.4 退出登录后 token 还能用sa-token 的退出登录调用StpUtil.logout()它会把 Redis 里的会话删除之后再用旧 token 访问接口会提示未登录。如果你发现退出后 token 依然有效一般有两个原因一个是没有把原来若依前端“退出登录”接口里的tokenService.delLoginUser替换成StpUtil.logout()另一个是若依的LogoutController还是旧逻辑只清除了 Redis 中的 JWT token没有调用 sa-token 的 logout。若依原本的退出逻辑在SysLoginService里有logout()方法改造后建议统一替换成public void logout() { StpUtil.logout(); }如果业务中有“踢人下线”需求比如管理员强制让某个用户退出可以直接用StpUtil.kickout(userId)这个 API 非常实用Spring Security 原生实现起来要多写不少代码。下面把集成过程中最容易踩到的坑整理成一个速查表方便你直接对照排查问题现象可能原因解决方法登录接口被拦截无法访问白名单没加登录路径在 SaInterceptor 的 excludePathPatterns 中增加/login、/captchaImage等登录成功后接口全部 401请求头 token-name 不一致统一token-name去掉前端Bearer前缀SaCheckPermission 全部失效StpInterface 未实现或未被扫描实现StpInterface注入 Spring 容器并返回权限集合退出登录后 token 仍可用退出接口未调用StpUtil.logout将若依原 logout 方法替换为StpUtil.logout()同账号重复登录互相顶掉is-concurrent未配置yml 设置is-concurrent: trueRedis 连接报错缺少 commons-pool2 依赖添加commons-pool2依赖5.5 改造完成后一定要做的事回归现有接口权限认证是横切所有接口的基础设施改完不能只在登录接口上验证一下就交付。我的做法是把若依自带的系统管理模块全部过一遍每个菜单下点几个关键按钮确认新增、编辑、删除、导出这些敏感操作的权限校验都正常。这是因为若依的权限点非常细有些按钮走的是hasPermi的前端判断有些是后端接口的PreAuthorize如果只替换了后端注解而忽略了前端v-hasPermi指令的对接就会出现“按钮可见但点击无权限”的情况。另外记得检查若依的代码生成功能。若依的代码生成器会生成带PreAuthorize注解的 Controller 代码你后面用若依生成新模块时生成的代码还是旧语法。要么改模板要么在生成后手动把注解替换为SaCheckPermission。我当时偷懒直接全局搜索替换但后来发现生成器的模板文件也要一起改否则每生成一个模块就要手工处理一次。若依的生成模板在ruoyi-generator/src/main/resources/vm/java/controller.java.vm里把里面的PreAuthorize(ss.hasPermi(...))改成SaCheckPermission(...)模板就行一劳永逸。6. 集成完成后的心得与建议这套若依集成 sa-token 的方案前前后后我在不同项目里改过三四次每次都能节省不少时间。相比 Spring Securitysa-token 最直观的优势就是“问题好查”token 对不上就是 token 对不上权限返回空就是权限返回空你不用在过滤器链里翻来翻去找哪个环节吞了异常。如果你现在正准备动手我的建议是不要一上来就追求把所有接口里的 Spring Security 相关代码一次性换完。先把依赖换掉把登录、拦截器、StpInterface这三条核心链路跑通系统能正常登录退出了再批量替换PreAuthorize注解和SecurityUtils调用。分批改的好处是每次报错范围小定位快不至于改完几百个文件之后出一堆问题连从哪排查都不知道。还有一个很多人忽略的点sa-token 默认的token-style是 uuidtoken 会比较长。如果你有 URL 参数传递 token 的场景可能要考虑换短一点的自定义 token 风格比如token-style: tik。不过局域网管理系统一般用请求头传 token这个问题影响不大。最后分享一个小技巧sa-token 的StpUtil.getSession()是支持跨请求持久化的你可以在登录后往 session 里塞任意对象。若依原本的 LoginUser 只缓存了用户信息、权限、角色改造后你可以顺手把用户的部门、岗位、数据权限范围也塞进去后面做数据权限过滤的时候直接从 session 里取省掉一次数据库查询。这也是 sa-token 的 Session 设计比 JWT 无状态方案更灵活的地方别浪费这个能力。