对接SaToken @SaCheckEL 鉴权注解

对接SaToken @SaCheckEL 鉴权注解


前言

原生SpringBoot 2.6.3、Spring 5.3.25(JDK8) 框架系统 对接SaToken 身份认证,使用 SpEL 表达式进行资源请求的鉴权。

一、引入插件和配置SaToken属性配置

        <!-- Sa-Token 权限认证,用于启动SaTokenContext实现 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.40.0</version>
        </dependency>
        <!-- Sa-Token 注解鉴权使用 EL 表达式 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-el</artifactId>
            <version>1.40.0</version>
        </dependency>
sa-token:
  # token前缀
  token-prefix: Bearer
  # token名称 (同时也是cookie名称)
  token-name: Token
  # token有效期,单位s 默认12小时, -1代表永不过期
  timeout: 43200
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  active-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false

二、配置

1.自定义SaTokenInterceptor并注入Bean

SaTokenInterceptor类

@Component
@Slf4j
public class SaTokenInterceptor implements HandlerInterceptor {
	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        log.debug("------------>进入拦截器SaTokenInterceptor:{}", handler.toString());
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "86400");
        response.setHeader("Access-Control-Allow-Headers", "*");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        //当请求路径包含/doc.html(swagger文档),跳过Token验证
        if ( swaggerDocSkipToken(request.getRequestURI())) {
            return true;
        }
        String token = request.getHeader("satoken");
        String baseToken = request.getHeader("token");
        SaSession tokenSession = null;

        if (StrUtil.isEmpty(baseToken) || StrUtil.isEmpty(token)) {
            PrintWriter out = response.getWriter();
            QuantdoResponse resp = new QuantdoResponse();
            log.info("SaTokenInterceptor:Satoken=" + token + ",Token=" + baseToken + "为空!");
            resp.setCode(OrderNotLoginException.TOKEN_TIMEOUT_CODE);
            resp.setMessage(OrderNotLoginException.TOKEN_TIMEOUT_MESSAGE);
            out.append(JSONObject.toJSONString(resp));
            return false;
        }

        if (StrUtil.isNotBlank(token)) {
            tokenSession = StpUtil.getTokenSessionByToken(token);
        }

        if (tokenSession != null ) {
            Long userId = (Long) tokenSession.getLoginId(); 
            return true;
        } else {
                PrintWriter out = response.getWriter();
                QuantdoResponse resp = new QuantdoResponse();
                log.info("SaTokenInterceptor:未获取到登录用户信息!");               resp.setCode(OrderNotLoginException.NOT_VALID_EMP_CODE);
                return false;
            }
        }
        else{
            PrintWriter out = response.getWriter();
            QuantdoResponse resp = new QuantdoResponse();
            log.info("SaTokenInterceptor:未获取到令牌satoken!");
            resp.setCode(OrderNotLoginException.TOKEN_TIMEOUT_CODE);
            out.append(JSONObject.toJSONString(resp));
            return false;
        }
    }

    //Swagger Doc
    public static final boolean swaggerDocSkipToken(String requestURL){
        if (requestURL.contains("/doc.html") || requestURL.contains(".js") || requestURL.contains(".css")
         || requestURL.contains(".ico") || requestURL.contains("/swagger-resources")
         || requestURL.contains("/api-docs")  ) {
            return true;
        }
        else{
            return false;
        }
    }
}

SaTokenConfig 类注入SaTokenInterceptor Bean

package com.isoftstone.order.service.config;
import com.isoftstone.order.service.interceptor.SaTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SaTokenConfig {
    @Bean
    public SaTokenInterceptor getSaTokenInterceptor() {
        return new SaTokenInterceptor();
    }
}

2.WebMvcConfig配置SaTokenInterceptor,拦截所有请求路径

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private SaTokenInterceptor saTokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(saTokenInterceptor)
                .addPathPatterns("/**") // 拦截所有请求路径
                .excludePathPatterns("/login", "/assets/**"); 
                // 排除登录和注册接口
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(module);
        objectMapper.configure(
        DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
        converter.setObjectMapper(objectMapper);
        converters.add(0,converter);
    }
}

