前后端分离架构下Sa-Token认证实践与优化

发布时间:2026/7/18 9:01:04
前后端分离架构下Sa-Token认证实践与优化 1. 前后端分离架构下的认证挑战在传统单体应用中服务端通过Session机制维护用户状态是再自然不过的事情——浏览器自动携带Cookie服务端从Session中读取用户信息整个过程对开发者几乎透明。但当我们采用前后端分离架构时这套机制就面临根本性挑战。去年我在重构一个老系统时就踩过这个坑。前端用Vue.js独立部署在8080端口后端Spring Boot服务跑在9090端口。当我尝试用传统Session方案时发现登录状态根本无法保持。原因很简单跨域请求下浏览器默认不会携带凭据即使后端设置了Set-Cookie头前端也无法自动存储和发送这些Cookie。更麻烦的是移动端场景。我们的APP用React Native开发完全不存在浏览器环境传统的Session-Cookie机制彻底失效。此时必须采用无状态的Token方案由客户端主动维护认证凭据。这就是为什么在前后端分离架构中像Sa-Token这样的认证框架变得如此重要。2. Sa-Token的核心设计理念Sa-Token的巧妙之处在于它用极简的API统一了有状态和无状态的认证模式。其核心对象StpUtil只有不到10个常用方法却覆盖了绝大多数认证场景。比如// 登录 StpUtil.login(10001); // 检查是否登录 if(StpUtil.isLogin()) { // 获取当前用户ID Object userId StpUtil.getLoginId(); } // 注销 StpUtil.logout();这种设计背后是三个关键决策Token即身份默认采用UUID生成Token字符串完全无状态天然适配RESTful架构双重存储Token既会返回给客户端也会在服务端Redis中存储关联数据如需自动续期通过tokenTimeout和activityTimeout配置可以实现灵活的会话保持策略我特别喜欢它的无侵入式设计。不需要像Spring Security那样实现各种接口只需添加注解就能完成权限控制SaCheckLogin GetMapping(/user/info) public RUserInfo getUserInfo() { // 只有登录用户能访问 }3. 实战SpringBoot集成Sa-Token3.1 基础环境搭建首先创建Spring Boot项目我用的2.7.x版本添加依赖dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.34.0/version /dependency !-- 如果要用Redis持久化会话 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-dao-redis/artifactId version1.34.0/version /dependency配置文件application.yml关键项sa-token: token-name: satoken # 前端存储的token键名 timeout: 86400 # token有效期(秒) activity-timeout: -1 # 无操作自动续期(-1不续期) token-style: uuid # token生成策略 is-share: true # 同一账号多端登录 is-read-body: true # 允许从请求体读取token3.2 跨域与认证配置创建配置类解决前后端分离的核心痛点Configuration public class SaTokenConfig implements WebMvcConfigurer { // 解决跨域 Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(*) .allowedHeaders(*) .exposedHeaders(satoken) // 关键 .allowCredentials(true); } // 注册拦截器 Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/user/doLogin); } }特别注意exposedHeaders(satoken)这行配置。由于浏览器安全限制自定义header需要显式暴露才能被前端读取。3.3 登录接口实现典型的登录控制器示例RestController RequestMapping(/user) public class UserController { PostMapping(/doLogin) public R doLogin(RequestBody LoginDto dto) { // 模拟数据库校验 if(admin.equals(dto.getUsername()) 123456.equals(dto.getPassword())) { // 关键登录操作 StpUtil.login(10001); return R.ok() .put(token, StpUtil.getTokenValue()) .put(userInfo, getUserInfo(10001)); } return R.error(账号或密码错误); } // 获取当前用户信息 SaCheckLogin GetMapping(/info) public R info() { return R.ok().data(getUserInfo(StpUtil.getLoginIdAsLong())); } }4. 前端集成方案4.1 Token的存储与传输前端拿到登录接口返回的token后通常有三种处理方式LocalStorage存储推荐SPA使用// 登录成功后 localStorage.setItem(satoken, res.data.token) // 请求拦截器 axios.interceptors.request.use(config { const token localStorage.getItem(satoken) if(token) { config.headers[satoken] token } return config })Cookie存储需配合withCredentials// 登录接口需要设置 axios.defaults.withCredentials true // 服务端要配置 response.setHeader(Access-Control-Allow-Credentials, true)内存存储移动端APP常用4.2 401拦截与跳转前端需要统一处理认证失败的情况axios.interceptors.response.use( response response, error { if(error.response.status 401) { router.push(/login) } return Promise.reject(error) } )5. 高级配置与优化5.1 Redis集成会话持久化对于生产环境建议启用Redis存储spring: redis: host: 127.0.0.1 port: 6379 # password: 你的密码 sa-token: is-share: false # 禁止多端登录 is-concurrent: true # 允许并发登录 is-read-body: false # 关闭body读取 is-read-header: true # 开启header读取 is-read-cookie: false # 关闭cookie读取5.2 踢人下线与账号封禁Sa-Token提供了精细的会话控制// 强制注销指定用户 StpUtil.logout(10001); // 封禁账号(单位秒) StpUtil.disable(10001, 3600); // 检查是否被封禁 if(StpUtil.isDisable(10001)) { long disableTime StpUtil.getDisableTime(10001); }5.3 自定义Token策略默认UUID可能不符合某些业务需求可以自定义生成器Configuration public class SaTokenCustomConfig { Bean public StpLogic getStpLogic() { return new StpLogic(user) { Override public String createTokenValue(Object loginId, String device) { return custom_ loginId _ System.currentTimeMillis(); } }; } }6. 常见问题排查6.1 跨域问题深度解决即使配置了CORS仍可能遇到预检请求失败确保OPTIONS请求不被拦截// 在拦截器中排除OPTIONS .excludePathPatterns(/**, HttpMethod.OPTIONS.toString())Cookie未携带检查前端withCredentials和服务端Allow-Credentials自定义Header被过滤确保exposedHeaders包含所有自定义header6.2 Token失效异常可能原因排查链检查Redis是否宕机如果启用确认token超时配置timeout/activity-timeout验证token生成策略是否一致检查是否有并发登录冲突is-share配置6.3 微服务环境下的特殊处理在Gateway层需要额外配置// 转发时携带Token Component public class SaTokenFilter implements GlobalFilter { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest() .mutate() .header(satoken, exchange.getRequest().getHeaders().getFirst(satoken)) .build(); return chain.filter(exchange.mutate().request(request).build()); } }7. 安全加固建议HTTPS必备Token在明文传输下极易被窃取短期有效期建议普通业务token不超过24小时敏感操作二次验证关键业务接口应结合短信/邮件验证码日志审计记录所有登录/注销事件限流防刷对登录接口实施IP限流如Guava RateLimiter// 登录限流示例 private final RateLimiter loginLimiter RateLimiter.create(5.0); // 每秒5次 PostMapping(/doLogin) public R doLogin(RequestBody LoginDto dto) { if(!loginLimiter.tryAcquire()) { throw new RuntimeException(操作过于频繁); } // ...原有逻辑 }在最近的一个电商项目中我们采用Sa-TokenRedis的方案日均处理200万次认证请求平均响应时间保持在15ms以内。特别是在双11大促期间通过合理的Token超时设置和Redis集群部署系统平稳度过了流量高峰。