Java代码安全设计-设计模式-装饰模式

Java设计模式-装饰模式(Decorate)

目录

  • 为什么要使用装饰模式
  • 什么是装饰模式
  • JavaSE装饰模式的应用
  • Struts2装饰模式的应用

1、装饰模式是为了解决类功能扩展,而不增加类与子类的耦合性。

2、不要让自己停留在审计OWASP TOP10漏洞的阶段,赶紧突破

一、为什么要使用装饰模式

1.1 使用原生的HttpServlet,实现对所有HTTP请求的参数过滤如何做?

我们知道Servlet可以在doGet、doPost中方法中接收HTTP的参数,如果要过滤参数,那么肯定要在doGet与doPost中处理,在每个doGet与doPost都做修改,会带来哪些问题?

  1. 重复代码过多
  2. 代码扩展性差

如果要修改需求,比如增加过滤的特殊字符,或者修改过滤的逻辑,都会引起所有过滤代码的修改,所以要用装饰模式解决问题(要彻底解决问题需要装饰模式+Filter,本文不体现Filter)。

1.2 实现过滤HTTP参数的功能

该项目复用【JSPServlet】项目代码,代码如下(只展示了核心代码)

SafeLogin.java :登录业务

MyHttpServletRequest:扩展的HttpServletRequestWrapper装饰类

web.xml :定义新增的SafeLogin类

public class SafeLogin extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 使用自定义的HttpServletRequest的装饰类,里面定义了参数的过滤方法
        MyHttpServletRequest safeRequest = new MyHttpServletRequest(request);
        // 这里调用的getParameter,是MyHttpServletRequest的getParameter方法,定义了过滤参数的方法
        String userName = safeRequest.getParameter("username");
        String password = safeRequest.getParameter("password");

        System.out.println("过滤后 username:" + userName);
        System.out.println("过滤后 password:" + password);
    }
}
public class MyHttpServletRequest extends HttpServletRequestWrapper {
    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request
     * @throws IllegalArgumentException if the request is null
     */
    public MyHttpServletRequest(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        // 调用父类的getParameter方法,也就是原生的HttpServletRequest
        String value = super.getParameter(name);
        System.out.println("过滤前 " + name + ":" + value);
        value = value.replace("'","");
        return value;
    }
}
<servlet>
    <servlet-name>safeLogin</servlet-name>
    <servlet-class>org.example.SafeLogin</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>safeLogin</servlet-name>
    <url-pattern>/safeLogin</url-pattern>
</servlet-mapping>

可以发现在SafeLogin中,使用了自定义的MyHttpServletRequest装饰类,该类继承了HttpServletRequestWrapper,所以,想要扩展HttpServletRequest,就需要继承HttpServletRequestWrapper类,有这样的好处:

  1. 对原始类HttpServletRequest没有任何侵入性,不必对原始类做任何修改
  2. 通过继承装饰类实现对原始类的装饰,扩展性好,也可以扩展多个装饰类,例如:过滤SQL字符、过滤XSS特殊字符等

对于初学者会有这样的疑问:

  1. 什么是装饰模式
  2. 为什么要继承HttpServletRequestWrapper,而不是HttpServletRequest
  3. HttpServletRequest与HttpServletRequestWrapper是什么关系

对于前两个问题我们在本文的后面小结解答,先看第三个问题

下面是关于HttpServletRequest的类UML图

image-20230201112535142

根据装饰模式的特点,将HttpServletRequest的装饰模式分解如下(这是装饰模式的关键点,现在看不明白没关系,下面文章会详细讲解)

  1. 抽象组件(Component) :定义装饰方法的规范 – ServletRequest类
  2. 被装饰者(ConcreteComponent) :Component的具体实现,也就是我们要装饰的具体对象 – HttpServletRequest接口
  3. 装饰者组件(Decorator) :持有组件(Component)对象的实例引用,该类的职责就是为了装饰具体组件对象,定义的规范 – ServletRequestWrapper类
  4. 具体装饰(ConcreteDecorator) :负责给构件对象装饰附加的功能。-- HttpServletRequestWrapper、MyServletRequest

了解完后,HttpServletRequest与HttpServletRequestWrapper是什么关系的问题迎刃而解,HttpServletRequestWrapper是HttpServletRequest的装饰类,通过装饰类扩展HttpServletRequest的功能。

为什么要继承HttpServletRequestWrapper,而不是HttpServletRequest?