三、重载SaToken权限接口和方法注入@SaCheckEL

1.重载SaToken权限接口

/**
 * @Description 工单权限校验,完成 Sa-Token 的自定义权限验证扩展
 * @Date 2025/2/18 16:11
 * @Created by chenjun
 */
@Component
@Slf4j
public class OrderStpInterfaceImpl implements StpInterface {
    @Autowired
    private UserAndRoleService userAndRoleService;
    @Resource
    private StringRedisUtil stringRedisUtil;
    public static final String env = "test";//'local'环境跳过Permission认证
    /**
     * 通过用户id查询用户基础资源表code集合,单个资源表code如:tenant:tenant:user:add
     * @param loginId  账号id:登录defUser实体id
     * @param loginType 账号类型
     * @return "tenant:tenant:user|tenant:tenant:user:query|tenant:tenant:user:add|tenant:tenant:user:delete|tenant:tenant:user:edit";
     *
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        List<String> resourceCodes = userAndRoleService.getMenuResourceCodeByUser(loginId);
        return resourceCodes;
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return null;
    }
}

2.控制层注入@SaCheckEL

用户控制层UserRest 注入@SaCheckEL

@Api(tags = {"用户控制层"})
@RestController
@RequestMapping("/userRest/")
public class UserRest {

    @Autowired
    private UserService userService;

    //用户查询
    @SaCheckEL("stp.checkPermission('tenant:tenant:user:query')")
    @GetMapping("")
    public List<OrderWorkListResp> query(){
        .....
    }

    //用户增加
    @SaCheckEL("stp.checkPermission('tenant:tenant:user:add')")
    @PostMapping("")
    public boolean add(User vo){
        .....
    }

    //用户编辑
    @SaCheckEL("stp.checkPermission('tenant:tenant:user:edit')")
    @PutMapping("")
    public boolean edit(User vo){
        .....
    }
}

四、SaToken @SaCheckEL鉴权的流程

  1. 注入stp.checkPermission(‘角色资源表编码Code,如:mytask:home:myTaskList:query’) 、
 public void checkPermission(String permission) {
        if (!this.hasPermission(this.getLoginId(), permission)) {
            throw (new NotPermissionException(permission, this.loginType)).setCode(11051);
        }
    }

1.1 getLoginId():

public Object getLoginId() {
        if (this.isSwitch()) {
            return this.getSwitchLoginId();
        } else {
            String tokenValue = this.getTokenValue(true);
            if (SaFoxUtil.isEmpty(tokenValue)) {
                throw NotLoginException.newInstance(this.loginType, "-1", "未能读取到有效 token", (String)null).setCode(11011);
            } else {
                String loginId = this.getLoginIdNotHandle(tokenValue);
                if (SaFoxUtil.isEmpty(loginId)) {
                    throw NotLoginException.newInstance(this.loginType, "-2", "token 无效", tokenValue).setCode(11012);
                } else if (loginId.equals("-3")) {
                    throw NotLoginException.newInstance(this.loginType, "-3", "token 已过期", tokenValue).setCode(11013);
                } else if (loginId.equals("-4")) {
                    throw NotLoginException.newInstance(this.loginType, "-4", "token 已被顶下线", tokenValue).setCode(11014);
                } else if (loginId.equals("-5")) {
                    throw NotLoginException.newInstance(this.loginType, "-5", "token 已被踢下线", tokenValue).setCode(11015);
                } else {
                    if (this.isOpenCheckActiveTimeout()) {
                        this.checkActiveTimeout(tokenValue);
                        if (this.getConfigOrGlobal().getAutoRenew()) {
                            this.updateLastActiveToNow(tokenValue);
                        }
                    }
                    return loginId;
                }
            }
        }
    }

未启用切换模式,尝试获取Token值,若Token为空,则抛出异常(代码11011)。
根据Token解析登录ID(getLoginIdNotHandle),若登录ID无效或存在特定状态(如过期、被顶下线等),抛出对应异常(代码11012至11015)。
若登录ID有效且启用了活动超时检查(isOpenCheckActiveTimeout),检查Token的活动超时并可能更新最后活跃时间。
最终返回有效的登录ID。

1.2 hasPermission(Object loginId, String permission)

  public boolean hasPermission(Object loginId, String permission) {
        return this.hasElement(this.getPermissionList(loginId), permission);
    }

调用 getPermissionList(loginId) 获取用户的权限列表。
调用 hasElement(list, permission) 检查权限列表中是否存在指定权限。
返回检查结果(布尔值)。

1.3 下钻this.hasElement
内部匹配核心算法采用【动态规划(DP)解决匹配】:
Sa-Token 内部工具类SaFoxUtil

/**
	 * 字符串模糊匹配
	 * <p>example:
	 * <p> user* user-add   --  true
	 * <p> user* art-add    --  false
	 * @param patt 表达式
	 * @param str 待匹配的字符串
	 * @return 是否可以匹配
	 */
	public static boolean vagueMatch(String patt, String str) {
		// 两者均为 null 时,直接返回 true
		if(patt == null && str == null) {
			return true;
		}
		// 两者其一为 null 时,直接返回 false
		if(patt == null || str == null) {
			return false;
		}
		// 如果表达式不带有*号,则只需简单equals即可 (这样可以使速度提升200倍左右)
		if( ! patt.contains("*")) {
			return patt.equals(str);
		}
		// 深入匹配
		return vagueMatchMethod(patt, str);
	}
private static boolean vagueMatchMethod(String pattern, String str) {
        int m = str.length();
        int n = pattern.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;

        int i;
        for(i = 1; i <= n && pattern.charAt(i - 1) == '*'; ++i) {
            dp[0][i] = true;
        }

        for(i = 1; i <= m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if (pattern.charAt(j - 1) != '*') {
                    if (str.charAt(i - 1) == pattern.charAt(j - 1)) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                } else {
                    dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
                }
            }
        }

