1. 为什么选择Sa-Token进行网关鉴权
在微服务架构中,网关作为流量的统一入口,承担着重要的安全防护职责。传统的Spring Security虽然功能强大,但对于中小型项目来说配置复杂度较高。而Sa-Token作为一款轻量级Java权限认证框架,具有以下显著优势:
- 注解式鉴权:只需在Controller方法添加
@SaCheckLogin等注解即可完成权限控制,相比Spring Security的繁琐配置更符合"约定优于配置"原则 - 无缝集成Redis:默认支持分布式会话存储,天然适合微服务场景
- 开箱即用的功能:包含踢人下线、账号封禁、多端登录等常见业务场景解决方案
- 低学习成本:官方文档清晰,API设计直观,1小时内即可上手核心功能
提示:对于已经使用Spring Security的项目,Sa-Token可以作为补充方案专门处理网关层的统一鉴权,两者并不冲突。
2. 环境准备与基础搭建
2.1 项目结构规划
建议采用如下微服务模块划分:
springcloud-alibaba ├── shop-gateway # 网关服务(端口7000) ├── shop-auth # 认证服务(端口8101) ├── shop-product # 商品服务(端口8080) └── shop-common # 公共依赖模块2.2 关键依赖配置
在网关和认证服务的pom.xml中添加核心依赖:
<!-- Sa-Token核心包 --> <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> <!-- SpringCloud Gateway --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>3. 认证中心实现细节
3.1 数据库设计
创建基础用户表结构:
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `status` tinyint DEFAULT '1' COMMENT '1-正常 0-禁用', PRIMARY KEY (`id`), UNIQUE KEY `idx_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.2 核心认证逻辑
在auth服务中创建AuthController:
@RestController @RequestMapping("/user") public class AuthController { @PostMapping("/doLogin") public JsonResult doLogin(String username, String password) { // 1. 模拟数据库查询 if(!"zhang".equals(username) || !"123456".equals(password)) { return JsonResult.error("账号或密码错误"); } // 2. 会话登录 StpUtil.login(10001); // 3. 返回token return JsonResult.success("登录成功", StpUtil.getTokenValue()); } }3.3 Redis配置优化
建议在application.yml中调整连接池参数:
spring: redis: lettuce: pool: max-active: 200 # 根据QPS调整 max-idle: 50 min-idle: 10 max-wait: 1000ms4. 网关层鉴权实现
4.1 全局过滤器配置
创建Gateway全局过滤器处理鉴权:
@Component public class SaTokenFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1. 获取Request对象 ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); // 2. 放行登录接口和白名单 if(path.contains("/auth/user/doLogin") || path.contains("/public/")) { return chain.filter(exchange); } // 3. 校验登录状态 try { String token = request.getHeaders().getFirst("Authorization"); if(!StpUtil.checkLogin(token)) { return unauthorizedResponse(exchange); } } catch (Exception e) { return unauthorizedResponse(exchange); } return chain.filter(exchange); } private Mono<Void> unauthorizedResponse(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json"); return response.writeWith(Mono.just(response.bufferFactory() .wrap("{\"code\":401,\"msg\":\"未登录或token已过期\"}".getBytes()))); } @Override public int getOrder() { return -100; } }4.2 路由配置示例
gateway服务的application.yml配置:
spring: cloud: gateway: routes: - id: auth-service uri: lb://shop-auth predicates: - Path=/auth/** filters: - StripPrefix=1 - id: product-service uri: lb://shop-product predicates: - Path=/product/** filters: - StripPrefix=1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 205. 业务服务集成方案
5.1 商品服务鉴权示例
在product服务中添加权限控制:
@RestController @RequestMapping("/product") public class ProductController { @SaCheckLogin @GetMapping("/{id}") public Product getById(@PathVariable Long id) { // 获取当前登录用户ID long userId = StpUtil.getLoginIdAsLong(); log.info("当前用户ID:{}", userId); return productService.getById(id); } @SaCheckRole("admin") @PostMapping public void addProduct(@RequestBody Product product) { productService.save(product); } }5.2 跨服务用户信息传递
建议采用以下方式传递用户信息:
- 方案一:JWT方式
// 网关过滤器添加用户信息 exchange.getRequest().mutate() .header("user-id", StpUtil.getLoginIdAsString()) .build();- 方案二:Feign拦截器
public class FeignAuthInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { if(StpUtil.isLogin()) { template.header("user-id", StpUtil.getLoginIdAsString()); } } }6. 常见问题排查指南
6.1 502 Bad Gateway错误分析
当出现502错误时,按以下步骤排查:
检查Nacos服务注册状态
curl http://localhost:8848/nacos/v1/ns/instance/list?serviceName=shop-auth验证Redis连接是否正常
@SpringBootTest class RedisTest { @Autowired private RedisTemplate redisTemplate; @Test void testConnection() { redisTemplate.opsForValue().set("test", "value"); assert "value".equals(redisTemplate.opsForValue().get("test")); } }检查Gateway路由配置是否正确
logging: level: org.springframework.cloud.gateway: DEBUG
6.2 鉴权失效场景处理
场景一:Token无法识别
- 检查Redis中是否存在对应token(key格式:
satoken:login:token:[token]) - 确认服务间时间同步(时区差异会导致token失效)
场景二:注解不生效
- 确保Controller方法被Spring管理(是否加了@RestController)
- 检查Sa-Token配置类:
@Configuration public class SaTokenConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } }
7. 性能优化建议
7.1 Redis缓存策略
使用Hash结构存储会话信息:
// 在SaToken配置中启用hash存储 @Bean public SaTokenDao saTokenDao() { return new SaTokenDaoRedisHash(); }设置合理的TTL:
sa-token: timeout: 86400 # token有效期(秒) activity-timeout: -1 # 无操作续期时间(-1不续期)
7.2 网关层优化
启用响应缓存:
@Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES)); return cacheManager; }限流配置:
spring: cloud: gateway: default-filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200
8. 生产环境注意事项
Token安全增强:
- 启用HTTPS传输
- 设置Token前缀防止猜测攻击:
sa-token: token-prefix: "Bearer"
监控指标收集:
@RestController @RequestMapping("/monitor") public class AuthMonitorController { @GetMapping("/stats") public Map<String, Object> getStats() { Map<String, Object> map = new HashMap<>(); map.put("loginCount", StpUtil.getLoginCount()); map.put("tokenSessionSize", SaManager.getSaTokenDao().searchData( "satoken:login:token:*", 0, 10).size()); return map; } }灾备方案:
- 配置Redis哨兵或集群模式
- 实现本地缓存降级策略:
public class HybridTokenDao implements SaTokenDao { @Override public String get(String key) { // 先查Redis,失败查本地缓存 } }
在实际项目中,我们通过这套方案实现了日均100万+请求的鉴权处理,平均延迟控制在15ms以内。关键点在于合理设置Redis参数和做好网关层的限流防护。对于更复杂的权限场景,可以结合Sa-Token的路由拦截功能实现动态权限控制。