<audio id="audio" title="答疑篇：安全篇思考题答案合集" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/30/d9/304913fee45ec6b5390af039b20fccd9.mp3"></audio>

你好，我是朱晔。

今天，我们继续一起分析这门课“安全篇”模块的第27~30讲的课后思考题。这些题目涉及了数据源头、安全兜底、数据和代码、敏感数据相关的4大知识点。

接下来，我们就一一具体分析吧。

### [27 | 数据源头：任何客户端的东西都不可信任](https://time.geekbang.org/column/article/235700)

**问题1：**在讲述用户标识不能从客户端获取这个要点的时候，我提到开发同学可能会因为用户信息未打通而通过前端来传用户ID。那我们有什么好办法，来打通不同的系统甚至不同网站的用户标识吗？

答：打通用户在不同系统之间的登录，大致有以下三种方案。

第一种，把用户身份放在统一的服务端，每一个系统都需要到这个服务端来做登录状态的确认，确认后在自己网站的Cookie中保存会话，这就是单点登录的做法。这种方案要求所有关联系统都对接一套中央认证服务器（中央保存用户会话），在未登录的时候跳转到中央认证服务器进行登录或登录状态确认。因此，这种方案适合一个公司内部的不同域名下的网站。

第二种，把用户身份信息直接放在Token中，在客户端任意传递，Token由服务端进行校验（如果共享密钥话，甚至不需要同一个服务端进行校验），无需采用中央认证服务器，相对比较松耦合，典型的标准是JWT。这种方案适合异构系统的跨系统用户认证打通，而且相比单点登录的方案，用户体验会更好一些。

第三种，如果需要打通不同公司系统的用户登录状态，那么一般都会采用OAuth 2.0的标准中的授权码模式，基本流程如下：

1. 第三方网站客户端转到授权服务器，上送ClientID、重定向地址RedirectUri等信息。
1. 用户在授权服务器进行登录并且进行授权批准（授权批准这步可以配置为自动完成）。
1. 授权完成后，重定向回到之前客户端提供的重定向地址，附上授权码。
1. 第三方网站服务端通过授权码+ClientID+ClientSecret去授权服务器换取Token。这里的Token包含访问Token和刷新Token，访问Token过期后用刷新Token去获得新的访问Token。

因为我们不会对外暴露ClientSecret，也不会对外暴露访问Token，同时使用授权码换取Token的过程是服务端进行的，客户端拿到的只是一次性的授权码，所以这种模式比较安全。

**问题2：**还有一类和客户端数据相关的漏洞非常重要，那就是URL地址中的数据。在把匿名用户重定向到登录页面的时候，我们一般会带上redirectUrl，这样用户登录后可以快速返回之前的页面。黑客可能会伪造一个活动链接，由真实的网站+钓鱼的redirectUrl构成，发邮件诱导用户进行登录。用户登录时访问的其实是真的网站，所以不容易察觉到redirectUrl是钓鱼网站，登录后却来到了钓鱼网站，用户可能会不知不觉就把重要信息泄露了。这种安全问题，我们叫做开放重定向问题。你觉得，从代码层面应该怎么预防开放重定向问题呢？

答：要从代码层面预防开放重定向问题，有以下三种做法可供参考：

- 第一种，固定重定向的目标URL。
- 第二种，可采用编号方式指定重定向的目标URL，也就是重定向的目标URL只能是在我们的白名单内的。
- 第三种，用合理充分的校验方式来校验跳转的目标地址，如果是非己方地址，就告知用户跳转有风险，小心钓鱼网站的威胁。

### [28 | 安全兜底：涉及钱时，必须考虑防刷、限量和防重](https://time.geekbang.org/column/article/237060)

**问题1：**防重、防刷都是事前手段，如果我们的系统正在被攻击或利用，你有什么办法及时发现问题吗？

答：对于及时发现系统正在被攻击或利用，监控是较好的手段，关键点在于报警阈值怎么设置。我觉得可以对比昨天同时、上周同时的量，发现差异达到一定百分比报警，而且报警需要有升级机制。此外，有的时候大盘很大的话，活动给整个大盘带来的变化不明显，如果进行整体监控可能出了问题也无法及时发现，因此可以考虑对于活动做独立的监控报警。

**问题2：**任何三方资源的使用一般都会定期对账，如果在对账中发现我们系统记录的调用量低于对方系统记录的使用量，你觉得一般是什么问题引起的呢？

答：我之前遇到的情况是，在事务内调用外部接口，调用超时后本地事务回滚本地就没有留下数据。更合适的做法是：

1. 请求发出之前先记录请求数据提交事务，记录状态为未知。
1. 发布调用外部接口的请求，如果可以拿到明确的结果，则更新数据库中记录的状态为成功或失败。如果出现超时或未知异常，不能假设第三方接口调用失败，需要通过查询接口查询明确的结果。
1. 写一个定时任务补偿数据库中所有未知状态的记录，从第三方接口同步结果。

值得注意的是，对账的时候一定要对两边，不管哪方数据缺失都可能是因为程序逻辑有bug，需要重视。此外，任何涉及第三方系统的交互，都建议在数据库中保持明细的请求/响应报文，方便在出问题的时候定位Bug根因。

### [29 | 数据和代码：数据就是数据，代码就是代码](https://time.geekbang.org/column/article/237139)

**问题1：**在讨论SQL注入案例时，最后那次测试我们看到sqlmap返回了4种注入方式。其中，布尔盲注、时间盲注和报错注入，我都介绍过了。你知道联合查询注入，是什么吗？

