尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

若依框架从Spring Security替换为SaToken的完整改造实践

若依框架从Spring Security替换为SaToken的完整改造实践 做Java后台开发的朋友对若依RuoYi这套脚手架一定不陌生项目里要接权限认证我第一个想到的就是直接用它自带的Spring Security方案。但真正上手之后很多人会被Security那套过滤器链、UserDetailsService、权限表达式绕得头晕。尤其是只需要做个简单的登录校验、接口权限拦截搭Security总感觉杀鸡用了牛刀。SaToken这个轻量级权限认证框架API设计非常友好全中文文档核心功能开箱即用。这次我把若依框架里的认证逻辑从Spring Security换成了SaToken整个过程从依赖调整、配置类编写到登录流程改造踩了不少坑今天完整复盘一遍给打算做同样改造的朋友一个能参考的路线。如果你正在纠结要不要替换若依自带的认证体系或者已经被SaToken的“怎么接进RuoYi”卡住这篇文章正好对症。我会把改造的核心思路、版本选型、依赖配置、登录接口替换、权限注解适配、前端Token联动以及常见的报错和排查方法都串起来讲不是只贴代码每一步都会解释为什么这么做。1. 项目概述与集成思路1.1 若依框架与SaToken能擦出什么火花若依框架在国内Java后台管理项目里的普及率很高前后端分离版默认使用的是Spring Security做认证授权Redis做缓存配合JWT实现无状态登录。这套方案本身没有问题稳定性、生态都没得说但对于中小型项目或者快速迭代的业务系统来说它有两个很现实的“重”第一Spring Security的配置成本高。完整的SecurityConfig动辄几百行各种过滤链、认证管理器、密码加密器、异常处理器新手看一眼就容易劝退。哪怕只是改一个放行路径也要搞清楚它在整个过滤器链里的顺序。第二权限注解和业务代码之间的耦合感比较强。Security的注解式权限控制需要手动开启表达式写法对前端同学也不够直观联调时经常因为权限码匹配不上来回扯皮。SaToken的设计理念正好是另外一个路子“简单、可靠、零学习成本”。它的核心只有两个动作——登录时StpUtil.login(id)写入会话校验时StpUtil.checkLogin()或加个注解SaCheckLogin就完成了认证。权限控制则是SaCheckPermission(system:user:list)直接把权限码写在方法上和若依原有的菜单权限模型能无缝对接。最关键的是它对Spring Boot的适配非常顺滑引入一个starter就能跑起来。1.2 整体集成方案选型在动手之前我先明确了一条原则这次改造不是把SaToken和Spring Security同时并存而是彻底切换。因为两套认证体系同时挂在项目里过滤器链会打架登录会话的状态也不好统一管理。具体按这条路线走保留若依的表结构、菜单权限模型、后端代码生成逻辑不动移除Security相关的自动配置和依赖避免过滤器冲突引入SaToken的Spring Boot Starter改为SaToken统一管理登录会话登录接口改成调用SaToken的API生成token返回前端前端保持原来的请求头传递模式把Authorization的value值对接上SaToken生成的token即可。实际操作发现若依后端代码本身的分层结构很干净Controller和Service层基本都是标准的所以改造SaToken时不需要动业务逻辑只要把认证入口和拦截配置替换掉就行。整个改造大概半天时间业务代码零侵入。2. 环境准备与依赖引入2.1 版本选型改造基于的环境是若依前后端分离版v3.8.x这个版本Spring Boot父依赖是2.5.15JDK要求1.8以上。SaToken我用的是当前比较稳定的1.38.0版本。版本选型这里多说一句尽量不要直接拿最新版用要看SaToken官方文档里对Spring Boot 2.x的适配说明。1.38.0这个版本对若依的Spring Boot 2.x系列兼容性测试过后续接入时没有遇到版本冲突的问题。2.2 排除Spring Security依赖若依的ruoyi-framework模块里pom.xml中引入了Spring Security的依赖。如果不排除即使你写了SaToken的拦截器原始的Security过滤器链会把所有请求拦在先导致SaToken根本不起作用。在ruoyi-framework/pom.xml中找到这段依赖直接注释或移除!-- 注释或删除 Spring Security 相关依赖 -- !-- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency --同时ruoyi-common模块里如果引用了security相关的工具类需要一并检查清理。比较典型的是ruoyi-common-security里的SecurityUtils工具类它内部依赖SecurityContextHolder属于Security的API单独留着一个引用会导致项目启动报ClassNotFoundException。2.3 添加SaToken依赖在ruoyi-framework模块的pom.xml中加入SaToken的Spring Boot Starter!-- Sa-Token 权限认证 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.38.0/version /dependency如果你的项目里接入了Redis建议再加一个官方提供的Redis集成包用Redis统一管理token会话这样在集群部署时不会出现“一台机器登录另一台机器不认账”的问题dependency groupIdcn.dev33/groupId artifactIdsa-token-redis-jackson/artifactId version1.38.0/version /dependency注意引入sa-token-redis-jackson后还要确保项目中的Redis连接配置是正常的否则启动时虽然不会直接报错但一旦调用StpUtil.login()就会抛Redis连接异常。2.4 修改application.yml配置在若依的application.yml配置文件中加入SaToken的配置段sa-token: # token名称同时也是cookie名称 token-name: Authorization # token有效期单位秒7天 timeout: 604800 # token最低活跃频率单位秒如果超过这个时间没有访问token失效 active-timeout: -1 # 是否允许同一账号并发登录为true时允许一起登录 is-concurrent: true # 多人登录同一账号时是否共用一个token为true时所有登录共用一个token is-share: false # token风格 token-style: uuid # 是否输出操作日志 is-log: truetoken-name这里我改成了Authorization目的是和若依原来的请求头昵称保持一致。这样前端不用大改登录后后端返回什么token前端照旧放进请求头名叫Authorization的字段里就实现了无缝切换。token-style用默认的uuid即可没必要追新去改定制化程度越高排查问题时越费劲。3. 核心配置类与拦截器实现3.1 编写SaToken配置类在ruoyi-framework模块里新建一个配置类比如叫SaTokenConfigure实现WebMvcConfigurer接口在里面注册SaToken的拦截器package com.ruoyi.framework.config; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.stp.StpUtil; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class SaTokenConfigure implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { // 注册Sa-Token拦截器校验规则为StpUtil.checkLogin()即所有请求必须登录 registry.addInterceptor(new SaInterceptor(handle - StpUtil.checkLogin())) .addPathPatterns(/**) .excludePathPatterns(/login, /captchaImage, /register, /error); } }这里的关键点是若依的登录验证码接口和登录接口本身不能拦截否则用户还没登录就被挡在门外了。captchaImage是生成验证码的接口register是注册接口如果开启了注册功能error是Spring Boot错误页路径放行它可以让异常信息正常展示。这个拦截器相当于之前Security里的HttpSecurity配置中的authorizeRequests()作用是全局拦截除了白名单以外的接口。SaToken的写法比Security的链式配置短得多直观很多。3.2 登录会话的获取方式默认情况下SaToken会把token解析结果放进当前请求上下文中。在业务代码里需要获取当前登录用户时SaToken的关联机制比Security要轻量很多。我通常在Controller里直接通过StpUtil.getLoginIdAsLong()拿用户ID再把这个ID带到Service层去查用户信息GetMapping(/userInfo) public AjaxResult getUserInfo() { // 从SaToken会话中直接取出当前登录用户ID Long userId StpUtil.getLoginIdAsLong(); SysUser user userService.selectUserById(userId); return AjaxResult.success(user); }对比原本若依里要通过SecurityUtils.getLoginUser().getUser()来拿用户SaToken这种从全局会话中取ID的方式要简洁不少弱化了Spring上下文对业务代码的侵入。3.3 全局异常处理适配SaToken的异常体系是基于RuntimeException展开的。框架里自带的顶级异常是NotLoginException登录失效时会抛出。若依原本有全局异常处理器GlobalExceptionHandler我们需要在其中加上SaToken异常的捕获逻辑把这些异常转换成前端能读懂的统一返回格式。新增以下两个方法import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; /** * 未登录异常 */ ExceptionHandler(NotLoginException.class) public AjaxResult handleNotLoginException(NotLoginException e) { return AjaxResult.error(e.getMessage()); } /** * 没有权限异常 */ ExceptionHandler({NotPermissionException.class, NotRoleException.class}) public AjaxResult handleNoAuthException(Exception e) { return AjaxResult.error(抱歉您没有访问权限); }这里有一个细节要提醒SaToken的NotLoginException有两种情况一种是未登录一种是token已过期但请求中带了旧token。在返回给前端时建议把HTTP状态码控制在200由前端根据返回体的code字段来判断逻辑。因为很多前端项目的axios拦截器只要遇到非2xx状态码就会走统一的错误提示频繁弹窗体验很差。但是如果你的前端已经定制好了axios拦截器根据HTTP 401状态码做跳转登录页的逻辑也可以在上面的异常处理方法上加ResponseStatus(HttpStatus.UNAUTHORIZED)让SaToken的未登录异常透传出401状态码。具体怎么选取决于团队前后端约定没有绝对的对错。4. 登录认证流程改造4.1 改造登录接口若依原来的登录流程是前端提交用户名、密码、验证码到/login接口后端先校验验证码再调用AuthenticationManager的authenticate()方法完成认证成功之后生成JWT token返回给前端。改造后登录接口的核心逻辑变得非常清爽PostMapping(/login) public AjaxResult login(RequestBody LoginBody loginBody) { // 1. 校验验证码这段逻辑可以保留若依原样 validateCaptcha(loginBody.getUsername(), loginBody.getCode(), loginBody.getUuid()); // 2. 校验用户名密码 SysUser user userService.selectUserByUserName(loginBody.getUsername()); if (user null) { return AjaxResult.error(用户不存在); } if (!SecurityUtils.matchesPassword(loginBody.getPassword(), user.getPassword())) { return AjaxResult.error(密码错误); } // 3. 校验用户状态是否停用、删除等 if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { return AjaxResult.error(用户已删除); } if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { return AjaxResult.error(用户已被停用); } // 4. SaToken登录生成token StpUtil.login(user.getUserId()); String token StpUtil.getTokenValue(); // 5. 返回给前端 return AjaxResult.success(登录成功, token); }这个过程中我用到了若依自带的SecurityUtils.matchesPassword()做密码校验。如果你在前面把ruoyi-common-security整个模块都移除了那这个工具类就没了此时需要自己用BCrypt来校验或者单独引入spring-security-crypto这个轻量级密码加密库。更省事的做法是直接在ruoyi-common模块里保留下BCrypt的依赖因为密码加密算法本身和认证框架无关纯粹是密码哈希。4.2 设置Token关联信息SaToken的StpUtil.login()方法执行后会话ID会和当前线程、当前请求绑定。如果你想在前端回显登录用户名或者在后端其他接口快速拿到当前登录人的昵称头像可以在登录时把这些数据塞进token的Session里StpUtil.login(user.getUserId()); // 将用户信息存入token Session StpUtil.getSession().set(loginUser, user);取的时候也很简单SysUser loginUser (SysUser) StpUtil.getSession().get(loginUser);如果你之前用过Servlet传统的HttpSession这个API的思路是一模一样的只不过底层的载体从服务器内存变成了token会话。数据量小的话可以这样直接塞进去如果数据量大建议只存userId需要时再去查库避免把token摘要撑得很大。4.3 退出登录逻辑若依原来的退出登录接口是递交给Security的LogoutHandler处理改造后直接用SaToken的一行代码PostMapping(/logout) public AjaxResult logout() { StpUtil.logout(); return AjaxResult.success(退出成功); }StpUtil.logout()会同时做几件事删除当前token的会话数据、清理登录标记、如果开启了Redis存储还会清理Redis里的对应缓存。前端退出的逻辑不用改还是调用这个接口成功后跳回登录页。5. 接口鉴权与权限注解适配5.1 用SaCheckPermission替代PreAuthorize若依原本在Controller方法上用的是Spring Security的PreAuthorize注解形如PreAuthorize(ss.hasPermi(system:user:list))改造后需要换成SaToken的SaCheckPermissionSaCheckPermission(system:user:list) GetMapping(/list) public TableDataInfo list(SysUser user) { startPage(); ListSysUser list userService.selectUserList(user); return getDataTable(list); }这个改动是全文最机械也最耗时的一步。若依所有Controller方法都是统一风格可以用IDE的全局替换功能把PreAuthorize(ss.hasPermi(xxx))批量替换成SaCheckPermission(xxx)速度很快。前端调用接口的逻辑不用动权限码还是那些权限码。5.2 开启注解鉴权SaToken默认不像Spring Security那样自动扫描注解需要在配置类上加上SaCheckPermission注解激活标识。其实SaToken的设计里只要拦截器注册了注解就会自动生效不需要额外的EnableXxx注解。但有一个容易忽略的细节SaToken的注解鉴权是通过拦截器实现的它的作用范围只覆盖被SpringMVC拦截的请求。如果你有内部服务之间互相调用的场景比如使用Feign调用其他微服务接口这些调用不经过Controller层拦截器注解校验就不会生效。这种情况下要么在Feign调用的入口处手动调用StpUtil.checkPermission()要么把一个微服务内部的接口排除在鉴权之外保证只有对外暴露的网关层做统一鉴权。5.3 自定义权限校验扩展我见过不少项目除了按钮级权限码还需要额外校验数据范围比如“部门经理只能看本部门数据”。Spring Security的实现方式是自定义PermissionEvaluatorSaToken的扩展相对直接一些你可以写一个自定义的权限校验工具类Component public class StpKit { /** * 校验当前用户是否属于指定部门 */ public boolean isDeptAdmin(Long deptId) { SysUser user (SysUser) StpUtil.getSession().get(loginUser); if (user null) { return false; } return deptId.equals(user.getDeptId()); } }然后在业务逻辑中手动调用SaCheckPermission(system:user:list) GetMapping(/list) public TableDataInfo list(SysUser user) { if (!StpKit.isDeptAdmin(user.getDeptId())) { return error(无权访问该部门数据); } // ... }这种方式比注解更灵活适合数据权限维度复杂的场景。SaToken官方还支持自定义注解拦截器去实现更高级的权限模型但普通项目用上面的方式足够了不要把架构搞得过度设计。6. 前后端对接与潜在问题排查6.1 前端Token存储与请求头联动前端若依项目普遍是用Vue2 Vuex axios这套组合登录后token存在localStorage里。改造完成后后端登录接口返回的token会变成SaToken生成的新格式uuid字符串前端拆包时不需要改逻辑token怎么存还怎么存。需要注意的地方在axios的请求拦截器。若依原生写法是service.interceptors.request.use(config { if (getToken()) { config.headers[Authorization] getToken() } return config })这段代码直接兼容SaToken的token-name。如果你把yml里的token-name设置成了别的比如satoken那这里也要一起改成config.headers[satoken] getToken()。前后端字段保持一致是联调的第一原则。6.2 IDEA导入若依项目报错“error adding module to project: null”很多朋友第一步就卡在项目导入上IDEA报了一个“error adding module to project: null”的错看着一头雾水。我遇到这个问题时反复检查了Maven配置和JDK版本最后定位到是.idea目录下的模块信息缓存冲突导致的。解决步骤关闭IDEA进入项目根目录删除.idea文件夹和所有.iml文件重新用IDEA打开项目右键根pom.xml选择“Add as Maven Project”等待Maven重新导入所有依赖再重新构建项目。如果是直接从Git拉取的若依分支拉下来后如果发现IDEA没有把子模块识别出来可以检查根pom.xml里的 标签看看ruoyi-admin、ruoyi-framework、ruoyi-common、ruoyi-system这几个模块是否都在。漏了哪个模块就手动删掉后再重新添加。6.3 拦截器不生效的问题排查改造完配置类启动项目后如果发现SaToken的拦截器没有生效请求仍然能畅通无阻地访问接口可以按这个顺序排查先确认SaTokenConfigure类是否被Spring扫描到。若依的启动类是放在com.ruoyi包下的如果你新建的配置类放在了com.ruoyi.framework.config里那没问题如果放在了其他包下比如com.example.config启动类的ComponentScan默认扫描不到加上Configuration也没用。再确认是否真的排除了Spring Security的自动配置。若依的Spring Boot启动类上一般不会显式写SpringBootApplication(exclude SecurityAutoConfiguration.class)我们前面只是去掉了pom里的依赖如果你是通过注释依赖的方式操作要注意spring-boot-starter-security是否还被传递引入。可以打开项目的External Libraries里搜索一下spring-security相关的包如果还存在说明某个模块还在间接引用它需要继续排查排除掉。最后看拦截器的排除路径是否与实际请求路径匹配。如果你把登录接口路径改成了/api/login但排除路径写的是/login拦截器会拦截掉登录请求导致用户永远无法登录。6.4 跨域问题若依前后端分离部署时一般会配置跨域过滤器CorsFilter。引入SaToken后接口请求认证逻辑发生在拦截器层。跨域配置本身和认证框架无直接关系但如果前端在请求头里加了Authorization后端没有在CorsFilter的allowedHeaders里加上Authorization浏览器会先发出一个OPTIONS预检请求此时如果后端拦截器把OPTIONS请求当作正常请求拦截了就会导致CORS失败。解决办法有两个方式一在SaToken拦截器里放行所有OPTIONS请求registry.addInterceptor(new SaInterceptor(handle - StpUtil.checkLogin())) .addPathPatterns(/**) .excludePathPatterns(/login, /captchaImage, /error) .excludePathPatterns(HttpMethod.OPTIONS.toString()); // 放行预检请求方式二在CorsFilter的配置中显式声明允许Authorization请求头config.setAllowedHeaders(Arrays.asList(Authorization, Content-Type, X-Token));两种可以同时做稳妥。6.5 清理缓存与重启验证所有代码改完后强烈建议先clean再install因为若依是多模块项目模块间依赖容易有增量编译问题。启动项目后用Postman或者Swagger做一下冒烟测试不登录访问业务接口预期返回401或code401的信息取一个测试账号登录拿到token用token访问业务接口预期正常返回数据把token篡改一个字符再访问接口预期返回未登录异常退出登录后用旧token再访问一次预期token已失效。上面五步走完基本可以确认SaToken已经成功接入了若依的认证链路。如果第4步异常没有触发多半是异常处理器没配置好回去检查GlobalExceptionHandler里是否真的捕获了NotLoginException。7. 常见问题与排查技巧实录7.1 登录成功后获取不到用户信息现象StpUtil.login()执行成功后在同一个请求里调用StpUtil.getLoginId()正常但下一次请求就获取不到用户ID了。排查思路SaToken默认把会话数据放在内存里后端重启、或者未配置Redis持久化时token会全部失效。如果你开启了Redis集成包优先检查Redis连接是否正常Redis宕机时SaToken取不到session就会返回未登录异常。另一种可能是token在前端被清掉了。用浏览器的开发者工具看请求头里是否真的携带了Authorization。前端联调时经常有人把token存到了Cookie里请求头没带后端自然不认账。7.2 token签名机制说明有朋友问过SaToken的token签名问题。SaToken的token默认是服务端生成的随机字符串。它和JWT的“轻签名”思路不同不依赖客户端存储的其他加密信息。所以如果你在项目里非要验证签名通常是指是否开启了SaToken的jwt模式插件。SaToken官方提供sa-token-jwt插件开启后token会带有JWT格式的签名结构适合需要把一些基础信息直接编码到token里的场景。但绝大多数若依项目用不到这个功能默认的uuid风格token足够应付。引入jwt插件反而会增加token长度导致请求头体积变大没必要为了“高级感”增加复杂度。7.3 权限码失效的坑有一种情况容易被忽略若依的权限模型里登录成功后会把用户拥有的菜单权限码列表存起来原来的Security实现是从LoginUser对象里拿权限集合。切换成SaToken后你需要保证登录时把权限码列表同步到SaToken的session里。若依的登录成功后通常会执行permissionService.getMenuPermission(user)来获取权限码列表改造时要记得把这串代码放进登录流程里StpUtil.login(user.getUserId()); // 把权限码列表存起来供接口鉴权使用 StpUtil.getSession().set(permissionList, permissionService.getMenuPermission(user));如果不存SaCheckPermission注解校验就会发现自己找遍session也没拿到权限列表导致所有带权限注解的接口全部401。这个坑非常隐蔽因为登录接口本身是放行的登录时也不会报错等到你测试list接口才发现全被拦截了。7.4 前后端联调时接口全401联调时发现接口全都提示未登录第一时间去后端看日志。如果在日志里发现这样的字样“Sa-Token未能读取到有效token”说明前端根本没有把token带过来。此时打开浏览器的Network面板看请求头字段名是不是和yml里的token-name一致。例如yml里配置的是token-name: Authorization但前端请求头里写的是Config-Token这就是字段名没对齐。统一改成Authorization即可不需要动后端代码。7.5 项目部署后的注意事项若依前后端分离框架部署时后端项目一般打成jar包运行前端用Nginx托管静态文件。切换SaToken后部署上没有特殊要求唯一需要注意的就是Redis必须处于可用状态。因为SaToken默认把会话数据放在内存但如果你启用了Redis集成就是纯粹的“Redis驱动”Redis不可用等同于整个登录系统瘫痪。上线前一定要检查Redis的持久化策略别让服务器一重启缓存全丢了用户全挤在登录页。另外SaToken默认的token名称是satoken若依原前端封装的是Authorization。如果你的部署环境里有网关做统一Token校验保持默认的satoken可能更便于网关识别如果走单纯的前后端分离用Authorization更省事。这块根据自己团队的网关方案来定没有唯一答案。8. 一套自动缓解“误伤”的小技巧改完框架后因为若依里大量Controller方法都用了PreAuthorize注解如果漏替换一两个项目不会启动报错但运行到那个接口时会发现鉴权逻辑怪怪的甚至直接404。为了减少漏网之鱼我在改造时用了一个小技巧全项目搜索PreAuthorize如果结果不为0就说明还有漏网代码修改到完全清零为止。同理搜索SecurityUtils这个类如果业务代码里还有直接调用SecurityUtils的地方要么跟着改掉要么在common模块里保留一个兼容工具类把SecurityUtils.getUserId()的方法体改成public static Long getUserId() { return StpUtil.getLoginIdAsLong(); }这样能极大降低改造过程中的业务代码修改量。毕竟很多Service层都在用SecurityUtils获取当前操作人ID如果一个个改容易改漏。用这个“兼容壳”的做法过渡等项目跑稳定后再渐进式替换。还有一个习惯是改造时不要急着把原来的Security代码注释直接删除最好先用Git创建一个独立分支。这样万一在测试过程中发现SaToken在某些复杂业务场景下满足不了需求随时可以回退。我用Git分支的方式改整个过程比较有安全感。每次提交前跑一遍后端单元测试确认核心接口的鉴权逻辑没有回归。集成完SaToken并跑通单测后我自己的体会是这套方案尤其适合那些“不想被框架束缚”的团队。SaToken把登录会话、权限校验、踢人下线、账号封禁这些功能都整合在一套API里业务代码里写的都是直白的工具方法调用维护成本比Security那套上下文模型低了不少。如果你也在做类似的技术改造建议先按我整理的步骤走通一个最小闭环再根据业务需要去扩展数据权限和自定义拦截规则不要一上来就全量替换。从这次改造的实际效果看启动速度、接口响应时间都有小幅提升代码里关于权限认证的部分也明显更好读了些。
返回列表