一定要继承HttpServletRequestWrapper吗?其实不是,通过类的UML图可以看出,继承HttpServletRequestWrapper和继承ServletRequestWrapper是一样的,两者都是HttpServletRequest的装饰类。

最后一个问题,什么是装饰模式?什么情况下使用装饰模式?

  1. 你不希望修改原类,但想动态扩展原类功能,也可动态撤销
  2. 使用继承扩展对象行为不可行,继承是扩展对象功能最直接的方式,但代码耦合太强,而且最后不用继承层次太深
  3. 被final限制的类

二、什么是装饰模式

首先说一个开发场景

1月15日:产品经理找到你说:开发一个饮料售卖平台,前期只卖水

2月15日:产品经理又找你,说想卖可乐、芬达

3月1日:又找你,想可乐加冰

4月1日:又找你,想可乐加柠檬

可以看出产品的需求是多变的,各类设计模式的出现,解决的就是产品需求多变的问题而导致的代码重用性、耦合性、可维护性问题

比如第一次需求,我们只需要声明一个Water类就能完成需求

/**
 * 可乐
 */
public class Water {
    // 可乐的价格
    final int COLA_PRICE = 5;
    public int price() {
        System.out.println("可乐需要 " + COLA_PRICE + " 元");
        return COLA_PRICE;
    }
}

而后面的可乐、芬达、加冰、加柠檬的需求,可以在定义4个类,分别满足需求,但需求是多变的,假如某天可乐中要加100种调料,难道要写100个类?每个类中都写price方法?如果客户想添加3中调料呢?那可能会想那在类中定义3个调料的价格计算就行了,但你要知道,用户加的调料是不同的,难道要写100个调料的排列组合?这简直不可能,因为会造成2个问题:

  1. 每个类都有计算可乐+调料的方法,造成重复代码多
  2. 如果增加/删除一种调料、修改调料价格,那么所有涉及到的类都要改

所以就要想办法解决上面的问题,如何更好的给可乐增加调料?

直接看代码吧,代码结构如下

image-20230116162613427

Water.java

package org.decorate.drinks;

/**
 * 最终要装饰的类,装饰为不同的饮料
 */
public interface Water {
    /**
     * 饮料的价格
     * @return
     */
    public int price();
}

drinks.Cola.java

package org.decorate.drinks.drinks;

import org.decorate.drinks.Water;

/**
 * 可乐
 */
public class Cola implements Water {
    // 可乐的价格
    final int COLA_PRICE = 5;
    public int price() {
        System.out.println("可乐需要 " + COLA_PRICE + " 元");
        return COLA_PRICE;
    }
}

drinks.Fanta.java

package org.decorate.drinks.drinks;

import org.decorate.drinks.Water;

/**
 * 芬达饮料
 */
public class Fanta implements Water {
    // 芬达的价格
    final int FANTA_PRICE = 3;
    public int price() {
        System.out.println("芬达需要 " + FANTA_PRICE + " 元");
        return FANTA_PRICE;
    }
}

decorate.LceDecorate.java

package org.decorate.drinks.decorate;

import org.decorate.drinks.Water;
/**
 * 加冰
 */
public class IceDecorate implements Water {

    private Water water = null;
    private int ICE_PRICE = 1;

    public IceDecorate(Water water) {
        this.water = water;
    }

    public int price() {
        // 水的价格 + 冰的价格
        System.out.println("加冰需要 " + ICE_PRICE + " 元");
        return this.water.price() + ICE_PRICE;
    }
}

decorate.LemonDecorate.java

package org.decorate.drinks.decorate;

import org.decorate.drinks.Water;
/**
 * 加柠檬
 */
public class LemonDecorate implements Water {
    private Water water = null;
    private int LEMON_PRICE = 2;

    public LemonDecorate(Water water) {
        this.water = water;
    }

    public int price() {
        System.out.println("加柠檬需要 " + LEMON_PRICE + " 元");
        // 水的价格 + 柠檬的价格
        return this.water.price() + LEMON_PRICE;
    }
}

最终如何调用

package org.decorate.drinks;

import org.decorate.drinks.decorate.IceDecorate;
import org.decorate.drinks.decorate.LemonDecorate;
import org.decorate.drinks.drinks.Cola;

public class Customer {
    public static void main(String[] args) {
        // 客户要可乐加冰加柠檬
        Water water = new Cola();
        water = new IceDecorate(water);
        water = new LemonDecorate(water);
        int price = water.price();
        System.out.println("顾客需要支付 " + price + "元");

    }
}

