Springboot 使用 SaToken 进行登录认证权限管理以及路由规则接口拦截
Posted 繁华尽头满是殇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Springboot 使用 SaToken 进行登录认证权限管理以及路由规则接口拦截相关的知识,希望对你有一定的参考价值。
Springboot 使用 SaToken 进行登录认证、权限管理以及路由规则接口拦截
前言
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
还有踢人下线、账号封禁、路由拦截规则、微服务网关鉴权、密码加密等丰富功能
它不比 Shiro 和 SpringSecurity 的功能少,而且配置使用更加简单
一、引入和配置
先给你们看一下 Demo 文件结构
1.引入依赖
如果不需要将 token 信息存入 redis,只需要引入下面这一个依赖
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.31.0</version>
</dependency>
如果需要将 token 存入 redis,则还需要引入下面的依赖(一般搭建单点登录服务器才需要使用 redis)
使用redis ,无需任何其他配置,只需要多引入下面几个依赖,然后下面的 yml 加一些配置,satoken 就可以自动存储到 redis,非常方便
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式)-->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.31.0</version>
</dependency>
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>1.31.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2、配置yml
如下代码,如果不需要使用 redis ,则删除
alone-redis
和spring redis
配置,否则连接不到 redis 会报错
如果使用了 redis,我下面的配置是业务和鉴权分离的方式,也就是说,token 存储在
alone-redis
里面配置的数据库,我这里配置的是0
号数据库,它和spring reids
配置的数据库不冲突
server:
port: 8081
# Sa-Token配置
sa-token:
# token前缀
# Token前缀 与 Token值 之间必须有一个空格。
# 一旦配置了 Token前缀,则前端提交 Token 时,必须带有前缀,否则会导致框架无法读取 Token。
# 由于Cookie中无法存储空格字符,也就意味配置 Token 前缀后,Cookie 鉴权方式将会失效,此时只能将 Token 提交到header里进行传输
# token-prefix: Bearer
# token 名称 (同时也是cookie名称)
token-name: satoken
# token 有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token 临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: true
# 配置 Sa-Token 单独使用的 Redis 连接
alone-redis:
# Redis数据库索引(默认为0)
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s
spring:
# 配置业务使用的 Redis 连接
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 10s
3、配置全局异常处理
这一步可以不配置,配置的作用是,在鉴权失败的时候,不会报错,而是返回给前端鉴权失败的原因,方便我们开发调试
下面的异常会在鉴权失败的时候自动返回到前端,无需我们手动抛出和返回
package pers.xuyijie.satokendemo.exception;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author 徐一杰
* @date 2022/9/23 16:45
* @description SaToken全局异常拦截
*/
@RestControllerAdvice
public class GlobalExceptionHandler
/**
* 全局异常拦截,鉴权失败不会报错,会返回给前端报错原因
* @param e
* @return
*/
@ExceptionHandler
public SaResult handlerException(Exception e)
e.printStackTrace();
return SaResult.error(e.getMessage());
4、模拟用户角色和权限
这里我们给用户分配一下我们模拟的角色和权限,正常你们要从数据库读取用户的角色和拥有的权限
这里实现了 StpInterface 下面的方法,下面的方法会在接口鉴权之前自动调用,判断角色和权限,无需我们手动调用
package pers.xuyijie.satokendemo.permission;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author 徐一杰
* @date 2022/9/23 16:46
* @description 获取当前账号的权限和角色列表,这个类下面的方法会在接口鉴权之前自动调用
*/
@Component
public class UserPermission implements StpInterface
/**
* 返回一个账号所拥有的权限码集合
* 即你在调用 StpUtil.login(id) 时写入的标识值。
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType)
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<>();
list.add("1");
list.add("user-add");
list.add("user-delete");
list.add("user-update");
list.add("user-get");
list.add("article-get");
System.out.println("用户权限列表:" + list);
return list;
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType)
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<>();
list.add("user");
list.add("admin");
list.add("super-admin");
System.out.println("用户角色列表:" + list);
return list;
5、配置拦截器
如果在高版本 SpringBoot (≥2.6.x) 下注册拦截器失效,则需要添加 @EnableWebMvc 注解才可以使用
下面我们配置的规则叫作
路由拦截规则
,/user/**
意思就是接口地址为/user
开头的所有接口,也就是说,下面的我们UserController
里面的所有接口都在拦截范围内
package pers.xuyijie.satokendemo.config;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 徐一杰
* @date 2022/9/23 16:49
* @description
*/
@SpringBootConfiguration
@EnableWebMvc
public class SaTokenConfigure implements WebMvcConfigurer
/**
* 注册 Sa-Token 拦截器,打开注解式鉴权功能
* 如果在高版本 SpringBoot (≥2.6.x) 下注册拦截器失效,则需要额外添加 @EnableWebMvc 注解才可以使用
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry)
// 注册路由拦截器,自定义认证规则
registry.addInterceptor(new SaInterceptor(handler ->
// 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
// 角色认证 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
// 权限认证 -- 不同模块认证不同权限
SaRouter.match("/user/**", r -> StpUtil.checkRole("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// 甚至你可以随意的写一个打印语句
SaRouter.match("/**", r -> System.out.println("--------权限认证成功-------"));
).isAnnotation(true))
//拦截所有接口
.addPathPatterns("/**")
//不拦截/user/doLogin登录接口
.excludePathPatterns("/user/doLogin");
6、controller里调用satoken的方法
方法上面的注解是使用权限认证和拦截器的时候用的,下面我会讲到
我在下面的
UserController
演示了登录
、注销
、检查是否登录
、查看用户token
、获取token有效期
、对称加密
、非对称加密
方法,具体的方法每一行代码的作用,都在注视中写出来了,等一下我们测试每一个方法,为大家展示运行结果并解析代码
package pers.xuyijie.satokendemo.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.basic.SaBasicUtil;
import cn.dev33.satoken.secure.SaBase64Util;
import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
/**
* @author 徐一杰
* @date 2022/9/23 15:52
* @description
*/
@RestController
@RequestMapping("/user")
public class UserController
private static final String USERNAME = "xyj";
private static final String PASSWORD = "123456";
/**
* 测试登录
* @param username
* @param password
* @return
*/
@RequestMapping("/doLogin")
public SaResult doLogin(String username, String password)
//这个方法会强制在浏览器弹出一个认证框
SaBasicUtil.check("sa:123456");
if(username.equals(USERNAME) && password.equals(PASSWORD))
//这个是登录用户的主键,业务中你要从数据库中读取
StpUtil.login(1);
//获取登录生成的token
tokenInfo = StpUtil.getTokenInfo();
System.out.println(tokenInfo);
return SaResult.ok("登录成功,会话ID为 " + StpUtil.getLoginId() + " ,Token为:" + StpUtil.getTokenValue());
return SaResult.error("登录失败");
/**
* 查询登录状态
* @return
*/
@RequestMapping("/signOut")
public SaResult signOut()
String loginId = null;
if (StpUtil.isLogin())
loginId = (String) StpUtil.getLoginId();
StpUtil.logout();
return SaResult.ok("会话ID为 " + loginId + " 的用户注销登录成功");
/**
* 查询登录状态
* @return
*/
@RequestMapping("/isLogin")
public SaResult isLogin()
if (StpUtil.isLogin())
return SaResult.ok("会话是否登录:" + StpUtil.isLogin() + " ,会话ID为 " + StpUtil.getLoginId());
return SaResult.ok("会话是否登录:" + StpUtil.isLogin());
/**
* 根据Token值获取对应的账号id,如果未登录,则返回 null
* @param tokenValue
* @return
*/
@RequestMapping("/getUserByToken/tokenValue")
public SaResult getUserByToken(@PathVariable String tokenValue)
return SaResult.ok((String) StpUtil.getLoginIdByToken(tokenValue));
/**
* 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
* @return
*/
@RequestMapping("/getTokenTimeout")
public SaResult getTokenTimeout()
return SaResult.ok(String.valueOf(StpUtil.getTokenTimeout()));
@SaIgnore
@RequestMapping("/encodePassword")
public void encodePassword() throws Exception
/**
* md5加盐加密: md5(md5(str) + md5(salt))
*/
String md5 = SaSecureUtil.md5("123456");
String md5BySalt = SaSecureUtil.md5BySalt("123456", "salt");
System.out.println("MD5加密:" + md5);
System.out.println("MD5加盐加密:" + md5BySalt);
/**
* AES对称加密
*/
// 定义秘钥和明文
String key = "123456";
String text = "这是一个明文用于测试AES对称加密";
// 加密
String ciphertext = SaSecureUtil.aesEncrypt(key, text);
System.out.println("AES加密后:" + ciphertext);
// 解密
String text2 = SaSecureUtil.aesDecrypt(key, ciphertext);
System.out.println("AES解密后:" + text2);
/**
* RSA非对称加密
*/
// 定义私钥和公钥
HashMap<String, String> keyMap = SaSecureUtil.rsaGenerateKeyPair();
String privateKey = keyMap.get("private");
String publicKey = keyMap.get("public");
// 文本
String text1 = "这是一个明文用于测试RSA非对称加密";
// 使用公钥加密
String ciphertext1 = SaSecureUtil.rsaEncryptByPublic(publicKey, text1);
System.out.println("公钥加密后:" + ciphertext1);
// 使用私钥解密
String text3 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext1);
System.out.println("私钥解密后:" + text3);
/**
* Base64
*/
// 文本
String text4 = "这是一个明文用于测试Base64";
// 使用Base64编码
String base64Text = SaBase64Util.encodeSaToken使用SpringBoot整合SaToken关于数据权限
目录
-
点击查看上一篇:
【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权
前言
数据范围:1 所有数据、2 所在部门及子部门数据、3 所在部门数据、4 仅本人数据、5 自定义数据
关于数据权限,一直没有一个很好的通用的解决方案。
方式一:
之前有试过参考若依的那个用自定义注解+aop去实现数据权限,然后觉得通用性不高,无法适用于我自己的项目的业务场景,而且我用的mybatisplus,不是联表的话是不会去xml自己写sql语句的,而是直接用QueryWrapper,用这种实现方式是没办法把拼接好的sql放到QueryWrapper中做条件的。
方式二:
然后我又找了种方法,也是用注解和aop,但它是需要解析sql,把数据范围条件拼接到sql中,变成了一条新的sql语句去执行的。尝试了下这种方法,确实可以,不管是在xml自己写的SQL也好,还是用户mybatisplus自带的查询也好,甚至有分页都不用担心。但是面对复杂SQL比如子查询、嵌套查询等,要想把条件给拼接到正确的位置上去,太复杂了,那些group by、where、order by这些关键词位置判断就够呛,要是有子查询、嵌套查询,都有where,那过滤条件放到哪个where里面?这也是比较头疼的事。所以我感觉这种方法也不太行,代码太啰嗦。
方式三:
后面做项目的时候,我上面两种方法都没采用,我的想法是两种方法最后都是在原有的SQL上拼接条件,那为什么不把权限条件像其他条件一样,直接写在SQL里面,而是用注解、拦截器去拼接?就比如查询用户列表,a用户的数据权限是只能查看本部门的数据,所以权限条件是 dept_id=a用户的部门 ,然后列表有个搜索框,搜索姓名的,那么在mybatis中SQL就是
select *
from sys_user
<where>
<if test="name != null"> and name=#name </if>
<if test="dataScope != null"> and $dataScope </if>
</where>
如果不是自己写SQL,而是用QueryWrapper的话,那也简单
public PageInfo<SysUser> page(SysQuery query)
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
if (StrUtil.isNotBlank(query.getName()))
queryWrapper.eq("name",query.getName());
String apply = queryApplyScope("dept",null,1); // 获取权限条件
if (StrUtil.isNotBlank(apply))
queryWrapper.apply(apply);
return new PageInfo<>(baseMapper.selectList(queryWrapper));
像这样把权限条件和其他筛选条件一样,直接用不是挺好的嘛?还是说为了做到低侵入、无侵入?不管哪种方法,得看是否适用当前业务场景,不过如果没要求无侵入的话,直接用肯定是比注解+拦截器拼接的方式更灵活的。下面详细说下第二种实现和第三种实现,可以参考下。
方式二实现、自定义注解+拦截器拦截SQL,实现改变原SQL
参考文章:springboot自定义注解+mybatis拦截器实现数据权限
1、自定义注解 DataScope
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope
/** 拼接的条件是取 部门id 还是 部门name(1 id 2 name) */
int type() default 1;
/** 查询的字段名 */
String fieldName() default "";
/** 查询本人数据时使用的的字段名 */
String createBy() default "";
2、数据过滤处理 DataScopeAspect
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.entity.sys.SysUser;
import com.service.sys.SysUserService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* 数据过滤处理
*/
@Aspect
@Component
public class DataScopeAspect
/** 用于存储过滤条件的SQL */
public static final ThreadLocal DATA_AUTH_THREAD_LOCAL = new ThreadLocal();
@Resource
private SysUserService sysUserService;
// 配置织入点
@Pointcut("@annotation(DataScope)")
public void dataScopePointCut()
@Before("dataScopePointCut()")
public void doBefore(JoinPoint point)
DATA_AUTH_THREAD_LOCAL.remove();
handleDataAuthScope(point);
@After("@annotation(DataScope)")
public void doAfter()
//清空数据权限拼接SQL
DATA_AUTH_THREAD_LOCAL.remove();
/**
* 处理数据权限
* @param joinPoint 切面
*/
protected void handleDataAuthScope(final JoinPoint joinPoint)
// 获取当前的用户
SysUser user = (SysUser) StpUtil.getSession().get("user");
if (null == user)
return;
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
DataScope dataScope = method.getAnnotation(DataScope.class);
// 获取当前用户要拼接的数据权限条件
String str = sysUserService.queryApplyScope(dataScope.fieldName(), dataScope.createBy(), dataScope.type());
if (StrUtil.isNotBlank(str))
//设置数据权限拼接sql
DATA_AUTH_THREAD_LOCAL.set(str);
3、拦截SQL语句,并将过滤条件拼接到原SQL中
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
/**
* 拦截SQL语句,根据处理好的数据权限条件拼接在原SQL后,组成新的SQL语句
*/
@Component
@Intercepts(@Signature(type = Executor.class, method = "query", args = MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class))
public class DataScopeInterceptor implements Interceptor
/** 分组 */
private static final String GROUP_BY = "GROUP BY";
/** 排序 */
private static final String ORDER_BY = "ORDER BY";
/** 分页 */
private static final String LIMIT = "LIMIT";
/** where */
private static final String WHERE = "WHERE";
/** where条件 */
private static final String WHERE_CONDITION = " WHERE 1=1 ";
/**
* 拦截sql
* @param invocation
*/
@Override
public Object intercept(Invocation invocation) throws Throwable
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if (SqlCommandType.SELECT == sqlCommandType)
// 获取过滤条件
Object dataAuthSql = DataScopeAspect.DATA_AUTH_THREAD_LOCAL.get();
//如果添加数据权限
if (null != dataAuthSql)
BoundSql boundSql = (BoundSql) invocation.getArgs()[5];
StringBuilder newSqlBuilder = new StringBuilder();
// 获取到原始sql语句
String mSql = boundSql.getSql();
System.out.println("原始SQL:\\t"+mSql);
mSql = addWhere(mSql);
//重写sql语句 前面拼接数据权限语句
if (mSql.indexOf(GROUP_BY) > 0)
newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(GROUP_BY)))
.append(" and ")
.append(dataAuthSql.toString())
.append(" ")
.append(mSql.substring(mSql.lastIndexOf(GROUP_BY), mSql.length()));
else if (mSql.indexOf(ORDER_BY) > 0)
newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(ORDER_BY)))
.append(" and ")
.append(dataAuthSql.toString())
.append(" ")
.append(mSql.substring(mSql.lastIndexOf(ORDER_BY), mSql.length()));
else if (mSql.indexOf(LIMIT) > 0)
newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(LIMIT)))
.append(" and ")
.append(dataAuthSql.toString())
.append(" ")
.append(mSql.substring(mSql.lastIndexOf(LIMIT), mSql.length()));
else if (mSql.indexOf(WHERE) > 0)
newSqlBuilder.append(mSql)
.append(" and ")
.append(dataAuthSql.toString());
else
newSqlBuilder.append(mSql)
.append(WHERE_CONDITION)
.append(" and ")
.append(dataAuthSql.toString());
System.out.println("增强后的SQL:\\t"+newSqlBuilder.toString());
//通过反射修改sql语句
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSqlBuilder.toString());
return invocation.proceed();
/**
* 添加where关键字
* @param sql sql语句
* @return sql
*/
private String addWhere(String sql)
if (sql.indexOf(WHERE) >= 0)
return sql;
StringBuilder newSqlBuilder = new StringBuilder();
if (sql.indexOf(GROUP_BY) > 0)
newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(GROUP_BY)))
.append(WHERE_CONDITION)
.append(sql.substring(sql.lastIndexOf(GROUP_BY), sql.length()));
else if (sql.indexOf(ORDER_BY) > 0)
newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(ORDER_BY)))
.append(WHERE_CONDITION)
.append(sql.substring(sql.lastIndexOf(ORDER_BY), sql.length()));
else if (sql.indexOf(LIMIT) > 0)
newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(LIMIT)))
.append(WHERE_CONDITION)
.append(sql.substring(sql.lastIndexOf(LIMIT), sql.length()));
else
newSqlBuilder.append(sql).append(" ").append(WHERE_CONDITION);
return newSqlBuilder.toString();
4、用户列表
/**
* 用户列表
*/
@GetMapping("/list")
@DataScope(fieldName="b.id",createBy="a.id")
@SaCheckPermission("system:user:view")
public ResultVo list(SysQuery query)
return ResultUtil.success(service.page(query));
<select id="page" resultType="com.entity.sys.SysUser">
SELECT a.id,user_name,real_name,dept_id,name deptName
FROM sys_user a
LEFT JOIN sys_dept b on a.dept_id=b.id
<where>
<if test="code != null">
AND a.user_name like concat('%', #code, '%')
</if>
<if test="name != null">
AND a.real_name like concat('%', #name, '%')
</if>
<if test="parentId != null">
AND b.id=#parentId
</if>
</where>
ORDER BY a.create_time desc
</select>
以上方式亲测单表、联表查询没什么问题,也不影响分页,但是遇上子查询、嵌套查询就以上是关于Springboot 使用 SaToken 进行登录认证权限管理以及路由规则接口拦截的主要内容,如果未能解决你的问题,请参考以下文章
SaToken使用springboot+redis+satoken权限认证
SaToken使用SpringBoot整合SaToken关于数据权限
SaToken使用SpringBoot整合SaToken关于数据权限
SaToken使用SpringBoot整合SaToken关于数据权限