<audio id="audio" title="36 | 继承有什么安全缺陷？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/93/03d1c1cd441504b06a53bef54fe21293.mp3"></audio>

有时候，为了解决一个问题，我们需要一个解决办法。可是，这个办法本身还会带来更多的问题。新问题的解决带来更新的问题，就这样周而复始，绵延不绝。

比如[上一篇文章](https://time.geekbang.org/column/article/87256)[中](https://time.geekbang.org/column/article/87256)，我们说到的敏感信息通过异常信息泄露的问题，就是面向对象设计和实现给我们带来的小困扰。再比如[前面还有](https://time.geekbang.org/column/article/86590)[一个](https://time.geekbang.org/column/article/86590)[案例](https://time.geekbang.org/column/article/86590)，说到了共享内存或者缓存技术带来的潜在危害和挑战，这些都是成熟技术发展背后需要做出的小妥协。只是有时候，这些小小的妥协如果没有被安排好和处理好，可能就会带来不成比例的代价。

## 评审案例

我们一起来看一段节选的java.io.FilePermission类的定义。你知道为什么FilePermission被定义为final类吗？

```
package java.io;

// &lt;snipped&gt;
/**
 * This class represents access to a file or directory.  A
 * FilePermission consists of a pathname and a set of actions
 * valid for that pathname.
 * &lt;snipped&gt;
 */
public final class FilePermission
        extends Permission implements Serializable {
    /**
     * Creates a new FilePermission object with the specified actions.
     * &lt;i&gt;path&lt;/i&gt; is the pathname of a file or directory, and
     * &lt;i&gt;actions&lt;/i&gt; contains a comma-separated list of the desired
     * actions granted on the file or directory. Possible actions are
     * &quot;read&quot;, &quot;write&quot;, &quot;execute&quot;, &quot;delete&quot;, and &quot;readlink&quot;.
     * &lt;snipped&gt;
     */
    public FilePermission(String path, String actions);

    /**
     * Returns the &quot;canonical string representation&quot; of the actions.
     * That is, this method always returns present actions in the
     * following order: read, write, execute, delete, readlink. 
     * &lt;snipped&gt;
     */
    @Override
    public String getActions();

    /**
     * Checks if this FilePermission object &quot;implies&quot; the 
     * specified permission.
     * &lt;snipped&gt;
     * @param p the permission to check against.
     *
     * @return &lt;code&gt;true&lt;/code&gt; if the specified permission
     *         is not &lt;code&gt;null&lt;/code&gt; and is implied by this
     *         object, &lt;code&gt;false&lt;/code&gt; otherwise.
     */
    @Override
    public boolean implies(Permission p);

    // &lt;snipped&gt;
}

```

FilePermission被声明为final，也就意味着该类不能被继承，不能被扩展了。我们都知道，在面向对象的设计中，是否具备可扩展性是一个衡量设计优劣的好指标。如果允许扩展的话，那么想要增加一个“link”的操作就会方便很多，只要扩展FilePermission类就可以了。 但是对于FilePermission这个类，OpenJDK为什么放弃了可扩展性？

## 案例分析

如果我们保留FilePermission的可扩展性，你来评审一下下面的代码，可以看出这段代码的问题吗？

```
package com.example;

public final class MyFilePermission extends FilePermission {
    @Override
    public String getActions() {
      return &quot;read&quot;;
    }

    @Override
    public boolean implies(Permission p) {
      return true;
    }  
}

```

如果你还没有找出这个问题，可能是因为我还遗漏了对FilePermission常见使用场景的介绍。在Java的安全管理模式下，一个用户通常可能会被授予有限的权限。 比如用户“xuelei”可以读取用户“duke”的文件，但不能更改用户“duke”的文件。

授权的策咯可能看起来像下面的描述：

```
grant Principal com.sun.security.auth.UnixPrincipal &quot;xuelei&quot; {
    permission com.example.MyFilePermission &quot;/home/duke&quot;, &quot;read&quot;;
};

```

这项策略要想起作用，上面的描述就要转换成一个MyFilePermission的实例。然后调用该实例的implies()方法类判断是否可以授权一项操作。

```
Permission myPermission = ...  // read &quot;/home/duke&quot;

public void checkRead() {
  if (myPermission.implies(New FilePermission(file, &quot;read&quot;))) {
    // read is allowed.
  } else {
    // throw exception, read is not allowed.
  }
}

public void checkWrite() {
  if (myPermission.implies(New FilePermission(file, &quot;write&quot;))) {
    // writeis allowed.
  } else {
    // throw exception, write is not allowed.
  }  
}

```

这里请注意，MyFilePermission.implies()总是返回“true”， 所以上述的checkRead()和checkWrite()方法总是成功的，不管用户被明确指示授予了什么权限，实际上暗地里他已经被授予了所有权限。这就成功地绕过了Java的安全管理。

能够绕过Java安全机制的主要原因，在于我们允许了FilePermission的扩展。而扩展类的实现，有可能有意或者无意地改变了FilePermission的规范和运行，从而带来不可预料的行为。

如果你关注OpenJDK安全组的代码评审邮件组，你可能会注意到，对于面向对象的可扩展性这一便利和诱惑，很多工程师能够保持住克制。

保持克制，可能会遗漏一两颗看似近在眼前的甜甜的糖果，但可以减轻你对未来长期的担忧。

一个类或者方法如果使用了final关键字，我们可以稍微放宽心。如果没有使用final关键字，我们可能需要反复揣摩好长时间，仔细权衡可扩展性可能会带来的弊端。

一个公共类或者方法如果使用了final关键字，将来如果需要扩展性，就可以去掉这个关键字。但是，如果最开始没有使用final关键字，特别是对于公开的接口来说，将来想要加上就可能是一件非常困难的事。

上面的例子是子类通过改变父类的规范和行为带来的潜在问题。那么父类是不是也可以改变子类的行为呢？ 这听起来有点怪异，但是父类对子类行为的影响，有时候也的确是一个让人非常头疼的问题。

## 麻烦的继承

我先总结一下，父类对子类行为的影响大致有三种：

<li>
改变未继承方法的实现，或者子类调用的方法的实现（super）；
</li>
<li>
变更父类或者父类方法的规范；
</li>
<li>
为父类添加新方法。
</li>

第一种和第三种相对比较容易理解，第二种稍微复杂一点。我们还是通过一个例子来看看其中的问题。

Hashtable是一个古老的，被广泛使用的类，它最先出现在JDK 1.0中。其中，put()和remove()是两个关键的方法。在JDK 1.2中，又有更多的方法被添加进来，比如entrySet()方法。

```
public class Hashtable&lt;K,V&gt; ... {
    // snipped
    /**
     * Returns a {@link Set} view of the mappings contained in
     （ this map.
     * The set is backed by the map, so changes to the map are
     * reflected in the set, and vice-versa.  If the map is modified
     * while an iteration over the set is in progress (except through
     * the iterator's own {@code remove} operation, or through the
     * {@code setValue} operation on a map entry returned by the
     * iterator) the results of the iteration are undefined.  The set
     * supports element removal, which removes the corresponding
     * mapping from the map, via the {@code Iterator.remove},
     * {@code Set.remove}, {@code removeAll}, {@code retainAll} and
     * {@code clear} operations.  It does not support the
     * {@code add} or {@code addAll} operations.
     *
     * @since 1.2
     */
    public Set&lt;Map.Entry&lt;K,V&gt;&gt; entrySet() {
        // snipped
    }
    // snipped
}

```

这就引入了一个难以察觉的潜在的安全漏洞。 你可能会问，添加一个方法不是很常见吗？这能有什么问题呢？

问题在于继承Hashtable的子类。假设有一个子类，它的Hashtable里要存放敏感数据，数据的添加和删除都需要授权，在JDK 1.2之前，这个子类可以重写put()和remove()方法，加载权限检查的代码。在JDK 1.2中，这个子类可能意识不到Hashtable添加了entrySet()这个新方法，从而也没有意识到要重写覆盖entrySet()方法，然而，通过对entrySet()返回值的直接操作，就可以执行数据的添加和删除的操作，成功地绕过了授权。

```
public class MySensitiveData extends Hashtable&lt;Object, Object&gt; {
    // snipped
    @Override
    public synchronized Object put(Object key, Object value) {
        // check permission and then add the key-value
        // snipped
        super.put(key, value)
    }
    
    @Override
    public synchronized Object remove(Object key) {
        // check permission and then remove the key-value
        // snipped
        return super.remove(key);
    }
    // snipped, no override of entrySet()
}

```

```
MySensitiveData sensitiveData = ...   // get the handle of the data
Set&lt;Map.Entry&lt;Object, Object&gt;&gt; sdSet = sensitiveData.entrySet();
sdSet.remove(...);    // no permission check
sdSet.add(...);       // no permission check

// the sensitive data get modified, unwarranted.

```

现实中，这种问题非常容易发生。一般来说，我们的代码总是依赖一定的类库，有时候需要扩展某些类。这个类库可能是第三方的产品，也可能是一个独立的内部类库。但遗憾的是，类库并不知道我们需要拓展哪些类，也可能没办法知道我们该如何拓展。

所以，当有一个新方法添加到类库的新版本中时，这个新方法会如何影响扩展类，该类库也没有特别多的想象空间和处理办法。就像Hashtable要增加entrySet()方法时，让Hashtable的维护者意识到有一个特殊的MySensitiveData扩展，是非常困难和不现实的。然而Hashtable增加entrySet()方法，合情又合理，也没有什么值得抱怨的。

然而，当JDK 1.0/1.1升级到JDK 1.2时，Hashtable增加了entrySet()方法，上述的MySensitiveData的实现就存在严重的安全漏洞。要想修复该安全漏洞，MySensitiveData需要重写覆盖entrySet()方法，植入权限检查的代码。

可是，我们怎样可能知道MySensitiveData需要修改呢！ 一般来说，如果依赖的类库进行了升级，没有影响应用的正常运营，我们就正常升级了，而不会想到检查依赖类库做了哪些具体的变更，以及评估每个变更潜在的影响。这实在不是软件升级的初衷，也远远超越了大部分组织的能力范围。

而且，如果MySensitiveData不是直接继承Hashtable，而是经过了中间环节，这个问题就会更加隐晦，更加难以察觉。

```
public class IntermediateOne extends Hashtable&lt;Object, Object&gt;;

public class IntermediateTwo extends IntermediateOne;

public class Intermediate extends IntermediateTwo;

public class MySensitiveData extends Intermediate;

```

糟糕的是，随着语言变得越来越高级，类库越来越丰富，发现这些潜在问题的难度也是节节攀升。我几乎已经不期待肉眼可以发现并防范这类问题了。

那么，到底有没有办法可以防范此类风险呢？

主要有两个方法。

**一方面，当我们变更一个可扩展类时，要极其谨慎小心**。一个类如果可以不变更，就尽量不要变更，能在现有框架下解决问题，就尽量不要试图创造新的轮子。有时候，我们的确难以压制想要创造出什么好东西的冲动，这是非常好的品质。只是变更公开类库时，一定要多考虑这么做的潜在影响。你是不是开始思念final关键字的好处了？

**另一方面，当我们扩展一个类时，如果涉及到敏感信息的授权与保护，可以考虑使用代理的模式，而不是继承的模式**。代理模式可以有效地降低可扩展对象的新增方法带来的影响。

```
public class MySensitiveData {
    private final Hashtable hashtable = ...

    public synchronized Object put(Object key, Object value) {
        // check permission and then add the key-value
        hashtable.put(key, value)
    }

    public synchronized Object remove(Object key) {
        // check permission and then remove the key-value
        return hashtable.remove(key);
    }
}

```

我们使用了Java语言来讨论继承的问题，其实**这是一个面向对象机制的普遍的问****题，**甚至它也不单单是面向对象语言的问题，比如使用C语言的设计和实现，也存在类似的问题。

## 小结

通过对这个案例的讨论，我想和你分享下面两点个人看法。

<li>
**一个可扩展的类，子类和父类可能会相互影响，从而导致不可预知的行为。**
</li>
<li>
**涉及敏感信息的类，增加可扩展性不一定是个优先选项，要尽量避免父类或者子类的影响。**
</li>

学会处理和保护敏感信息，是一个优秀工程师必须迈过的门槛。

## 一起来动手

了解语言和各种固定模式的缺陷，是我们打怪升级的一个很好的办法。有时候，我们偏重于学习语言或者设计经验的优点，忽视了它们背后做出小小的妥协，或者缺陷。如果能利用好优点，处理好缺陷，我们就可以更好地掌握这些经验总结。毕竟世上哪有什么完美的东西呢？不完美的东西，用好了，就是好东西。

我们利用讨论区，来聊聊设计模式这个老掉牙的、备受争议的话题。说起“老掉牙”，科技的进步真是快，设计模式十多年前还是一个时髦的话题，如今已经不太受待见了，虽然我们或多或少，或直接或间接地都受益于设计模式的思想。如果你了解过设计模式，你能够分享某个设计模式的优点和缺陷吗？ 使用设计模式有没有给你带来实际的困扰呢？

上面的例子中，我们提到了使用代理模式来降低父类对子类的影响。那么你知道代理模式的缺陷吗？

欢迎你把自己的经验和看法写在留言区，我们一起来学习、思考、精进！

如果你觉得这篇文章有所帮助，欢迎点击“请朋友读”，把它分享给你的朋友或者同事。