在Customer类中选择饮料时,可以发现无论我如何选择调料,都不需要改其它类,只需要修改调用的代码即可,非常方便,哪怕后来多了其它饮料的种类,想加冰加柠檬都是可以直接调用的,这就是装饰模式的好处

我们抽象下装饰模式的基本特性

  1. 抽象构建Component(接口or抽象类 – Water)
  2. 抽象构建的实现类(主要是对这个类进行装饰 – Cola、Fanta)
  3. 具体的装饰类(你要装饰的代码类 – IceDecorate、LemonDecorate ,属性里必然有一个private变量指向Component抽象构(Water))。

类的关系图如下

image-20230421165615627

也可以将装饰类抽象为抽象方法,形成如下图

image-20230421165553600

再次抽象下装饰模式的基本特性

  1. 抽象组件(Component) :定义装饰方法的规范 – Water类
  2. 被装饰者(ConcreteComponent) :Component的具体实现,也就是我们要装饰的具体对象 – Cola、Fanta类
  3. 装饰者组件(Decorator) :持有组件(Component)对象的实例引用,该类的职责就是为了装饰具体组件对象,定义的规范 – Decorator类
  4. 具体装饰(ConcreteDecorator) :负责给构件对象装饰附加的功能。-- IceDecorator、lemonDecorator

看下具体代码,其中Water.java、Cola.java、Fantta.java类与前面一样,主要看下IceDecorator、lemonDecorator、Decorator类

Water.java

package org.decorate.drinks;

/**
 * 最终要装饰的类,装饰为不同的饮料
 */
public interface Water {
    /**
     * 饮料的价格
     * @return
     */
    public int price();
}

drinks.Cola.java

package org.decorate.drinks.drinks;

import org.decorate.drinks.Water;

/**
 * 可乐
 */
public class Cola implements Water {
    // 可乐的价格
    final int COLA_PRICE = 5;
    public int price() {
        System.out.println("可乐需要 " + COLA_PRICE + " 元");
        return COLA_PRICE;
    }
}

drinks.Fanta.java

package org.decorate.drinks.drinks;

import org.decorate.drinks.Water;

/**
 * 芬达饮料
 */
public class Fanta implements Water {
    // 芬达的价格
    final int FANTA_PRICE = 3;
    public int price() {
        System.out.println("芬达需要 " + FANTA_PRICE + " 元");
        return FANTA_PRICE;
    }
}

Decorator.java

package org.decorate.drinks2.decorate;

import org.decorate.drinks2.Water;

/**
 * 装饰抽象类
 */
public abstract class Decorate implements Water {
    private Water water = null;
    public Decorate(Water water){
        this.water = water;
    }
    public int price() {
        return this.water.price();
    }
}

decorate.IceDecorator.java

package org.decorate.drinks2.decorate;

import org.decorate.drinks2.Water;

/**
 * 加冰
 */
public class IceDecorate extends Decorate{
    final private int  ICE_PRICE = 1;
    public IceDecorate(Water water) {
        super(water);
    }

    public int price() {
        // 水的价格 + 冰的价格
        System.out.println("加冰需要 " + ICE_PRICE + " 元");
        return super.price() + ICE_PRICE;
    }
}

decorate.LemonDecorator.java

package org.decorate.drinks2.decorate;

import org.decorate.drinks2.Water;

/**
 * 加柠檬
 */
public class LemonDecorate extends Decorate{
    final private int LEMON_PRICE = 1;
    public LemonDecorate(Water water) {
        super(water);
    }

    public int price() {
        // 水的价格 + 柠檬的价格
        System.out.println("加柠檬需要 " + LEMON_PRICE + " 元");
        return super.price() + LEMON_PRICE;
    }
}

对比下两种实现方式,可以发现后面一种的类关系更加清晰,而且每个具体的装饰类中都减少了Water类的引用,减少了代码量,也减少了对象的生成。

image-20230116174437409

三、JavaSE装饰模式的应用

JavaSE中对装饰模式的应用,最典型的就是IO流,例如下面代码

static final String FILE = "test.log";
Reader reader = new InputStreamReader(new FileInputStream(FILE));

在大学中学习Java时,只知道这句话是创建了InputStreamReader对象,构造方法传递了FileInputStream对象,这样就可以读取一个文件,但并不明白为什么这么做,也没有调试过JDK的源码,并不明白什么是设计模式,只知道如何去用。

到现在开始分析Java代码安全时,分析的越深入,发现很多设计模式如果不明白,就会限制自己。

这里想到了《凤凰项目一个IT运维的传奇故事》的一句话