答：联合查询注入，也就是通过UNION来实现我们需要的信息露出，一般属于回显的注入方式。我们知道，UNION可以用于合并两个SELECT查询的结果集，因此可以把注入脚本来UNION到原始的SELECT后面。这样就可以查询我们需要的数据库元数据以及表数据了。

注入的关键点在于：

- 第一，UNION的两个SELECT语句的列数和字段类型需要一致。
- 第二，需要探查UNION后的结果和页面回显呈现数据的对应关系。

**问题2：**在讨论XSS的时候，对于Thymeleaf模板引擎，我们知道如何让文本进行HTML转义显示。FreeMarker也是Java中很常用的模板引擎，你知道如何处理转义吗？

答：其实，现在大多数的模板引擎都使用了黑名单机制，而不是白名单机制来做HTML转义，这样更能有效防止XSS漏洞。也就是，默认开启HTML转义，如果某些情况你不需要转义可以临时关闭。

比如，[FreeMarker](https://freemarker.apache.org/docs/dgui_misc_autoescaping.html)（2.3.24以上版本）默认对HTML、XHTML、XML等文件类型（输出格式）设置了各种转义规则，你可以使用?no_esc：

```
&lt;#-- 假设默认是HTML输出 --&gt;
${'&lt;b&gt;test&lt;/b&gt;'} &lt;#-- 输出: &amp;lt;b&amp;gt;test&amp;lt;/b&amp;gt; --&gt;
${'&lt;b&gt;test&lt;/b&gt;'?no_esc} &lt;#-- 输出: &lt;b&gt;test&lt;/b&gt; --&gt;

```

或noautoesc指示器：

```
${'&amp;'} &lt;#-- 输出: &amp;amp; --&gt;
&lt;#noautoesc&gt;
${'&amp;'} &lt;#-- 输出: &amp; --&gt;
...
${'&amp;'} &lt;#-- 输出: &amp; --&gt;
&lt;/#noautoesc&gt;
${'&amp;'} &lt;#-- 输出: &amp;amp; --&gt;

```

来临时关闭转义。又比如，对于模板引擎[Mustache](https://mustache.github.io/mustache.5.html)，可以使用三个花括号而不是两个花括号，来取消变量自动转义：

```
模板:
* {{name}}
* {{company}}
* {{{company}}}
数据:
{
  &quot;name&quot;: &quot;Chris&quot;,
  &quot;company&quot;: &quot;&lt;b&gt;GitHub&lt;/b&gt;&quot;
}
输出：
* Chris
*
* &amp;lt;b&amp;gt;GitHub&amp;lt;/b&amp;gt;
* &lt;b&gt;GitHub&lt;/b&gt;

```

### [30 | 如何正确保存和传输敏感数据？](https://time.geekbang.org/column/article/239150)

**问题1：**虽然我们把用户名和密码脱敏加密保存在数据库中，但日志中可能还存在明文的敏感数据。你有什么思路在框架或中间件层面，对日志进行脱敏吗？

答：如果我们希望在日志的源头进行脱敏，那么可以在日志框架层面做。比如对于logback日志框架，我们可以自定义MessageConverter，通过正则表达式匹配敏感信息脱敏。

需要注意的是，这种方式有两个缺点。

第一，正则表达式匹配敏感信息的格式不一定精确，会出现误杀漏杀的现象。一般来说，这个问题不会很严重。要实现精确脱敏的话，就只能提供各种脱敏工具类，然后让业务应用在日志中记录敏感信息的时候，先手动调用工具类进行脱敏。

第二，如果数据量比较大的话，脱敏操作可能会增加业务应用的CPU和内存使用，甚至会导致应用不堪负荷出现不可用。考虑到目前大部分公司都引入了ELK来集中收集日志，并且一般而言都不允许上服务器直接看文件日志，因此我们可以考虑在日志收集中间件中（比如logstash）写过滤器进行脱敏。这样可以把脱敏的消耗转义到ELK体系中，不过这种方式同样有第一点提到的字段不精确匹配导致的漏杀误杀的缺点。

**问题2：**你知道HTTPS双向认证的目的是什么吗？流程上又有什么区别呢？

答：单向认证一般用于Web网站，浏览器只需要验证服务端的身份。对于移动端App，如果我们希望有更高的安全性，可以引入HTTPS双向认证，也就是除了客户端验证服务端身份之外，服务端也验证客户端的身份。

单向认证和双向认证的流程区别，主要包括以下三个方面。

第一，不仅仅服务端需要有CA证书，客户端也需要有CA证书。

第二，双向认证的流程中，客户端校验服务端CA证书之后，客户端会把自己的CA证书发给服务端，然后服务端需要校验客户端CA证书的真实性。

第三，客户端给服务端的消息会使用自己的私钥签名，服务端可以使用客户端CA证书中的公钥验签。

这里还想补充一点，对于移动应用程序考虑到更强的安全性，我们一般也会把服务端的公钥配置在客户端中，这种方式的叫做SSL Pinning。也就是说由客户端直接校验服务端证书的合法性，而不是通过证书信任链来校验。采用SSL Pinning，由于客户端绑定了服务端公钥，因此我们无法通过在移动设备上信用根证书实现抓包。不过这种方式的缺点是需要小心服务端CA证书过期后续证书注意不要修改公钥。

好了，以上就是咱们整个《Java 业务开发常见错误100例》这门课的30讲正文的思考题答案或者解题思路了。

关于这些题目，以及背后涉及的知识点，如果你还有哪里感觉不清楚的，欢迎在评论区与我留言，也欢迎你把今天的内容分享给你的朋友或同事，一起交流。
