2026/2/19 2:13:49
网站建设
项目流程
长沙企业网站开发,百度关键词排名联系,广西南宁最新确诊名单,做网站前台用什么软件0. 背景
Spring的核心技术SpEL底层采用反射的方式获取对象属性、调用方法、创建对象等。如果不加以限制有非常大的安全漏洞。 如果访问权限过大,系统接收的字符串,很容易就执行恶意程序.比如在上一章 Spring使用el表达式 第一小节中执行的表达式T(Runtime).getRuntime().exec(…0. 背景Spring的核心技术SpEL底层采用反射的方式获取对象属性、调用方法、创建对象等。如果不加以限制有非常大的安全漏洞。如果访问权限过大,系统接收的字符串,很容易就执行恶意程序.比如在上一章 Spring使用el表达式 第一小节中执行的表达式T(Runtime).getRuntime().exec(calc)就轻松运行了windows的计算器。Spring 默认提供了两个contextStandardEvaluationContext: 默认的context,可以访问任意对象属性、调用任意对象方法、创建任意对象SimpleEvaluationContext: 功能首先的上下文,可以快速限制部分能力,要想安全控制并且想省事,可以直接使用这个.以下章节依次说明可以限制的地方以及实现方法.从开发角度来说可以是限制,也可以说是扩展1. 限制类在引擎加载具体字节码时,可以通过自定义TypeLocator限制某些类的加载.比如以下代码,实现一个白名单功能,为了针对性的处理,这个类直接从StandardTypeLocator扩展.然后重写findType方法public class LimtClassTypeLocator extends StandardTypeLocator { SetString whiteClassSet new HashSet(); public LimtClassTypeLocator(String... className) { whiteClassSet new HashSet(Arrays.asList(className)); } Override public Class? findType(String typeName) throws EvaluationException { if (this.whiteClassSet.contains(typeName)) { return super.findType(typeName); } throw new EvaluationException(类名: typeName 不允许调用); } }然后我们需依次测试一下几种情况使用T(ClassName)语法测试白名单之内和之外的class使用new ClassName()语法测试白名单之内和之外的class测试inline list语法 {1,2,3}测试inline map语法 {1:a,2:b}测试代码中可以访问的class设置为org.apache.commons.lang.StringUtils,测试代码如下public void testLimtClass() { ExpressionParser elParser new SpelExpressionParser(); StandardEvaluationContext ctx new StandardEvaluationContext(); ctx.setTypeLocator(new LimtClassTypeLocator(org.apache.commons.lang.StringUtils)); System.out.println(TypeLocator: 测试白名单的class); String whiteExpr T(org.apache.commons.lang.StringUtils).substring(小游戏 地心侠士,4); Object whiteValue elParser.parseExpression(whiteExpr).getValue(ctx); System.out.println(whiteValue); System.out.println(TypeLocator: 测试非法的class); try { String illegalClass T(org.apache.commons.lang.StringEscapeUtils).escapeHtml(小游戏 地心侠士); Object illegalValue elParser.parseExpression(illegalClass).getValue(ctx); } catch (EvaluationException e) { System.out.println(e.getMessage()); } System.out.println(TypeLocator: 测试非法构造函数); String cotrExpr new java.text.SimpleDateFormat(yyyy-MM-dd HH:mm:ss); try { SimpleDateFormat df (SimpleDateFormat) elParser.parseExpression(cotrExpr).getValue(ctx); System.out.println(当前时间: df.format(new Date())); } catch (EvaluationException e) { System.out.println(e.getMessage()); } System.out.println(TypeLocator: 测试拦截内联list); String initList {小游戏,地心侠士}; // java.util.Collections$UnmodifiableRandomAccessList? List inlineLst elParser.parseExpression(initList).getValue(ctx, List.class); inlineLst.forEach(System.out::println); System.out.println(TypeLocator: 测试拦截内联Map); //java.util.Collections$UnmodifiableMap?, ? String initMap {gameType:小游戏,gameName:地心侠士}; Map inlineMap elParser.parseExpression(initMap).getValue(ctx, Map.class); inlineMap.forEach((k, v) - System.out.println(k : v)); }测试允许结果如下TypeLocator: 测试白名单的class 地心侠士 TypeLocator: 测试非法的class 类名: org.apache.commons.lang.StringEscapeUtils不允许调用 TypeLocator: 测试非法构造函数 EL1003E: A problem occurred whilst attempting to construct an object of type java.text.SimpleDateFormat using arguments (java.lang.String) TypeLocator: 测试拦截内联list 小游戏 地心侠士 TypeLocator: 测试拦截内联Map gameType : 小游戏 gameName : 地心侠士从测试结果可以看LimtClassTypeLocator有效拦截了非法的静态方法调用以及非法的构造函数调用.但其中有一点,内联的list和map 拦截失败了关键代码:ctx.setTypeLocator(new LimtClassTypeLocator(org.apache.commons.lang.StringUtils))2. 限制属性spel引擎中,可以直接对对象属性进行读写操作.要限制属性读和写,需要通过实现PropertyAccessor接口.该接口提供4个方法,依次是两个读写判断,以及读写操作.如果需要对属性值特殊处理,也可以通过此能力实现.比如把电话号中间几位改成星号.LimitPropertyAccessors这个类,不允许读的属性为gameType,不允许写的属性为gameName.具体代码如下public class LimitPropertyAccessors extends ReflectivePropertyAccessor { Override public boolean canRead(EvaluationContext context, Nullable Object target, String name) throws AccessException { if (gameType.equals(name)) { throw new AccessException(gameType属性不允许访问); } return super.canRead(context, target, name); } Override public boolean canWrite(EvaluationContext context, Nullable Object target, String name) throws AccessException { if (gameName.equals(name)) { throw new AccessException(gameName 属性不允许赋值); } return super.canWrite(context, target, name); } }属性读测试代码如下:public void testLimitReadProperty() { ElTestObject testObject new ElTestObject(小游戏, 地心侠士); StandardEvaluationContext ctx new StandardEvaluationContext(); ctx.setPropertyAccessors(Arrays.asList(new LimitPropertyAccessors())); ExpressionParser elParser new SpelExpressionParser(); ctx.setVariable(var, testObject); String allowProp #var.gameName; System.out.println(测试允许访问的属性 gameName); Object value elParser.parseExpression(allowProp).getValue(ctx); System.out.println(获取成功: value); String disallowProp #var.gameType; System.out.println(测试禁止访问的属性 gameType ); try { value elParser.parseExpression(disallowProp).getValue(ctx); System.out.println(value); } catch (Exception e) { System.out.println(属性访问失败: e.getMessage()); } }运行结果如下:测试允许访问的属性 gameName 获取成功: 地心侠士 测试禁止访问的属性 gameType 属性访问失败: EL1021E: A problem occurred whilst attempting to access the property gameType: gameType属性不允许访问属性写测试代码如下:public void testLimitWriteProperty() { ElTestObject testObject new ElTestObject(小游戏, 地心侠士); System.out.println(对象原始值); System.out.println(testObject.toString()); StandardEvaluationContext ctx new StandardEvaluationContext(); ctx.setPropertyAccessors(Arrays.asList(new LimitPropertyAccessors())); ExpressionParser elParser new SpelExpressionParser(); ctx.setVariable(var, testObject); String allowWriteProp #var.gameType小游戏 666; elParser.parseExpression(allowWriteProp).getValue(ctx); System.out.println(测试可以赋值属性 gameType ); System.out.println(testObject.toString()); System.out.println(测试不可以访问属性 gameName ); String disAllowWriteProp #var.gameName地心侠士 666; try { elParser.parseExpression(disAllowWriteProp).getValue(ctx); } catch (Exception e) { System.out.println(属性赋值失败: e.getMessage()); } }运行结果如下对象原始值 ElTestObject [gameType小游戏, gameName地心侠士] 测试可以赋值属性 gameType ElTestObject [gameType小游戏 666, gameName地心侠士] 测试不可以访问属性 gameName 属性赋值失败: EL1034E: A problem occurred whilst attempting to set the property gameName: gameName 属性不允许赋值关键代码:ctx.setPropertyAccessors(Arrays.asList(new LimitPropertyAccessors()));3. 限制方法限制某些方法不能调用,需要实现MethodFilter接口.通过filter方法返回可以调用的方法.该接口定义为FunctionalInterface可以直接使用lambda表达式实现.测试代码如下:public void testLimitMethod() { ElTestObject testObject new ElTestObject(小游戏, 地心侠士); StandardEvaluationContext ctx new StandardEvaluationContext(); // 设置成只能调用 setGameName 方法 ctx.registerMethodFilter(ElTestObject.class, method - { return method.stream().filter(m - m.getName().equals(getGameName)).toList(); }); ctx.setVariable(var, testObject); String limitMethod #var.getGameType(); System.out.println(调用getGameType); ExpressionParser elParser new SpelExpressionParser(); try { Object limitValue elParser.parseExpression(limitMethod).getValue(ctx); System.out.println(limitValue); } catch (Exception e) { System.out.println(调用getGameType失败: e.getMessage()); } System.out.println(调用getGameName); String allowMethod #var.getGameName(); Object allowValue elParser.parseExpression(allowMethod).getValue(ctx); System.out.println(获取成功: allowValue); }测试结果如下调用getGameType 调用getGameType失败:EL1004E: Method call: Method getGameType() cannot be found on type com.herbert.script.SpringElScriptSafeAndExtend$ElTestObject 调用getGameName 获取成功: 地心侠士关键代码:ctx.registerMethodFilter(ElTestObject.class, method-method)4. 限制Bean引擎使用beanName语法,可以访问对应bean,针对一些特殊存在的bean,可以限制使用,这里需要实现接口BeanResolver自定义一个BeanResolver.代码如下:public class LimtBeanResolver implements BeanResolver { ElTestObject testObject new ElTestObject(小游戏, 地心侠士); Override public Object resolve(EvaluationContext context, String beanName) throws AccessException { if (game.equals(beanName)) { return testObject; } return new String(不允许访问的bean:[ beanName ]); } }从代码可知,如果传递的game会返回实例testObject,其他则返回不允许访问的bean:[ beanName ]测试代码如下public void limitBean() { ExpressionParser elParser new SpelExpressionParser(); StandardEvaluationContext ctx new StandardEvaluationContext(); // ctx.setBeanResolver(new BeanFactoryResolver((BeanFactory) applicationContext)); ctx.setBeanResolver(new LimtBeanResolver()); Object value elParser.parseExpression(game).getValue(ctx); System.out.println(测试允许访问的bean); System.out.println(value); value elParser.parseExpression(other).getValue(ctx); System.out.println(测试不允许访问的bean); System.out.println(value); }运行结果如下测试允许访问的bean ElTestObject [gameType小游戏, gameName地心侠士] 测试不允许访问的bean 不允许访问的bean:[other]关键代码:ctx.setBeanResolver(new LimtBeanResolver());5. 限制内容有时需要对应脚本中的参数内容做一些特殊处理,这时就需要通过TypeConverter对一些值做一些特殊处理.除此之外还可以通过PropertyAccessors实现.接下来,我们实现一个TypeConverter,主要功能是把参数中的666替换成999,代码如下public class LimtTypeConvert extends StandardTypeConverter { Override public Nullable Object convertValue(Nullable Object value, Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { if (value.equals(666)) { value 999; } return super.convertValue(value, sourceType, targetType); } }测试代码如下public void testLimtValue() { String initList 小游戏 地心侠士 666 ; ExpressionParser elParser new SpelExpressionParser(); StandardEvaluationContext ctx new StandardEvaluationContext(); ctx.setTypeConverter(new LimtTypeConvert()); Object expValue elParser.parseExpression(initList).getValue(ctx); System.out.println(使用typeconvert,将666变成999); System.out.println(expValue); }运行结果如下:使用typeconvert,将666变成999 小游戏 地心侠士999关键代码:ctx.setTypeConverter(new LimtTypeConvert());6. 扩展函数引擎中的函数扩展,实际就是把函数作为一个变量放到ctx中,然后通过访问对象的方式调用该函数.测试代码如下public void testExtendFunction() throws NoSuchMethodException, SecurityException { ElTestObject testObject new ElTestObject(小游戏, 地心侠士); ExpressionParser elParser new SpelExpressionParser(); StandardEvaluationContext ctx new StandardEvaluationContext(); Method method ElTestObject.class.getMethod(joinString, String.class, String.class); // 内部调用 setVariable ctx.registerFunction(joinString, method); ctx.setVariable(var, testObject); ctx.setVariable(method, method); String extendMethod #joinString(#var.gameType,#var.gameName); System.out.println(测试扩展方法(registerFunction)); Object value elParser.parseExpression(extendMethod).getValue(ctx); System.out.println(value); extendMethod #method(#var.gameType,#var.gameName); System.out.println(测试扩展方法(setVariable)); value elParser.parseExpression(extendMethod).getValue(ctx); System.out.println(value); }运行结果如下:测试扩展方法(registerFunction) 注册方法调用成功: 小游戏 : 地心侠士 测试扩展方法(setVariable) 注册方法调用成功: 小游戏 : 地心侠士关键代码:ctx.registerFunction(joinString, method);7. 操作符重写操作符重写,只能重写部分数字相关的操作.并且操作符两边,至少有一边是数字才行.需要是想接口OperatorOverloader,我们这实现一个对象数字的功能public class AddExtendStringToObj implements OperatorOverloader { Override public boolean overridesOperation(Operation operation, Nullable Object leftOperand, Nullable Object rightOperand) throws EvaluationException { if (operation Operation.ADD leftOperand instanceof ElTestObject NumberUtils.isNumber(rightOperand.toString())) { return true; } return false; } Override public Object operate(Operation operation, Nullable Object leftOperand, Nullable Object rightOperand) throws EvaluationException { ElTestObject left (ElTestObject) leftOperand; left.setGameName(left.getGameName() rightOperand.toString()); return leftOperand; } }测试代码如下:public void testExtendOperator() { ElTestObject testObject new ElTestObject(小游戏, 地心侠士); ExpressionParser elParser new SpelExpressionParser(); StandardEvaluationContext ctx new StandardEvaluationContext(); ctx.setOperatorOverloader(new AddExtendStringToObj()); ctx.setVariable(var, testObject); String el #var 666; Object value elParser.parseExpression(el).getValue(ctx); System.out.println(测试操作符重写); System.out.println(value); }运行结果如下:测试操作符重写 ElTestObject [gameType小游戏, gameName地心侠士666]从测试结果可以看出 666 的数字被添加到对象gameName中.关键代码:ctx.setOperatorOverloader(new AddExtendStringToObj());8. SimpleEvaluationContext 使用SimpleEvaluationContext是一个构造模式的上下文,需要使用build构造具体功能的上下文.快速实现一个只读的上下文测试代码如下:public void testOnlyRead() { ElTestObject testObject new ElTestObject(小游戏, 地心侠士); // 不会主动注册 MethodResolver 不能访问方法 SimpleEvaluationContext safeContxt SimpleEvaluationContext.forReadOnlyDataBinding().build(); safeContxt SimpleEvaluationContext.forReadOnlyDataBinding(). build(); safeContxt.setVariable(var, testObject); System.out.println(测试只读模式 读取值); String readExpr #var.gameName; ExpressionParser elParser new SpelExpressionParser(); Object readValue elParser.parseExpression(readExpr).getValue(safeContxt); System.out.println(readValue); System.out.println(测试只读模式 修改值); String wirteExpr #var.gameName地心侠士 666; try { elParser.parseExpression(wirteExpr).getValue(safeContxt); System.out.println(属性修改成功); } catch (Exception e) { System.out.println(属性内容修改失败: e.getMessage()); } System.out.println(测试安全模式 调用方法); String elMethod #var.getGameName(); try { elParser.parseExpression(elMethod).getValue(safeContxt); } catch (Exception e) { System.out.println(方法调用失败: e.getMessage()); } System.out.println(testObject.toString()); }运行结果如下:测试只读模式 读取值 地心侠士 测试只读模式 修改值 属性内容修改失败:EL1068E: The expression component #var.gameName地心侠士 666 is not assignable 测试安全模式 调用方法 方法调用失败:EL1004E: Method call: Method getGameName() cannot be found on type com.herbert.script.SpringElScriptSafeAndExtend$ElTestObject ElTestObject [gameType小游戏, gameName地心侠士]从测试结果可以知道SimpleEvaluationContext需要在代码明确指定可访问的属性,比如上边的测试代码没有调用withMethodResolvers就不能调用对象方法.关键代码:SimpleEvaluationContext.forReadOnlyDataBinding().build()9. 总结需要限制引擎能力,主要需要主要从类,属性,方法,内容层面限制.限制类 :org.springframework.expression.TypeLocator限制属性:org.springframework.expression.PropertyAccessor限制方法:org.springframework.expression.MethodResolver限制内容:org.springframework.expression.TypeConverter限制bean:org.springframework.expression.BeanResolver扩展主要体现在扩展方法操作符重写扩展索引访问扩展方法:ctx.registerFunction(String, Method)操作符重写:org.springframework.expression.OperatorOverloader扩展索引访问:org.springframework.expression.IndexAccessor