摘要
本文档全程采用校园生活类比,将复杂的微服务架构、网关、Redis、Sa-Token等专业概念翻译成小学生也能听懂的大白话,从0开始系统化讲解如何搭建一套完整的微服务全局权限校验系统。文档包含完整的架构设计、核心流程、可直接复制的代码样例,以及生产环境最佳实践(含非最佳实践对比案例),新手也能10分钟上手,快速跑通全流程。
目录
[先把所有专业词「翻译」成校园大白话](#1-先把所有专业词翻译进校园大白话)
[整个系统的完整工作流程](#2-整个系统的完整工作流程)
[核心架构与组件分工](#3-核心架构与组件分工)
[从零开始代码实现](#4-从零开始代码实现)
[核心原理解析](#5-核心原理解析)
[登出请求的特殊处理](#6-登出请求的特殊处理)
[生产环境最佳实践(含非最佳实践对比)](#7-生产环境最佳实践含非最佳实践对比)
[最终总结](#8-最终总结)
1. 先把所有专业词「翻译」成校园大白话
在讲架构之前,我们先把所有组件对应到你熟悉的校园场景,彻底搞懂每个东西是干嘛的、为什么需要它。
1.1 为什么要把权限校验放在「大门」?
如果每个楼都单独设保安:
重复干活,标准不统一,改规则麻烦
坏人可以绕开大门,从楼后面的窗户翻进去
把所有检查放在大门总保安室:
一次检查,全学校通用
规则统一,改一次全学校生效
所有人必须先过大门,绝对安全
1.2 为什么必须要有「总档案室(Redis)」?
如果没有总档案室,每个保安都自己揣一本花名册:
学生被封禁了,大门保安知道了,图书馆保安不知道
多个保安的花名册不一样,全学校乱套了
有了总档案室:
所有保安都来同一个地方查信息,全学校内容100%一致
改信息只需要改一次,所有人马上就能查到最新的
速度极快,1毫秒就能出结果
2. 整个系统的完整工作流程
我们用「学生要去图书馆查书」的例子,把整个请求流程讲清楚。
2.1 校园场景版流程
学生到大门:拿着学生卡(Token),要去图书馆(内容服务)查书
总保安第一步检查:看是不是白名单(比如教务处),不是的话必须查卡
总保安第二步查身份:拿着学生卡去总档案室(Redis)核对:卡是不是真的?有没有过期?
总保安第三步查权限:查权限表:这个学生有没有进图书馆的权限?
总保安指路放行:身份、权限都没问题,给学生指路(路由转发)到图书馆
图书馆细粒度检查:不用再查卡,但可以做更细的检查:能不能借这本专业书?
提供服务:检查通过,提供查书、借书的服务
2.2 对应专业版HTTP请求流程
前端发起请求,携带Token,先到达网关
网关判断:请求路径是否在白名单里?在的话直接放行
不在白名单:调用Sa-Token,校验Token是否有效(从Redis读取会话)
登录校验通过:校验用户是否有访问该接口的粗粒度权限
权限校验通过:网关按照配置的路由规则,把请求转发到对应的后端微服务
后端微服务收到请求:Sa-Token自动从Redis同步会话,执行细粒度权限校验
校验通过:执行业务逻辑,返回结果
3. 核心架构与组件分工
我们用最主流的 Spring Cloud Alibaba + Spring Cloud Gateway + Redis + Sa-Token 架构。
3.1 核心设计原则
网关做全局通用校验:所有请求必须过网关,统一做登录校验
业务服务做细粒度校验:双重保险,网关只保证你能进这个楼
全集群会话共享:所有模块都连接同一个Redis
配置统一管理:通用配置都放在公共模块
4. 从零开始代码实现
前置环境准备
JDK 17+
Maven 3.6+
Redis 6.0+
Spring Cloud Alibaba 2023.x
4.1 步骤1:创建公共依赖模块(common)
4.1.1 pom.xml 引入核心依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>4.1.2 实现Sa-Token的权限查询接口
package com.example.common.satoken;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
List<String> permissionList = new ArrayList<>();
if ("10001".equals(loginId.toString())) {
permissionList.add("*"); // 管理员拥有所有权限
return permissionList;
}
if ("10002".equals(loginId.toString())) {
permissionList.add("content:view");
permissionList.add("user:info");
}
return permissionList;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
List<String> roleList = new ArrayList<>();
if ("10001".equals(loginId.toString())) {
roleList.add("admin");
}
if ("10002".equals(loginId.toString())) {
roleList.add("user");
}
return roleList;
}
}4.1.3 统一异常处理类
package com.example.common.exception;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotLoginException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Result handleNotLoginException(NotLoginException e) {
return Result.error(401, "未登录,请先登录");
}
@ExceptionHandler(NotPermissionException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Result handleNotPermissionException(NotPermissionException e) {
return Result.error(403, "权限不足,无法访问");
}
@Data
public static class Result {
private int code;
private String msg;
private Object data;
public static Result success(Object data) {
Result result = new Result();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
public static Result error(int code, String msg) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
}4.2 步骤2:创建网关模块(gateway)
4.2.1 pom.xml 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>gateway</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>4.2.2 编写网关全局拦截器
package com.example.gateway.filter;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaTokenGatewayFilter {
// 白名单:不用登录就能访问的接口
private static final String[] WHITE_LIST = {
"/user/login",
"/user/register",
"/user/captcha"
};
@Bean
public SaReactorFilter saReactorFilter() {
return new SaReactorFilter()
.addInclude("/**")
.addExclude(WHITE_LIST)
.setAuth(obj -> {
// 1. 先校验是否登录
StpUtil.checkLogin();
// 2. 粗粒度权限校验
SaRouter.match("/content/**", () -> StpUtil.checkPermission("content:*"));
SaRouter.match("/user/**", r -> !r.isMatch(WHITE_LIST), () -> StpUtil.checkPermission("user:*"));
})
.setError(e -> SaResult.error(e.getMessage()));
}
}4.2.3 编写网关配置文件 application.yml
server:
port: 8080
spring:
application:
name: gateway
data:
redis:
host: localhost
port: 6379
password:
database: 0
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- id: content-service
uri: lb://content-service
predicates:
- Path=/content/**
nacos:
discovery:
server-addr: localhost:8848
namespace: public
sa-token:
token-name: satoken
token-timeout: 2592000
is-concurrent: true
is-share: true
token-style: uuid
is-log: false4.3 步骤3:创建业务微服务(user-service)
4.3.1 pom.xml 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>4.3.2 编写登录接口
package com.example.user.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.example.common.exception.GlobalExceptionHandler;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/login")
public GlobalExceptionHandler.Result login(@RequestParam String username, @RequestParam String password) {
if ("admin".equals(username) && "123456".equals(password)) {
StpUtil.login(10001);
return GlobalExceptionHandler.Result.success(StpUtil.getTokenInfo());
}
if ("user".equals(username) && "123456".equals(password)) {
StpUtil.login(10002);
return GlobalExceptionHandler.Result.success(StpUtil.getTokenInfo());
}
return GlobalExceptionHandler.Result.error(400, "账号密码错误");
}
@GetMapping("/info")
public GlobalExceptionHandler.Result getUserInfo() {
Long userId = StpUtil.getLoginIdAsLong();
return GlobalExceptionHandler.Result.success("当前登录用户ID:" + userId);
}
@PostMapping("/logout")
public GlobalExceptionHandler.Result logout() {
StpUtil.logout();
return GlobalExceptionHandler.Result.success("退出登录成功");
}
}4.3.3 配置文件 application.yml
server:
port: 8081
spring:
application:
name: user-service
data:
redis:
host: localhost
port: 6379
password:
database: 0
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: public
sa-token:
token-name: satoken
token-timeout: 2592000
is-concurrent: true
is-share: true
token-style: uuid4.4 步骤4:创建第二个业务微服务(content-service)
4.4.1 编写内容服务接口
package com.example.content.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.example.common.exception.GlobalExceptionHandler;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/content")
public class ContentController {
@GetMapping("/list")
@SaCheckPermission("content:view")
public GlobalExceptionHandler.Result getContentList() {
return GlobalExceptionHandler.Result.success("这是内容列表,只有有content:view权限的人能看到");
}
@PostMapping("/edit")
@SaCheckPermission("content:edit")
public GlobalExceptionHandler.Result editContent() {
return GlobalExceptionHandler.Result.success("内容编辑成功,只有有content:edit权限的人能操作");
}
}5. 核心原理解析
5.1 分布式会话共享原理
用户登录时,Sa-Token生成唯一Token,在Redis里创建对应的Session对象
网关和所有微服务都连接同一个Redis,都能通过Token拿到完全一致的会话信息
修改用户权限、封禁用户,只需要修改Redis里的Session,全集群马上生效
5.2 网关拦截的核心原理
Spring Cloud Gateway的过滤器机制,会把所有请求按顺序经过我们配置的SaReactorFilter:
先判断是否在白名单,在的话直接跳过
不在白名单的话,先校验Token是否有效
登录校验通过后,执行粗粒度的权限校验
所有校验都通过后,转发到对应的后端微服务
5.3 双重权限校验的安全原理
网关层的粗粒度校验:把所有坏人拦在外面
业务服务层的细粒度校验:双重保险,即使网关出现配置漏洞,业务服务也能做二次校验
6. 登出请求的特殊处理
6.1 先纠正2个致命安全误区
绝对不能信任用户请求里自带的id参数:用户可以随意修改,直接使用会导致严重的越权漏洞
登出接口绝对不能放进白名单:注销学生卡必须先出示有效学生卡
6.2 登出请求的完整正确流程
前端发起登出请求,只需要在请求头里带上token
网关校验token有效性,从Redis解析出真实的loginId
下游服务收到请求,自动从token里解析loginId,调用Sa-Token的logout方法
删除Redis里对应的会话,让token彻底失效
6.3 极简零改造方案(99%业务场景首选)
下游user-service的登出接口,一行代码搞定:
@PostMapping("/logout")
public GlobalExceptionHandler.Result logout() {
StpUtil.logout();
return GlobalExceptionHandler.Result.success("退出登录成功");
}7. 生产环境最佳实践(含非最佳实践对比)
7.1 白名单配置
7.2 权限分层
7.3 Redis使用
7.4 安全防护
7.5 高可用
8. 最终总结
Sa-Token配合网关+Redis的微服务权限架构,核心就是**「统一入口、一次校验、全集群共享、双重保险」**:
网关作为统一入口,所有请求必须经过,做全局的登录校验和粗粒度权限校验
Redis作为统一的会话存储,保证全集群所有模块的会话、权限数据100%一致
业务服务做细粒度的权限校验,和网关形成双重保险,绝对安全
Sa-Token屏蔽了所有底层的复杂逻辑,用极简的API实现了全场景的权限管理
一句话记住:网关管大门,Redis管档案,Sa-Token管门禁,业务服务管自己的事,分工明确,安全可靠,开箱即用。