基于 Spring Cloud Gateway + Sa-Token 的架构为例,Token 异常的执行链路

0 阅读3分钟

当 Token 失效 / 错误时,程序会在「鉴权逻辑执行之前」就触发异常并被全局异常处理器捕获,直接返回错误响应,不会走到后续的权限校验(如 getPermissionList/getRoleList)步骤

基于 Spring Cloud Gateway + Sa-Token 的架构为例,Token 异常的执行链路如下: image.png

核心关键点:

  1. Token 异常:在 StpUtil.checkLogin()/checkRole()/checkPermission() 执行时触发,直接抛异常,不会调用 getRoleList/getPermissionList
  2. Token 有效但权限不足:会调用 getRoleList/getPermissionList 从 Redis 读权限 / 角色,再判断是否匹配,不匹配则抛异常;
@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private RedisUtil redisUtil;

    private final String authPermissionPrefix = "auth.permission";

    private final String authRolePrefix = "auth.role";


    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return getAuth(loginId.toString(),authPermissionPrefix);
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return getAuth(loginId.toString(),authRolePrefix);
    }

    private List<String> getAuth(String loginId,String prefix){
        String authKey = redisUtil.buildKey(prefix,loginId);
        String authValue = redisUtil.get(authKey);
        if (StringUtils.isBlank(authValue)){
            return Collections.emptyList();
        }
        List<String> list = new Gson().fromJson(authValue, List.class);
        return list;
    }

}

3. 异常最终兜底:所有 Sa-Token 异常都会被 GatewayExceptionHandler 捕获,返回 JSON 响应。

import cn.dev33.satoken.exception.SaTokenException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lee.club.gateway.entity.Result;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {
        ServerHttpRequest request = serverWebExchange.getRequest();
        ServerHttpResponse response = serverWebExchange.getResponse();
        Integer code = 200;
        String message = "";
        if (throwable instanceof SaTokenException){
            code = 401;
            message = "用户无权限";
        }else {
            code = 500;
            message = "系统繁忙";
        }
        Result result = Result.fail(code, message);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return response.writeWith(Mono.fromSupplier(()->{
            DataBufferFactory dataBufferFactory = response.bufferFactory();
            byte[] bytes = null;
            try {
                bytes = objectMapper.writeValueAsBytes(result);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            return dataBufferFactory.wrap(bytes);
        }));
    }
}

逐节点拆解执行逻辑

1. 入口:SaReactorFilter 拦截请求

  • 触发条件:所有前端请求都会被 SaReactorFilter 拦截(因为配置了 addInclude("/**"));
  • 核心动作:进入 setAuth 方法,执行你定义的所有 SaRouter 鉴权规则。
/**
 * 权限认证的配置器
 */
@Configuration
public class SaTokenConfigure {

    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
            // 拦截地址
            .addInclude("/**")
            // 鉴权方法:每次访问进入
            .setAuth(obj -> {
                System.out.println("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath());
                // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
                SaRouter.match("/auth/**", "/auth/user/doLogin", r -> StpUtil.checkRole("user1"));
                SaRouter.match("/oss/**",  r -> StpUtil.checkLogin());
                SaRouter.match("/subject/subject/add",  r -> StpUtil.checkPermission("subject:add"));
                SaRouter.match("/subject/**",  r -> StpUtil.checkLogin());
            });
    }
}

2. 前置:Token 有效性校验

  • 触发时机:执行任何 StpUtil.checkRole()/checkPermission()/checkLogin() 时,Sa-Token 会先自动校验 Token

  • 结果分支

    • Token 失效 / 错误:直接抛 NotLoginException,跳过后续所有鉴权逻辑;
    • Token 有效:继续执行路由匹配和权限校验。

3. 核心:路由规则匹配

SaRouter 会按你配置的规则,判断请求路径是否匹配:

匹配路径执行动作核心目的
/auth/**StpUtil.checkRole("user1")校验用户是否有 user1 角色
/oss/**StpUtil.checkLogin()仅校验 Token 有效,不校验角色 / 权限
/subject/subject/addStpUtil.checkPermission("subject:add")校验用户是否有 subject:add 权限
/subject/**(非 add)StpUtil.checkLogin()仅校验 Token 有效
其他路径直接放行无鉴权要求

4. 数据支撑:从 Redis 读取角色 / 权限

  • 触发条件:Token 有效 + 执行 checkRole()/checkPermission()
  • 核心动作:Sa-Token 自动调用你实现的 StpInterfaceImpl.getRoleList()/getPermissionList(),从 Redis 读取该用户的角色 / 权限列表;
  • 兜底逻辑:Redis 中无数据 → 返回空列表 → 权限校验失败。

5. 决策:角色 / 权限是否匹配

  • 角色校验:判断 Redis 读取的角色列表是否包含 "user1"

  • 权限校验:判断 Redis 读取的权限列表是否包含 "subject:add"

  • 结果分支

    • 不匹配:抛 NotRoleException/NotPermissionException
    • 匹配:放行请求到后端服务。

6. 兜底:异常统一处理

所有 Sa-Token 异常(NotLoginException/NotRoleException/NotPermissionException)都会被 GatewayExceptionHandler 捕获:

  • NotLoginException → 返回 401 + "Token 失效或未登录";
  • NotRoleException/NotPermissionException → 返回 403 + "用户无权限";
  • 其他异常 → 返回 500 + "系统繁忙"。