对接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鉴权的流程
- 注入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;
}
}
备注:
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(…)方法执行的流程走向。