基于handleMethod写的一款分级式接口权限检查方案。
权限自动同步机制(启动更新,页面不提供增删改):
public class AuthorizationMappingGenerateExecutor implements EasyApplicationRunner {
@Autowired
private AuthorizationMappingMapper authorizationMappingMapper;
@Autowired
private RequestMappingHandlerMapping mapping;
@Autowired
private SupportSecurityProperties securityProperties;
// 启动时执行一次,随便用用作个排序。没安全问题
int i = 0;
@Override
public void doBusiness() {
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = mapping.getHandlerMethods();
Collection<HandlerMethod> handlerMethods = handlerMethodMap.values();
// 表中已存在的code
List<String> existsCodes = authorizationMappingMapper.selectProperties(Wrappers.lambdaQuery(), AuthorizationMapping::getCode);
// 排重set
Set<String> currentExistsCodes = new HashSet<>();
// 配置的级别
MappingLevel mappingLevel = securityProperties.getMappingLevel();
// 添加的权限
List<AuthorizationMapping> insertMappings = new ArrayList<>();
// 需要更新的权限
List<AuthorizationMapping> updateMappings = new ArrayList<>();
for (HandlerMethod handlerMethod : handlerMethods) {
Class<?> beanType = handlerMethod.getBeanType();
String name = beanType.getName();
// 只取本系统内的接口
if (!name.contains("xxxx")) {
continue;
}
AuthorizationMapping moduleMapping = buildModuleMapping(handlerMethod);
int type = chooseType(existsCodes, currentExistsCodes, moduleMapping);
if (type == 1) {
insertMappings.add(moduleMapping);
} else if (type == 2) {
updateMappings.add(moduleMapping);
}
// 只开启了一级权限检查
if (mappingLevel.is(MappingLevel.MODULE)) {
continue;
}
AuthorizationMapping controllerMapping = buildControllerMapping(moduleMapping.getCode(), handlerMethod);
int controllerType = chooseType(existsCodes, currentExistsCodes, controllerMapping);
if (controllerType == 1) {
insertMappings.add(controllerMapping);
} else if (controllerType == 2) {
updateMappings.add(controllerMapping);
}
// 只开启了二级权限检查
if (mappingLevel.is(MappingLevel.CONTROLLER)) {
continue;
}
// 方法级的权限检查
AuthorizationMapping methodMapping = buildMethodMapping(controllerMapping.getCode(), handlerMethod);
int methodType = chooseType(existsCodes, currentExistsCodes, methodMapping);
if (methodType == 1) {
insertMappings.add(methodMapping);
} else if (methodType == 2) {
updateMappings.add(methodMapping);
}
}
// 批量插入
authorizationMappingMapper.insertSplitBatch(insertMappings, 200);
// 配置是否更新,一条条更新太慢,影响启动速度。
if (securityProperties.getEnableMappingUpdate()) {
updateMappings.forEach(authorizationMappingMapper::updateById);
}
// 清掉子级,不占表空间。开放的话重启构建即可。code不会变
LambdaQueryWrapper<AuthorizationMapping> wrapper = Wrappers.lambdaQuery();
if (mappingLevel.is(MappingLevel.MODULE)) {
wrapper.in(AuthorizationMapping::getLevel, ZYListUtils.toList(MappingLevel.CONTROLLER.level(), MappingLevel.METHOD.level()));
} else if (mappingLevel.is(MappingLevel.CONTROLLER)) {
wrapper.eq(AuthorizationMapping::getLevel, MappingLevel.METHOD.level());
}
authorizationMappingMapper.delete(wrapper);
}
private int chooseType(List<String> existsCodes, Set<String> currentExistsCodes, AuthorizationMapping methodMapping) {
String code = methodMapping.getCode();
if (currentExistsCodes.contains(code)) {
return 0;
}
// 去重
currentExistsCodes.add(methodMapping.getCode());
// 更新or插入
if (!existsCodes.contains(code)) {
return 1;
} else {
return 2;
}
}
// 构建模块权限
private AuthorizationMapping buildModuleMapping(HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
Package aPackage = beanType.getPackage();
AuthorizationMapping packageMapping = buildBaseMapping(MappingLevel.MODULE);
packageMapping.setMapping("");
packageMapping.setParentCode(ZYTreeUtils.TREE_ROOT_ID);
String moduleCode = PermissionMappingGenerator.generateModuleKey(handlerMethod);
packageMapping.setCode(moduleCode);
String packageName = aPackage.getName();
packageMapping.setFileLocal(packageName);
String packageChinaName = try2getPackageChinaName(handlerMethod);
packageMapping.setName(null != packageChinaName ? packageChinaName : packageName);
return packageMapping;
}
// 构建controller级权限
private AuthorizationMapping buildControllerMapping(String parentCode, HandlerMethod handlerMethod) {
AuthorizationMapping packageMapping = buildBaseMapping(MappingLevel.CONTROLLER);
packageMapping.setMapping("");
packageMapping.setParentCode(parentCode);
String controllerCode = PermissionMappingGenerator.generateControllerKey(handlerMethod);
packageMapping.setCode(controllerCode);
Class<?> beanType = handlerMethod.getBeanType();
packageMapping.setFileLocal(beanType.getName());
Theme theme = beanType.getAnnotation(Theme.class);
packageMapping.setName(null != theme ? theme.value() : beanType.getSimpleName());
return packageMapping;
}
// 构建方法级权限
private AuthorizationMapping buildMethodMapping(String parentCode, HandlerMethod handlerMethod) {
String methodCode = PermissionMappingGenerator.generateMethodKey(handlerMethod);
AuthorizationMapping packageMapping = buildBaseMapping(MappingLevel.METHOD);
packageMapping.setCode(methodCode);
packageMapping.setParentCode(parentCode);
Class<?> beanType = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
packageMapping.setFileLocal(beanType.getName() + "." + method.getName());
packageMapping.setMapping(spellMapping(method, beanType));
packageMapping.setName(getMappingName(handlerMethod));
return packageMapping;
}
private AuthorizationMapping buildBaseMapping(MappingLevel level) {
AuthorizationMapping packageMapping = new AuthorizationMapping();
packageMapping.setCreateDate(System.currentTimeMillis());
packageMapping.setIsMiniAuth(0);
packageMapping.setSort(++i);
packageMapping.setLevel(level.level());
packageMapping.setRemarks(level.getMappingName());
return packageMapping;
}
private String getMappingName(HandlerMethod handlerMethod) {
SystemLog systemLog = handlerMethod.getMethodAnnotation(SystemLog.class);
if (null != systemLog) {
return systemLog.value();
}
return handlerMethod.getMethod().getName();
}
private String spellMapping(Method method, Class<?> beanType) {
StringBuilder path = new StringBuilder();
RequestMapping parentRequestMapping = beanType.getAnnotation(RequestMapping.class);
if (null != parentRequestMapping) {
String[] value = parentRequestMapping.value();
if (value.length > 0) {
path.append(value[0]);
}
}
RequestMapping methodRequestMapping = method.getAnnotation(RequestMapping.class);
GetMapping methodGetRequestMapping = method.getAnnotation(GetMapping.class);
PostMapping methodPostRequestMapping = method.getAnnotation(PostMapping.class);
if (null != methodRequestMapping) {
String[] value = methodRequestMapping.value();
if (value.length > 0) {
path.append(value[0]);
}
} else if (null != methodGetRequestMapping) {
String[] value = methodGetRequestMapping.value();
if (value.length > 0) {
path.append(value[0]);
}
} else if (null != methodPostRequestMapping) {
String[] value = methodPostRequestMapping.value();
if (value.length > 0) {
path.append(value[0]);
}
}
return path.toString();
}
private String try2getPackageChinaName(HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
Package aPackage = beanType.getPackage();
Theme packageTheme = aPackage.getAnnotation(Theme.class);
if (null != packageTheme) {
return packageTheme.value();
}
return null;
}
}
权限检查拦截器
public class AuthorizationMappingInterceptor implements EasyMvcInterceptor, InitializingBean {
private final static Set<String> NO_NEED_CHECK_CODES = new HashSet<>();
private final static Set<String> CHECK_CODES = new HashSet<>();
@Autowired
private AuthorizationMappingMapper authorizationMappingMapper;
@Autowired
private SupportSecurityProperties securityProperties;
@Override
public boolean doBusiness(HandlerMethod handlerMethod, HttpServletRequest request, HttpServletResponse response) {
// 开放接口
if (ZYRequestUtils.isDirectApi()) {
return true;
}
// 超管
if (UserHelper.isSuperAdmin()) {
return true;
}
// 需要跳过的权限
if (isSkipMappingCheck(handlerMethod)) {
return true;
}
// 查询需要的权限
MappingLevel mappingLevel = securityProperties.getMappingLevel();
String permissionCode = PermissionMappingGenerator.generateByLevel(handlerMethod, mappingLevel);
// 缓存在存在,直接通过检查
if (NO_NEED_CHECK_CODES.contains(permissionCode)) {
return true;
}
// 该接口没有做权限限制,直接放行
if (!CHECK_CODES.contains(permissionCode)) {
NO_NEED_CHECK_CODES.add(permissionCode);
return true;
}
// 用户权限信息
LoginUser loginAreaUser = UserHelper.getLoginAreaUser();
Set<String> mappingCodes = loginAreaUser.getMappingCodes();
if (!hasPermission(permissionCode, mappingCodes)) {
throw new PermissionException("您没有访问接口的权限");
}
return true;
}
private boolean hasPermission(String permissionCode, Set<String> mappingCodes) {
return null != mappingCodes && mappingCodes.contains(permissionCode);
}
@Override
public void afterPropertiesSet() {
// 根据权限级别查询需要检查的HandleMethod或controller
MappingLevel mappingLevel = securityProperties.getMappingLevel();
LambdaQueryWrapper<AuthorizationMapping> wrapper = Wrappers.lambdaQuery();
wrapper.eq(AuthorizationMapping::getLevel, mappingLevel.level());
wrapper.eq(AuthorizationMapping::getIsMiniAuth, NO);
wrapper.select(AuthorizationMapping::getCode);
List<String> authorizationMappings = authorizationMappingMapper.selectProperties(wrapper, AuthorizationMapping::getCode);
CHECK_CODES.addAll(authorizationMappings);
}
private boolean isSkipMappingCheck(HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
Package aPackage = beanType.getPackage();
SkipMappingCheck packageSkip = aPackage.getAnnotation(SkipMappingCheck.class);
if (null != packageSkip) {
return true;
}
SkipMappingCheck classSkip = beanType.getAnnotation(SkipMappingCheck.class);
if (null != classSkip) {
return true;
}
Method method = handlerMethod.getMethod();
SkipMappingCheck methodSkip = method.getAnnotation(SkipMappingCheck.class);
return null != methodSkip;
}
}
public enum MappingLevel implements Matcher {
MODULE("模块"),
CONTROLLER("控制类"),
METHOD("接口方法");
private String mappingName;
public String level() {
return this.name().toLowerCase();
}
@Override
public Object statusCode() {
return level();
}
}
code生成器
public class PermissionMappingGenerator {
private final static Map<String, String> PACKAGE_KEY_CACHE = new HashMap<>();
private final static Map<String, String> CONTROLLER_KEY_CACHE = new HashMap<>();
private final static Map<String, String> METHOD_KEY_CACHE = new HashMap<>();
public static String generateByLevel(HandlerMethod handlerMethod, MappingLevel mappingLevel) {
switch (mappingLevel) {
case METHOD:
return generateMethodKey(handlerMethod);
case CONTROLLER:
return generateControllerKey(handlerMethod);
case MODULE:
return generateModuleKey(handlerMethod);
default:
throw new LocalException("不能确定的权限级别");
}
}
public static String generateModuleKey(HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
Package aPackage = beanType.getPackage();
String packageName = aPackage.getName();
String cryptPackageCode = PACKAGE_KEY_CACHE.get(packageName);
if (ZYStrUtils.isNull(cryptPackageCode)) {
cryptPackageCode = ZYCryptUtils.encryptMD5(packageName).toLowerCase();
PACKAGE_KEY_CACHE.put(packageName, cryptPackageCode);
}
return cryptPackageCode;
}
public static String generateControllerKey(HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
String className = beanType.getName();
String cryptControllerCode = CONTROLLER_KEY_CACHE.get(className);
if (ZYStrUtils.isNull(cryptControllerCode)) {
cryptControllerCode = ZYCryptUtils.encryptMD5(className).toLowerCase();
CONTROLLER_KEY_CACHE.put(className, cryptControllerCode);
}
return cryptControllerCode;
}
public static String generateMethodKey(HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
List<String> keySign = new ArrayList<>();
keySign.add(beanType.getSimpleName());
keySign.add(method.getName());
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
keySign.add(parameter.getName());
}
String key = StrUtils.join(keySign, "#");
String cryptKey = METHOD_KEY_CACHE.get(key);
if (null == cryptKey) {
cryptKey = CryptUtils.encryptMD5(key).toLowerCase();
METHOD_KEY_CACHE.put(key, cryptKey);
}
return cryptKey;
}
}
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SkipMappingCheck {
}
与传统的路径匹配接口权限检查相比,本方案采用HandleMethod信息与角角直接绑定。多了一个权限颗粒度划分。对减少接品权限数量及减少人工分配量有明显的优势。自动的权限生成工具省去了维护权限编号协删改查的人工操作。减去了大量人工。实际使用中,默认使用module级,维护量很少。较严情况下,使用controller级。严格安全等保要求的情况下。使用mothod级。方案选择很灵活。重启服务即可任意切换。可以使用SkipMappingCheck 注解对整个模块,整个controller进行跳过处理。