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

📅 2026/7/18 9:19:10 👁️ 阅读次数 📝 编程学习
前后端分离架构下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();

这种设计背后是三个关键决策:

  1. Token即身份:默认采用UUID生成Token字符串,完全无状态,天然适配RESTful架构
  2. 双重存储:Token既会返回给客户端,也会在服务端Redis中存储关联数据(如需)
  3. 自动续期:通过tokenTimeoutactivityTimeout配置可以实现灵活的会话保持策略

我特别喜欢它的"无侵入式"设计。不需要像Spring Security那样实现各种接口,只需添加注解就能完成权限控制:

@SaCheckLogin @GetMapping("/user/info") public R<UserInfo> getUserInfo() { // 只有登录用户能访问 }

3. 实战:SpringBoot集成Sa-Token

3.1 基础环境搭建

首先创建Spring Boot项目(我用的2.7.x版本),添加依赖:

<dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version> </dependency> <!-- 如果要用Redis持久化会话 --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis</artifactId> <version>1.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 # 允许从请求体读取token

3.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后,通常有三种处理方式:

  1. 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 })
  1. Cookie存储(需配合withCredentials)
// 登录接口需要设置 axios.defaults.withCredentials = true // 服务端要配置 response.setHeader("Access-Control-Allow-Credentials", "true")
  1. 内存存储(移动端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,仍可能遇到:

  1. 预检请求失败:确保OPTIONS请求不被拦截
// 在拦截器中排除OPTIONS .excludePathPatterns("/**", HttpMethod.OPTIONS.toString())
  1. Cookie未携带:检查前端withCredentials和服务端Allow-Credentials

  2. 自定义Header被过滤:确保exposedHeaders包含所有自定义header

6.2 Token失效异常

可能原因排查链:

  1. 检查Redis是否宕机(如果启用)
  2. 确认token超时配置(timeout/activity-timeout)
  3. 验证token生成策略是否一致
  4. 检查是否有并发登录冲突(is-share配置)

6.3 微服务环境下的特殊处理

在Gateway层需要额外配置:

// 转发时携带Token @Component public class SaTokenFilter implements GlobalFilter { @Override public Mono<Void> 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. 安全加固建议

  1. HTTPS必备:Token在明文传输下极易被窃取
  2. 短期有效期:建议普通业务token不超过24小时
  3. 敏感操作二次验证:关键业务接口应结合短信/邮件验证码
  4. 日志审计:记录所有登录/注销事件
  5. 限流防刷:对登录接口实施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-Token+Redis的方案,日均处理200万次认证请求,平均响应时间保持在15ms以内。特别是在双11大促期间,通过合理的Token超时设置和Redis集群部署,系统平稳度过了流量高峰。