创建约束理论的艾利高德拉特告诉我们,在瓶颈之外的任何地方做出的改进都是假象。

所以必须要解决瓶颈问题,也就是对设计模式的盲区,要不然自己会永远停留在审计OWASP TOP10的阶段,那将是职业生涯的灾难。

从前面的读文件代码,就是非常典型的装饰模式,首先先看三种不同的对文件读取的方式

package org.decorate;

import java.io.*;

public class JavaSEDecorate {
    static final String FILE = "test.log";
    public static void main(String[] args) throws IOException {
        test1();
    }
    public static void test1() throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(FILE));
        String str;
        while ((str = in.readLine()) != null) {
            System.out.println(str);
        }
        in.close();
    }

    public static void test2() throws IOException{
        InputStream in = new FileInputStream(FILE);
        int tempbyte;
        while ((tempbyte = in.read()) != -1) {
            System.out.write(tempbyte);
        }
        in.close();
    }

    public static void test3() throws IOException {
        char[] tempchars = new char[3];
        int charread = 0;
        Reader reader = new InputStreamReader(new FileInputStream(FILE));
        // 读入多个字符到字符数组中,charread为一次读取字符数
        while ((charread = reader.read(tempchars)) != -1) {
            System.out.print(tempchars);
        }
    }
}

以上面的三种方式的一个为例

BufferedReader in = new BufferedReader(new FileReader(FILE));

回忆下前面关于装饰模式的四个关键点,但这个只用到了三个关键点的模式,对应到BufferedReader类中

  1. 抽象组件(Component) :定义装饰方法的规范(FileReader与BufferedReader的共同父类) – Reader类
  2. 被装饰者(ConcreteComponent) :Component的具体实现,也就是我们要装饰的具体对象 – FileReader类
  3. 具体装饰(ConcreteDecorator) :负责给构件对象装饰附加的功能,持有组件(Component)对象的实例引用Reader。-- BufferedReader

可以看到非常符合装饰模式的应用

FileReader封装StreamDecoder类封装FileInputStream类,最终BufferedReader.readLine方法,调用的是StreamDecoder.read()方法,只不过BufferedReader.readLine方法进行了字符串的封装处理

image-20230116155316235

为什么要这么嵌套呢?我们读文件时,比较自然的思路是逐行阅读,这是比较贴近人类的阅读方式,OK,我们看下FileReader类中有哪些方法,点一下下图标红按钮,可以看到父类继承的方法

image-20230116152243207

可以看到只有read方法,并没有我们想的readLine方法,在看下BufferedReader类的方法

image-20230116152448064

可以看到BufferedReader类中是有readLine方法,在FileReader类的基础上装饰了readLine方法

附一张IO流的继承关系图

img

通过上图可以看到IO流有字节流与字符流两类,而如何将字节转为字符流?答案是InputStreamReaderOutputStreamWriter,其中用都是装饰模式的设计方法

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File("text.txt"))));

image-20230116154032422

四、Struts2装饰模式的应用

  1. StrutsRequestWrapper
  2. OgnlContext
  3. OgnlTypeConverterWrapper
  4. OgnlNullhandlerWrapper

首先StrutsRequestWrapper扩展的是HttpServletRequestWrapper类,和我们前面自定义的MyServletRequest是相同道理,不在赘述

OgnlContext类代码如下,扩展的是Map类,内部持有一个Map对象

public class OgnlContext extends Object implements Map
{    
    private static Map RESERVED_KEYS = new HashMap(11);        
    private Map _values = new HashMap(23);
    private int _localReferenceCounter = 0;
    private Map _localReferenceMap = null;
    public OgnlContext(Map values)
    {
        super();
        this._values = values;
    }    
    public Map getValues()
    {
        return _values;
    }
    public void setValues(Map value) {
         for(Iterator it = value.keySet().iterator(); t.hasNext();) { 
         Object k = it.next();
          _values.put(k, value.get(k)); 
          } 
     } 
      //省略了代码...
}

类的关系图如下

image-20230201161740190

  1. 抽象组件(Component) :定义装饰方法的规范(FileReader与BufferedReader的共同父类) – Object类
  2. 被装饰者(ConcreteComponent) :Component的具体实现,也就是我们要装饰的具体对象 – Map接口
  3. 具体装饰(ConcreteDecorator) :负责给构件对象装饰附加的功能,持有组件(Component)对象的实例引用Map。-- OgnlContext