        return dp[m][n];
    }

SaFoxUtil 工具类测试SaFoxUtilTest. vagueMatch()

    @Test
	public void vagueMatch() {
		// 不模糊
		Assertions.assertTrue(SaFoxUtil.vagueMatch("hello", "hello"));

		// 正常模糊
		Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello world"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello*"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("hello*", "he"));

		// 带 -
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-add"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-*"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*", "user"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*-add-*", "user-xx-add-1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*-add-*", "user-add-1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*", "usermgt-list"));

		// 带 /
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/add"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/*"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*", "user"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*/add/*", "user/xx/add/1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*/add/*", "user/add/1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*", "usermgt/list"));

		// 带 :
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:add"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:*"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*", "user"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*:add:*", "user:xx:add:1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*:add:*", "user:add:1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*", "usermgt:list"));

		// 带 .
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user."));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.add"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.*"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*", "user"));
		Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*.add.*", "user.xx.add.1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*.add.*", "user.add.1"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*", "usermgt.list"));

		// 极端情况
		Assertions.assertTrue(SaFoxUtil.vagueMatch(null, null));
		Assertions.assertFalse(SaFoxUtil.vagueMatch(null, "hello"));
		Assertions.assertFalse(SaFoxUtil.vagueMatch("hello*", null));
}

1.4 追溯getPermissionList(Object loginId)

public List<String> getPermissionList(Object loginId) {
        return SaManager.getStpInterface().getPermissionList(loginId, this.loginType);
    }

{StpInterface} 接口默认

public interface StpInterface {
    List<String> getPermissionList(Object var1, String var2);
    List<String> getRoleList(Object var1, String var2);
}

{StpInterface} 接口默认的实现类StpInterfaceDefaultImpl ,getPermissionList方法为空,返回
return new ArrayList<>();

