MyBatis拦截器实现sql查询权限动态修改

动机和具体情景

最近考虑怎么在Mybatis自动创建sql执行过程中进行介入,来不对原有代码耦合的情况下,实现对sql的修改。
考虑情景,比如多部门管理系统,员工工资和账户信息敏感,每个部门只能查到对应权限的员工信息。为了实现sql的鉴权,本来是需要将原始的sql语句加上某个权限字段的判断。
为了不耦合,现在的方案是在需要鉴权的Mybatis Mapper方法上增加一个注解,在运行过程中判断该注解存在即对sql进行修改,形成新的带权限字段判断的sql,这样对原始代码的修改就少很多(加个注解就行)。

基本原理和解析流程

Mybatis 允许在映射语句过程中的某一点进行拦截调用,其提供了基于反射的拦截类Interceptor来对方法进行拦截。这些可拦截的方法存在的原始执行类包括:Executor(执行器相关),ParameterHandler(参数处理相关),ResultSetHandler(结果集相关),StatementHandler(sql语法和会话创建相关)四种。
我们的需求是对sql语句进行改写,选择对StatementHandler进行改写。通过反射获取到此次Mybatis执行的原始Mapper接口和方法名,通过判断我们的自定义注解@permission是否存在来选择鉴权行为,之后从session拿到当前查询权限,从配置文件中拿到权限可查询的数据范围,即可对sql进行修改。最后将修改后的sql反射注入回Mybatis的对应执行类即可。

具体实现

原始查询代码

AccountInfo 账户信息类

映射实体类,存放账户信息。要注意的是在数据库表中还有一个字段permission表示查询权限,在实体类中并没有表示。

public class AccountInfo implements Serializable {
    int id;
    String account;
    String name;
    BigDecimal money;
    public AccountInfo(){}
    public AccountInfo(int id, String account, String name, BigDecimal money){
        this.id = id;
        this.account = account;
        this.name = name;
        this.money = money;
}
AccountMapper 操作Mapper类

@Permission为自定义注解,表示该方法需要进行鉴权操作,只能查询当前权限下对应的数据信息。

@Mapper
public interface AccountMapper {
    @Permission
    @Select("select * from account")
    public List<AccountInfo> getAccountInfoList();
}
MainController 主要控制类

给出了查询接口,同时因为设定上权限是存在session中的,给了个模拟赋予权限的接口。

@Controller
public class MainController {
    @Autowired
    AccountMapper accountMapper;

    @RequestMapping("/getAccountInfo")
    @ResponseBody
    public String getAccountInfo() throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        List<AccountInfo> accounts  = accountMapper.getAccountInfoList();
        return objectMapper.writeValueAsString(accounts);
    }

    @RequestMapping("/setPermission")
    @ResponseBody
    public String setPermission(HttpServletRequest request, String permission){
        request.getSession().setAttribute("permission", permission);
        return permission;
    }
}

权限相关实现

配置文件设置

增加权限级联的设置,为值键对形式,表示key能查询的数据范围。

permission:
  permissionMap:
    develop: "\"develop\""
    advertise: "\"advertise\""
    finance: "\"develop\", \"advertise\", \"finance\""

需要注意的是sql字符串查询需要使用引号,所以配置文件中需要增加引号转义,方便后续sql的使用。

PermissionConfig读取配置文件

Map形式不能直接读取,增加对应的读取config类,主要其中的成员名称需要和配置文件中对应(这边为permissionMap)。

// 从配置文件中读取permission层次范围
@Configuration
@ConfigurationProperties(prefix = "permission")
@EnableConfigurationProperties(PermissionConfig.class)
public class PermissionConfig {
    private Map<String, String> permissionMap = new HashMap<>();

    public Map<String, String> getPermissionMap() {
        return permissionMap;
    }

    public void setPermissionMap(Map<String, String> permissionMap) {
        this.permissionMap = permissionMap;
    }
}
自定义注解