OgnlTypeConverterWrapper类的解析如下

  1. 抽象组件(Component) :TypeConverter接口
  2. 被装饰者(ConcreteComponent) :DefaultTypeConverter类
  3. 具体装饰(ConcreteDecorator) :OgnlTypeConverterWrapper类、XWorkConverter(Struts中的装饰类)
public interface TypeConverter
{
    public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType);
}
public class OgnlTypeConverterWrapper implements ognl.TypeConverter {  //接口实现是ognl接口
 
    private TypeConverter typeConverter;//实际传参是Struts自己的接口内
    
    public OgnlTypeConverterWrapper(TypeConverter conv) {
        if (conv == null) {
            throw new IllegalArgumentException("Wrapped type converter cannot be null");
        }
        this.typeConverter = conv;
    }
    
    public Object convertValue(Map context, Object target, Member member,    //他们有完全一样的方法
            String propertyName, Object value, Class toType) {
        return typeConverter.convertValue(context, target, member, propertyName, value, toType);
    }
    
    public TypeConverter getTarget() {
        return typeConverter;
    }
}
public class DefaultTypeConverter implements TypeConverter {
 
    public Object convertValue(Map<String, Object> context, Object target, Member member,
            String propertyName, Object value, Class toType) {
        return convertValue(context, value, toType);
    }
    
    public TypeConverter getTypeConverter( Map<String, Object> context )
    {
        Object obj = context.get(TypeConverter.TYPE_CONVERTER_CONTEXT_KEY);
        if (obj instanceof TypeConverter) {
            return (TypeConverter) obj;
            
        // for backwards-compatibility
        } else if (obj instanceof ognl.TypeConverter) {
            return new XWorkTypeConverterWrapper((ognl.TypeConverter) obj);
        }
        return null; 
    }
 
    public Object convertValue(Object value, Class toType) {
        Object result = null;
 
        if (value != null) {
            /* If array -> array then convert components of array individually */
            if (value.getClass().isArray() && toType.isArray()) {
                Class componentType = toType.getComponentType();
........省略代码
public class XWorkConverter extends DefaultTypeConverter {
 
    protected HashMap<Class, Map<String, Object>> mappings = new HashMap<Class, Map<String, Object>>(); // action
    protected HashSet<Class> noMapping = new HashSet<Class>(); // action
    protected HashMap<String, TypeConverter> defaultMappings = new HashMap<String, TypeConverter>();  // non-action (eg. returned value)
    protected HashSet<String> unknownMappings = new HashSet<String>();     // non-action (eg. returned value)
 		
    // 要装饰的对象,持有组件(Component)对象的实例引用-满足要求
    private TypeConverter defaultTypeConverter;
    private ObjectFactory objectFactory;
    private FileManager fileManager;
    private boolean reloadingConfigs;
 
    protected XWorkConverter() {
    }
 
    @Inject
    public void setObjectFactory(ObjectFactory factory) {
        this.objectFactory = factory;
        // note: this file is deprecated
        loadConversionProperties("xwork-default-conversion.properties");
 
        loadConversionProperties("xwork-conversion.properties");
    }
 
    @Inject  //注入方法
    public void setDefaultTypeConverter(XWorkBasicConverter conv) {
        this.defaultTypeConverter = conv;
    }
 
    //省略许多方法和属性...
}

OgnlNullhandlerWrapper类的解析如下

  1. 抽象组件(Component) :NullHandler接口
  2. 被装饰者(ConcreteComponent) :ObjectNullHandler类
  3. 具体装饰(ConcreteDecorator) :OgnlNullHandlerWrapper类
public interface NullHandler
{
    public Object nullMethodResult(Map context, Object target, String methodName, Object[] args);
 
    public Object nullPropertyValue(Map context, Object target, Object property);
}
public class ObjectNullHandler implements NullHandler
{
    /* NullHandler interface */
    public Object nullMethodResult(Map context, Object target, String methodName, Object[] args)
    {
        return null;
    }
 
    public Object nullPropertyValue(Map context, Object target, Object property)
    {
        return null;
    }
}
public class OgnlNullHandlerWrapper implements ognl.NullHandler {
 
    // 持有的被装饰的对象
    private NullHandler wrapped;
    
    public OgnlNullHandlerWrapper(NullHandler target) {
        this.wrapped = target;
    }
    
    public Object nullMethodResult(Map context, Object target,
            String methodName, Object[] args) {
        return wrapped.nullMethodResult(context, target, methodName, args);
    }
 
    public Object nullPropertyValue(Map context, Object target, Object property) {
        return wrapped.nullPropertyValue(context, target, property);
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MarginSelf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值