/**
 * 对 {@link StpInterface} 接口默认的实现类
 * <p>
 * 如果开发者没有实现 StpInterface 接口,则框架会使用此默认实现类,所有方法都返回空集合,即:用户不具有任何权限和角色。
 * 
 * @author click33
 * @since 1.10.0
 */
public class StpInterfaceDefaultImpl implements StpInterface {

	@Override
	public List<String> getPermissionList(Object loginId, String loginType) {
		return new ArrayList<>();
	}

	@Override
	public List<String> getRoleList(Object loginId, String loginType) {
		return new ArrayList<>();
	}

}

需重新自定义{StpInterface} 接口实现类,比如:

/**
 * @Description 工单权限校验,完成 Sa-Token 的自定义权限验证扩展
 * @Date 2025/2/18 16:11
 * @Created by chenjun
 */
@Component
@Slf4j
public class OrderStpInterfaceImpl implements StpInterface {
    @Autowired
    private UserAndRoleService userAndRoleService;
    @Resource
    private StringRedisUtil stringRedisUtil;
    public static final String env = "test";//'local'环境跳过Permission认证
    /**
     * 通过用户id查询用户基础资源表code集合,单个资源表code如:tenant:tenant:user:add
     * @param loginId  账号id:登录defUser实体id
     * @param loginType 账号类型
     * @return "tenant:tenant:user|tenant:tenant:user:query|tenant:tenant:user:add|tenant:tenant:user:delete|tenant:tenant:user:edit";
     *
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        List<String> resourceCodes = userAndRoleService.getMenuResourceCodeByUser(loginId);
        return resourceCodes;
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return null;
    }
}

1.6 当 this.hasPermission(this.getLoginId(), permission)) 匹配后则通过权限鉴权,否则抛出异常:
throw (new NotPermissionException(permission, this.loginType)).setCode(11051);

五、项目需求来定制化SaToken各种异常

import cn.dev33.satoken.exception.DisableLoginException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.hutool.json.JSONObject;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
@RestControllerAdvice
public class MyExceptionHandle {
    // 全局异常拦截(拦截项目中的NotLoginException异常)
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public String handlerException(Exception e, HttpServletRequest request, HttpServletResponse resp)
            throws Exception {
            // 不同异常返回不同状态码
            String aj = null;
            if (e instanceof NotLoginException) {	// 如果是未登录异常
                NotLoginException ee = (NotLoginException) e;
                aj = ("登录异常:" + ee.getMessage());
            }
            else if(e instanceof NotRoleException) {		// 如果是角色异常
                NotRoleException ee = (NotRoleException) e;
                aj = ("无此角色:" + ee.getRole());
            }
            else if(e instanceof NotPermissionException) {	// 如果是权限异常
                NotPermissionException ee = (NotPermissionException) e;
                aj = ("无此权限:" + ee.getCode());
            }
            else if(e instanceof DisableLoginException) {	// 如果是被封禁异常
                DisableLoginException ee = (DisableLoginException) e;
                aj = ("账号被封禁:" + ee.getDisableTime() + "秒后解封");
            }
            else {	// 普通异常, 输出:500 + 异常信息
                aj = (e.getMessage());
            }
            // 返回给前端
        resp.setContentType(MediaType.APPLICATION_JSON_VALUE);
        resp.setCharacterEncoding("utf-8");
        resp.setStatus(200);
            return aj;
    }
}

NotLoginException 场景值

备注:
1.多个权限注入方法
@SaCheckEL(“stp.checkPermissionOr(‘tenant:user:query’,‘tenant:user:add’)”)
@SaCheckEL(“stp.checkPermissionAnd(‘tenant:user:query’,‘tenant:user:add’)”)
参考 SpEL 表达式注解鉴权

2.IntelliJ IDE安装SpEL Assistant插件,可以追溯stp.checkPermission(…)方法执行的流程走向。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值