注解只是为了判断是否需要鉴权,不需要特殊的成员。同时设置retentionruntime,并设置作用对象为方法method

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
// 指名数据库查询方法需要和权限挂钩
public @interface Permission {}

Mybatis拦截器实现

主要的实现集中在PermissionInterceptor中,主要可分为几个部分:拦截配置,元数据获取,自定义注解判断,权限获取与sql修改,反射注入。主要逻辑集中在intercept方法中。

拦截配置

主要是通过@Intercepts注解对拦截器类需要拦截的Handler和方法进行设置,方便之后反射获取对应的类。我们这边是对语句的最终sql进行处理,选择StatementHandler中的prepare方法进行拦截,args中为方法参数类型来判断重载。如果是对query进行拦截,后续注入时其实已经执行了,新的sql并不会被调用。

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Component
public class PermissionInterceptor implements Interceptor {
元数据获取

用的Mybatis给的元数据类MetaObject进行获取。

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();

    @Autowired
    private PermissionConfig permissionConfig;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取sql信息
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        System.out.println("原sql为: " + sql);

        // 获取元数据
        MetaObject metaResultSetHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
        MappedStatement mappedStatement = (MappedStatement) metaResultSetHandler.getValue("delegate.mappedStatement");

MappedStatement 在 对应的Handler 的 delegate.mappedStatement 属性对象中,包含元数据信息。
获取类和方法信息。

        // 获取调用方法
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        System.out.println("调用方法为: " + id);
注解判断

反射获取对应方法的注解列表即可。

        // 注解查询
        Class clazz = Class.forName(className);
        Method method = clazz.getDeclaredMethod(methodName);
        boolean needPermission = method.isAnnotationPresent(Permission.class);

        // 对注解方法进行权限处理
        if(needPermission){
            System.out.println("需要进行sql权限变化");
获取权限信息并进行sql修改

配置文件中获取权限范围信息,增加到原sql的条件判断中。

	// 获取权限信息
	HttpSession session = HttpUtil.getSession();
	String permission = (String) session.getAttribute("permission");
	Map<String, String> map = permissionConfig.getPermissionMap();
	for(String key:map.keySet()){
		System.out.println(key);
	}
	String canSelectPermission = null;
	if(map.containsKey(permission)){
		canSelectPermission = map.get(permission);
	}
	System.out.printf("当前权限:%s, 可查询范围:%s%n", permission, canSelectPermission);

// 修改sql
	String newSql = String.format("select * from (%s) `range` where `range`.permission in (%s)", sql, canSelectPermission);
	// String newSql = "select * from account where permission in (\"advertise\")";
	System.out.println("修改后的sql为: " + newSql);
反射注入并执行

注入到BoundSql类中,替换原sql。

  // 反射修改handler中的sql以执行
  Class boundClass = boundSql.getClass();
  Field field = boundClass.getDeclaredField("sql");
  field.setAccessible(true);
  field.set(boundSql, newSql);

效果展示

数据库简单数据

包括一个查询权限字段
MyBatis拦截器实现sql查询权限动态修改_第1张图片

给予权限信息

Session中写入权限。
MyBatis拦截器实现sql查询权限动态修改_第2张图片

不同权限下获取的信息结果

Finance:

MyBatis拦截器实现sql查询权限动态修改_第3张图片
MyBatis拦截器实现sql查询权限动态修改_第4张图片
Develop:
MyBatis拦截器实现sql查询权限动态修改_第5张图片MyBatis拦截器实现sql查询权限动态修改_第6张图片

总结

一个简单的Mybatis的拦截器尝试,用于对sql依靠查询权限进行动态修改。主要就是Mybatis这个MetaObject需要知道对应的statement的value才能反射拿到,查了好久才发现是delegate.mappedStatement,很神秘。Demo代码传到github了,有兴趣的看一下吧。
https://github.com/huiluczP/mybatis_datascope

你可能感兴趣的:(java,spring,sql,MyBatis,springboot)