框架设计
基于Spring Security+JWT技术实现登录认证和访问授权,基于Mybatis 拦截器实现数据权限的控制。由于已有前文介绍Spring Security如有兴趣可以查看,在此将重点于数据权限的实现及菜单等表结构的设计。
Mybatis 拦截器
介绍Mybatis拦截器之前先来了解一下Mybatis工作流程。
工作流程

Configuration装载配置文件Mapper.xml。
通过装载得配置文件Configuration构建SqlSessionFactory。
通过SqlSessionFactory工厂创建SqlSession会话。
SqlSession持有Configuration、Executor对象执行被映射得Sql语句。
Executor真正执行Sql的核心接口。
StatementHandler封装了JDBC Statement类的相关操作,真正与数据打有交道的接口。
ResultSetHandler封装查询结果集,将StatementHandler执行结果输出到pojo。
拦截器
MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。那么拦截器拦截MyBatis中的哪些内容呢?
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
我们看到了可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。
总体概括为:
拦截执行器的方法
拦截参数的处理
拦截结果集的处理
拦截Sql语法构建的处理
分页插件PageHelper其实就是利用Mybatis拦截器实现得,我们这也是类似,通过Mybatis拦截器拦截执行器得方法对将要执行的sql语句进行拦截解析成Sql表达式,在根据认证用户所拥有的数据权限进行拼接过滤条件达到控制的效果。整体思路并不复杂,难点在于如何将用户所拥有的资源传递到拦截器中,拦截器如何解析Sql,以及Sql的重新组装。下面将贴出实现的核心代码。
核心代码实现
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
@Component
public class DataCountInterceptor implements Interceptor {private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();private static EqualsTo NO_Match = new EqualsTo();private static EqualsTo Match = new EqualsTo();static {NO_Match.setLeftExpression(new Column("1"));NO_Match.setRightExpression(new LongValue(0));Match.setLeftExpression(new Column("1"));Match.setRightExpression(new LongValue(1));}@Overridepublic Object intercept(Invocation invocation) throws Throwable {try {Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];Object parameter = args[1];RowBounds rowBounds = (RowBounds) args[2];ResultHandler resultHandler = (ResultHandler) args[3];Executor executor = (Executor) invocation.getTarget();CacheKey cacheKey;BoundSql boundSql;//由于逻辑关系,只会进入一次if (args.length == 4) {//4 个参数时boundSql = ms.getBoundSql(parameter);cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);} else {//6 个参数时cacheKey = (CacheKey) args[4];boundSql = (BoundSql) args[5];}//TODO 自己要进行的各种处理String id = ms.getId();id = id.substring(0, id.lastIndexOf('.'));MybatisDataAuthority localDataAuthority = DataPermissionHelper.getLocalDataAuthority();// 超级管理员放行if (localDataAuthority != null && localDataAuthority.getSuperAdmin() != 1) {String operatorAlias = Strings.isNotBlank(localDataAuthority.getOperatorAlias()) ? localDataAuthority.getOperatorAlias() : "operator";String gameAlias = Strings.isNotBlank(localDataAuthority.getGameAlias()) ? localDataAuthority.getGameAlias() : "game_code";String mediaAlias = Strings.isNotBlank(localDataAuthority.getMediaAlias()) ? localDataAuthority.getMediaAlias() : "media_code";String sql = boundSql.getSql();Statement sm = CCJSqlParserUtil.parse(sql);Select select = (Select) sm;assert select != null;SelectBody selectBody = select.getSelectBody();PlainSelect plainSelect = (PlainSelect) selectBody;// 设置whereExpression expression = Match;if (localDataAuthority.isHasOperator() && !localDataAuthority.isAllOperator()) {if (CollectionUtils.isEmpty(localDataAuthority.getOperators())) {expression = NO_Match;} else {List<Expression> collect = new ArrayList<>(16);for (Integer userId : localDataAuthority.getOperators()) {collect.add(new StringValue("" + userId + ""));}ExpressionList operatorList = new ExpressionList(collect);expression = new InExpression(new Column(operatorAlias), operatorList);}}if (localDataAuthority.isHasGame() && localDataAuthority.getAllGame() != 1) {Expression gameCodeExpression;if (CollectionUtils.isEmpty(localDataAuthority.getGames())) {gameCodeExpression = NO_Match;} else {List<Expression> collect = new ArrayList<>(16);for (String gameCode : localDataAuthority.getGames()) {collect.add(new StringValue("" + gameCode + ""));}ExpressionList gameCodeList = new ExpressionList(collect);gameCodeExpression = new InExpression(new Column(gameAlias), gameCodeList);}expression = new AndExpression(expression, gameCodeExpression);}if (localDataAuthority.isHasMedia() && localDataAuthority.getAllMedia() != 1) {Expression mediaCodeExpression;if (CollectionUtils.isEmpty(localDataAuthority.getMediaCodes())) {mediaCodeExpression = NO_Match;} else {List<Expression> collect = new ArrayList<>(16);for (String mediaCode : localDataAuthority.getMediaCodes()) {collect.add(new StringValue("" + mediaCode + ""));}ExpressionList mediaCodeList = new ExpressionList(collect);mediaCodeExpression = new InExpression(new Column(mediaAlias), mediaCodeList);}expression = new AndExpression(expression, mediaCodeExpression);}//代理人处理if (!CollectionUtils.isEmpty(localDataAuthority.getAgentIds())) {List<Expression> collect = new ArrayList<>(16);for (Integer agentId : localDataAuthority.getAgentIds()) {collect.add(new StringValue("" + agentId + ""));}ExpressionList agentIds = new ExpressionList(collect);InExpression inExpression = new InExpression(new Column("id"), agentIds);expression = new OrExpression(expression, inExpression);}CustomMultiExpression customMultiExpression = new CustomMultiExpression(Arrays.asList(expression));if (plainSelect.getWhere() != null) {plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), customMultiExpression));} else {plainSelect.setWhere(customMultiExpression);}Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");additionalParametersField.setAccessible(true);Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), plainSelect.toString(), boundSql.getParameterMappings(), parameter);//设置动态参数for (String key : additionalParameters.keySet()) {pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));}return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, pageBoundSql);} else {return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);}} finally {DataPermissionHelper.clearDataPermission();}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}}
由上代码可以看到,我们定义了一个拦截器,把将要执行Executor的sql语句进行拦截,通过线程上下文拿到此前封装好的数据形式MybatisDataAuthority,在由Jsqlparser解析器将一段完整的Sql解析成方便拼装的Sql表达式,最后将此sql输出由执行器执行,达到控制的效果。
public class DataPermissionHelper {private static ThreadLocal<MybatisDataAuthority> localDataPermission = new ThreadLocal<>();/*** 获取 Page 参数** @return*/public static MybatisDataAuthority getLocalDataAuthority() {return localDataPermission.get();}public static void clearDataPermission() {localDataPermission.remove();}public static void startDataPermissionHasAgent(Integer permissionId, Boolean hasGame, Boolean hasMedia, String operatorAlias, String gameAlias, String mediaAlias, String userDataTable) {// 获取当前的用户LoginUser loginUser = SecurityUtils.getLoginUser();List<Integer> userIds = new ArrayList<>(16);boolean allOperator = false;if (loginUser != null) {// 查询本人的当前页面的操作权限for (PermissionV2Vo permissionV2Vo : loginUser.getPermissionV2VoList()) {if (permissionV2Vo.getId().equals(permissionId)) {switch (permissionV2Vo.getDataAuthority()) {// 本人case 1:userIds.add(loginUser.getUserId());break;// 本部门case 2:// 获取部门的人员Example example = new Example(UserV2.class);example.createCriteria().andEqualTo("departmentId", loginUser.getUser().getDepartmentId());userIds = SpringUtil.getBeanByType(UserV2Mapper.class).selectByExample(example).stream().map(BaseEntity::getId).collect(Collectors.toList());break;// 公司case 3:allOperator = true;default:break;}}}}// 获取代理人UserDataPermissionMapper userDataPermissionMapper = SpringUtil.getBeanByType(UserDataPermissionMapper.class);Example example = new Example(UserDataPermission.class);example.createCriteria().andEqualTo("status", 1).andEqualTo("tableName", userDataTable).andEqualTo("userId", loginUser.getUserId());List<UserDataPermission> userDataPermissions = Optional.ofNullable(userDataPermissionMapper.selectByExample(example)).orElse(new ArrayList<>(16));List<Integer> agentIds = userDataPermissions.stream().map(UserDataPermission::getRelatedId).collect(Collectors.toList());MybatisDataAuthority mybatisDataAuthority = MybatisDataAuthority.builder().superAdmin(loginUser.getUser().getSuperAdmin()).hasOperator(true).hasMedia(hasMedia).hasGame(hasGame).operatorAlias(operatorAlias).gameAlias(gameAlias).allGame(CollectionUtils.isEmpty(loginUser.getGameCodes()) ? 1 : 0).allMedia(CollectionUtils.isEmpty(loginUser.getMediaCodes()) ? 1 : 0).allOperator(allOperator).mediaAlias(mediaAlias).operators(userIds).games(loginUser.getGameCodes()).games(loginUser.getGameCodes()).mediaCodes(loginUser.getMediaCodes()).agentIds(agentIds).build();localDataPermission.set(mybatisDataAuthority);}public static void startDataPermission(Integer permissionId, Boolean hasGame, Boolean hasMedia, String operatorAlias, String gameAlias, String mediaAlias) {// 获取当前的用户boolean allOperator = false;LoginUser loginUser = SecurityUtils.getLoginUser();List<Integer> userIds = new ArrayList<>(16);if (loginUser != null) {// 查询本人的当前页面的操作权限for (PermissionV2Vo permissionV2Vo : loginUser.getPermissionV2VoList()) {if (permissionV2Vo.getId().equals(permissionId)) {switch (permissionV2Vo.getDataAuthority()) {// 本人case 1:userIds.add(loginUser.getUserId());break;// 本部门case 2:// 获取部门的人员Example example = new Example(UserV2.class);example.createCriteria().andEqualTo("departmentId", loginUser.getUser().getDepartmentId());userIds = SpringUtil.getBeanByType(UserV2Mapper.class).selectByExample(example).stream().map(BaseEntity::getId).collect(Collectors.toList());break;// 公司case 3:allOperator = true;default:break;}}}}MybatisDataAuthority mybatisDataAuthority = MybatisDataAuthority.builder().superAdmin(loginUser.getUser().getSuperAdmin()).hasOperator(true).hasMedia(hasMedia).hasGame(hasGame).operatorAlias(operatorAlias).gameAlias(gameAlias).allOperator(allOperator).allGame(CollectionUtils.isEmpty(loginUser.getGameCodes()) ? 1 : 0).allMedia(CollectionUtils.isEmpty(loginUser.getMediaCodes()) ? 1 : 0).mediaAlias(mediaAlias).operators(userIds).games(loginUser.getGameCodes()).games(loginUser.getGameCodes()).mediaCodes(loginUser.getMediaCodes()).build();localDataPermission.set(mybatisDataAuthority);}public static void startOnlyGamePermission(String gameAlias) {// 获取当前的用户LoginUser loginUser = SecurityUtils.getLoginUser();MybatisDataAuthority mybatisDataAuthority = MybatisDataAuthority.builder().superAdmin(loginUser.getUser().getSuperAdmin()).hasOperator(false).hasMedia(false).gameAlias(gameAlias).hasGame(true).gameAlias(gameAlias).allGame(CollectionUtils.isEmpty(loginUser.getGameCodes()) ? 1 : 0).allMedia(CollectionUtils.isEmpty(loginUser.getMediaCodes()) ? 1 : 0).games(loginUser.getGameCodes()).build();localDataPermission.set(mybatisDataAuthority);}public static void startOnlyMediaPermission(String mediaAlias) {// 获取当前的用户LoginUser loginUser = SecurityUtils.getLoginUser();MybatisDataAuthority mybatisDataAuthority = MybatisDataAuthority.builder().superAdmin(loginUser.getUser().getSuperAdmin()).hasOperator(false).hasMedia(true).mediaAlias(mediaAlias).hasGame(false).allGame(CollectionUtils.isEmpty(loginUser.getGameCodes()) ? 1 : 0).allMedia(CollectionUtils.isEmpty(loginUser.getMediaCodes()) ? 1 : 0).mediaCodes(loginUser.getMediaCodes()).build();localDataPermission.set(mybatisDataAuthority);}public static void startOnlyMediaAndGamePermission(String mediaAlias, String gameAlias) {// 获取当前的用户LoginUser loginUser = SecurityUtils.getLoginUser();MybatisDataAuthority mybatisDataAuthority = MybatisDataAuthority.builder().superAdmin(loginUser.getUser().getSuperAdmin()).hasOperator(false).hasMedia(true).mediaAlias(mediaAlias).hasGame(true).gameAlias(gameAlias).allGame(CollectionUtils.isEmpty(loginUser.getGameCodes()) ? 1 : 0).allMedia(CollectionUtils.isEmpty(loginUser.getMediaCodes()) ? 1 : 0).mediaCodes(loginUser.getMediaCodes()).games(loginUser.getGameCodes()).build();localDataPermission.set(mybatisDataAuthority);}public static List<Integer> getOperatorList(Integer permissionId) {LoginUser loginUser = SecurityUtils.getLoginUser();List<Integer> userIds = new ArrayList<>(16);if (loginUser != null) {// 查询本人的当前页面的操作权限for (PermissionV2Vo permissionV2Vo : loginUser.getPermissionV2VoList()) {if (permissionV2Vo.getId().equals(permissionId)) {switch (permissionV2Vo.getDataAuthority()) {// 本人case 1:userIds.add(loginUser.getUserId());break;// 本部门case 2:// 获取部门的人员Example example = new Example(UserV2.class);example.createCriteria().andEqualTo("departmentId", loginUser.getUser().getDepartmentId());userIds = SpringUtil.getBeanByType(UserV2Mapper.class).selectByExample(example).stream().map(BaseEntity::getId).collect(Collectors.toList());break;// 公司case 3:default:break;}}}}return userIds;}
}
上面代码为根据认证用户所拥有的资源进行数据的封装传递给拦截器的过程。主要思路为根据用户所属的权限组获取到用户当前页面拥有的数据权限:公司、部门、个人,若所属权限为公司即可查查看所有资源,无需处理。若为部门则将部门下的所有用户传递给拦截器过滤,若为个人同理。
/** * 获取应用名称列表 * @return*/
public List<AppNameVo> getAllAppNameList(){DataPermissionHelper.startOnlyGamePermission("game_code");return appConfigMapper.getAllAppNameList();
}
此为示例代码,可以看到在appConfigMapper.getAllAppNameList();上增加了一行开启游戏权限过滤的代码,通过此代码将原本查所有的游戏变成了只查当前用户拥有的游戏。