基于springboot+注解+mybatis拦截器的权限控制
一、功能设计
基于角色的权限访问控制(Role-Base Access Control)
(1)、需要做数据权限功能的表加上一个权限id字段,如部门id;
(2)、将部门分配给不同角色,再将角色分配给用户;
(3)、在mapper上添加注解,拦截指定的sql;
/**
* 根据条件分页查询用户列表
*
* @param userVo 用户信息
* @return 用户信息集合信息
*/
@DataScope(scopeTableAlias = "d")
public List<SysUser> selectUserList(UserVo userVo);
(4)、取到对应sql后将sql进行处理,如:
select * from t where t.部门id in ('部门1','部门2','部门3')
二、后端实现
1、创建数据权限注解类
DataScope 权限注解
/**
* 数据权限过滤注解
*
* @author ao
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 表的别名
*/
public String scopeTableAlias() default "";
}
2、创建自定义mybatis拦截器
DataScopeInterceptor 类
/**
* Mybatis - 数据权限拦截器
* @author ao
*/
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
@Component
public class DataScopeInterceptor implements Interceptor {
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 初始化 tokenService 从redis获取用户信息
TokenService tokenService = SpringBootUtils.getBean(TokenService.class);
//获取mapper对象
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// 获取方法上的数据权限注解,如果没有注解,则直接通过(mybatis责任链模式)
DataScope dataScope = getPermissionByDelegate(mappedStatement);
if (dataScope == null) {
log.info("----未添加注解,无需数据过滤----");
return invocation.proceed();
}
log.info("----启用数据权限拦截器----");
if (StringUtils.isEmpty(dataScope.scopeTableAlias())
&& StringUtils.isEmpty(dataScope.userAlias())
&& StringUtils.isEmpty(dataScope.deptAlias())){
log.info("----未添加表别名,跳过数据过滤----");
return invocation.proceed();
}
// 获取用户信息
LoginUser loginUser = tokenService.getLoginUser();
if (loginUser == null) {
log.error("数据权限获取失败,当前用户信息为空!");
}
// 根据用户权限处理要拼接的sql
String authSql = handleDataScope(loginUser,dataScope);
//如果要拼接的sql为空,那直接返回
if("".equals(authSql.trim())){
log.info("拼接的sql为空");
return invocation.proceed();
}
//原sql
String sql = mappedStatement.getBoundSql(invocation.getArgs()[1]).getSql();
log.info("原始sql:{}",sql);
//处理sql拼接
this.permissionSql(sql,authSql,invocation,dataScope);
return invocation.proceed();
}
/**
* 获取数据权限注解信息
*
* @param mappedStatement
* @return
*/
private DataScope getPermissionByDelegate(MappedStatement mappedStatement) {
try {
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
final Class<?> cls = Class.forName(className);
final Method[] method = cls.getMethods();
for (Method me : method) {
// 判断mapper对象的方法是否存在权限注解
if (me.getName().equals(methodName) && me.isAnnotationPresent(DataScope.class)) {
return AnnotationUtils.findAnnotation(me, DataScope.class);
}
}
} catch (Exception e) {
log.error("权限注解获取失败:",e);
}
return null;
}
private String handleDataScope(LoginUser loginUser,DataScope controllerDataScope)
{
// 根据用户权限拼装sql 这里只是举例,实际情况根据自己业务来
return "(t.deptId in (1,2))";
}
/**
* 权限sql包装
* @param sql 原sql
* @param authSql 数据权限sql
* @param invocation
*/
private void permissionSql(String sql,String authSql,Invocation invocation,DataScope dataScope) {
final Object[] args = invocation.getArgs();
BoundSql boundSql;
//mapper对象
MappedStatement statement = (MappedStatement) args[0];
Object parameterObject = args[1];
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
boundSql = statement.getBoundSql(parameterObject);
} else {
//6 个参数时
boundSql = (BoundSql) args[5];
}
MappedStatement newStatement = newMappedStatement(statement, new DataScopeSqlSource(boundSql));
MetaObject msObject = MetaObject.forObject(newStatement, new DefaultObjectFactory(),
new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
//sql拼接
if(!"".equals(authSql.trim())){
Statement stmt = null;
// 如果转换失败 直接嵌套sql(默认查询了权限字段)
try {
stmt = CCJSqlParserUtil.parse(sql);
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
//判断是否存在where
if (((PlainSelect) selectBody).getWhere() == null) {
sql = sql + " where " +authSql;
}else {
sql = sql + " and " + authSql;
}
} catch (JSQLParserException e) {
sql = "select * from (" + sql + ") "+ dataScope.scopeTableAlias() +" where " +authSql;
}
log.info("拼接sql为:{}",sql);
}
//sql替换
msObject.setValue("sqlSource.boundSql.sql", sql);
args[0] = newStatement;
if (args.length == 6) {
// 6个参数时
Object parameter = args[1];
List<ParameterMapping> additionalParameters = boundSql.getParameterMappings();
BoundSql dataScopeBoundSql = new BoundSql(newStatement.getConfiguration(), sql, additionalParameters, parameter);
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
dataScopeBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}
args[5] = dataScopeBoundSql;
}
}
/**
* MappedStatement包装
* @param ms
* @param newSqlSource
* @return
*/
private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource,
ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
}
DataScopeSqlSource 类:
public class DataScopeSqlSource implements SqlSource {
private BoundSql boundSql;
public DataScopeSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
3、注册自定义的拦截器
1、使用mybatis配置文件注册,在mybatis-config.xml中写入:
<plugins>
<plugin interceptor="com.xxx.interceptor.DataScopeInterceptor"></plugin>
</plugins>
2、在mybatis配置类中设置:
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
{
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
String mapperLocations = env.getProperty("mybatis.mapperLocations");
String configLocation = env.getProperty("mybatis.configLocation");
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
VFS.addImplClass(SpringBootVFS.class);
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
sessionFactory.setPlugins(DataScopeInterceptor);
return sessionFactory.getObject();
}
三、自定义拦截器未生效问题
1、 多个拦截器拦截类型导致:
拦截器的调用顺序分为两大种,第一种是拦截的不同对象,例如拦截 Executor 和 拦截 StatementHandler 就属于不同的拦截对象, 这两类的拦截器在整体执行的逻辑上是不同的,在 Executor 中的 query 方法执行过程中会调用StatementHandler。
所以StatementHandler 属于 Executor 执行过程中的一个子过程。 所以这两种不同类别的插件在配置时,一定是先执行 Executor 的拦截器,然后才会轮到 StatementHandler。所以这种情况下配置拦截器的顺序就不重要了,在 MyBatis 逻辑上就已经控制了先后顺序。
2、 拦截类型都为Executor
<plugins>
<plugin interceptor="com.xxx.interceptor.myInterceptor1"></plugin>
<plugin interceptor="com.xxx.interceptor.myInterceptor2"></plugin>
<plugin interceptor="com.xxx.interceptor.myInterceptor3"></plugin>
</plugins>
执行顺序为:3>2>1>Executor>1>2>3(1>2>3是因为使用代理之后会还会有返回)
3、引入了pagehelper
当项目引入了pagehelper之后,debug发现mapper对象中pagehelper的分页拦截器始终在最后注册,导致自定义拦截器在pagehelper之后执行,最终分页数据和总计出现偏差。
通过查看PageHelperAutoConfiguration源码发现它有一个@AutoConfigureAfter(MybatisAutoConfiguration.class)注解,这表明他是在MybatisAutoConfiguration加载完成后,才执行自己的加载。
解决办法:
1、使用spring监听器注册自定义拦截器
(1)、创建配置类
@Component
public class MyBatisInterceptorConfig implements ApplicationListener<SqlsessionEvent> {
private static final Logger log = LoggerFactory.getLogger(MyBatisInterceptorConfig.class);
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Override
public void onApplicationEvent(SqlsessionEvent sqlsessionEvent) {
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
// 数据权限拦截器
sqlSessionFactory.getConfiguration().addInterceptor(new DataScopeInterceptor());
}
log.info("mybatis自定义拦截器:DataScopeInterceptor");
}
}
(2)、创建监听事件类
public class SqlsessionEvent extends ApplicationContextEvent {
public SqlsessionEvent(ApplicationContext source) {
super(source);
}
}
(3)、在启动类中注册监听器
public class StartApplication
{
public static void main(String[] args){
ConfigurableApplicationContext context = SpringApplication.run(StartApplication.class, args);
context.publishEvent(new SqlsessionEvent(context));
}
}
这个时候再debug时发现pagehelper的分页拦截器已经在先注册了
2、使用自动装配@Configuration+@AutoConfigureAfter
由于PageHelperAutoConfiguration有@AutoConfigureAfter(MybatisAutoConfiguration.class),有两种方式
(1)、启动类中排除PageHelperAutoConfiguration,手动注册PageHelper分页拦截器;
排除PageHelperAutoConfiguration
@SpringBootApplication(exclude = {PageHelperAutoConfiguration.class})
@MapperScan({"a.b.*.**.mapper"})
public class StartMainApplication {
public static void main(String[] args) {
SpringApplication.run(StartMainApplication .class,args);
}
}
自动装配类MyInterceptorAutoConfig
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@AutoConfigureAfter({MybatisAutoConfiguration.class})
public class MyInterceptorAutoConfig implements InitializingBean {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Override
public void afterPropertiesSet() {
for (SqlSessionFactory sessionFactory : this.sqlSessionFactoryList) {
// 数据权限拦截器
DataScopeInterceptor dataScopeInterceptor = new DataScopeInterceptor();
//PageHelper 分页插件
PageInterceptor pageInterceptor = new PageInterceptor();
org.apache.ibatis.session.Configuration configuration = sessionFactory.getConfiguration();
if (!this.containsInterceptor(configuration, dataScopeInterceptor)) {
configuration.addInterceptor(dataScopeInterceptor);
}
if (!this.containsInterceptor(configuration, pageInterceptor)) {
configuration.addInterceptor(pageInterceptor);
}
}
}
private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration, Interceptor interceptor) {
try {
return configuration.getInterceptors().contains(interceptor);
} catch (Exception var) {
return false;
}
}
}
(2)、在PageHelperAutoConfiguration加载完成后再加载自定义拦截器
自动装配类MyInterceptorAutoConfig
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@AutoConfigureAfter({PageHelperAutoConfiguration.class})
public class MyInterceptorAutoConfig implements InitializingBean {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Override
public void afterPropertiesSet() {
for (SqlSessionFactory sessionFactory : this.sqlSessionFactoryList) {
// 数据权限拦截器
DataScopeInterceptor dataScopeInterceptor = new DataScopeInterceptor();
org.apache.ibatis.session.Configuration configuration = sessionFactory.getConfiguration();
if (!this.containsInterceptor(configuration, dataScopeInterceptor)) {
configuration.addInterceptor(dataScopeInterceptor);
}
}
}
private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration, Interceptor interceptor) {
try {
return configuration.getInterceptors().contains(interceptor);
} catch (Exception var) {
return false;
}
}
}
注意:此处使用@AutoConfigureAfter时,需指明该类为自动装配类,而不是用户自定义配置类。
将自定义的自动装配类放到启动类非同级包下,在MATE-INF下新建spring.factories写入以下配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.autoconfigure.MyInterceptorAutoConfig
再进行debug测试:
这时可以发现自定义拦截器在MybatisPlus插件和PageHelper之后注册的了