现在的位置: 首页 > 综合 > 正文

jstl 标签 编码

2017年08月22日 ⁄ 综合 ⁄ 共 6955字 ⁄ 字号 评论关闭

对于大多数软件而言,安全性至关重要。为此,开发者投入大量的时间和精力,设计、开发、测试和维护软件安全。这篇介绍 Spring 安全框架(以前叫 Acegi )的 文章
详尽地描述了应用程序的安全性问题:

安全目标变化多端,采用全面系统方法的非常重要。在安全领域,我们主张采取“逐层安全”的策略。每个层依靠自身尽量提高安全性,后续各层提供进一步的安全保障。各层安全越缜密,应用程序越强健越安全。

虽然整合身份验证、授权和输入校验等功能轻而易举,但是开发者大多忽视了一个陷阱:盲目信任用户输入。虽然发送给浏览器的数据是合法的,但这并不意
味着浏览器返回数据未被用户做过不恰当的改动。尽管普通用户不会留意,但是在浏览器地址栏中,应用程序参数会清楚地显示出来。在 GET
请求发给服务器之前,用户可以毫无阻碍地“定制”这个请求。同样的道理,高级用户可以毫无阻碍地在本地“定制”表单,并提交更改后的 POST
请求。为了使应用程序尽可能安全,必须能够阻止这类更改。

这个问题的一个解决方法,是在生成的 URL
地址中,为了保护应用程序,对敏感数据禁止做识别和处理。本文给出一个设计方案,请求的参数和值利用自定义的 JSTL
标记加密,然后发给浏览器;而且,提交给服务器的数据利用 servlet
过滤器进行解密。这形成一种简单易用、颇为显见的系统,给应用程序添加了一层保护。

虽然本文非常
简要地介绍了 JSTL 和 servlet 过滤器,但是读者事先应当对此二者有基本认识。如果读者需要了解 JSTL 或 servlet 过滤器,那么请参阅 参考资料
一节,其中有几篇值得推荐的介绍性文章。本文不要求读者熟悉加密和解密算法

自定义 JSTL 标记

基于 Java 的 Web 应用程序,有一些常见功能。为了避免开发者重复编写常见功能,Sun 发布了 JSP 标准标记库(JSTL)
。本文鼓励开发者放弃采用 scriptlet 的 Model 1 开发风格。对于常见功能的开发,JSTL 以 JSP 标记库的形式,提供了一套实现标准和语法。JSTL 已经被业界广为采用。其他模型-视图-控制器(MVC)框架,例如 Spring Web MVC
、Struts 1
2
,也有类似的库。

<c:url>
是一个常用的 JSTL 标记。在应用程序中,它用来生成 URL 地址。标记 <c:url>
的独到之处,是给出预设的应用程序上下文路径名,从而创建绝对 URL 地址、经由 URL 重写的会话管理以及请求参数的 URL 编码。标记 <c:url>
能够有选择地接受参数,进而动态生成 URL。

因为 JSTL 标记是 Java 类,所以可以扩展这些标记得到自定义功能或增强功能。具体而言,标记 <c:url>
处理 URL 编码请求参数的能力,才是我们覆盖处理的兴趣所在。但是在讨论实现新功能的方法之前,我们先从概念上了解一下,标记 <c:url>
如何进行URL编码。每个请求参数由名称和值两部分组成,例如:

index.jsp?client_id=1824-67-04-F9#4$&client_name=Big Box Company

为了进行 URL 编码,标记 <c:url>
取出各请求参数值并将它传递给一个转换类。这个类将值转换为 application/x-www-form-urlencoded
MIME 格式(详细内容参见 Java API 和 URLEncoder
)。下面,将这个更新后的名称-值对,增补到一个 String
。这个 String
,收录了先前全部完成编码的名称-值对。各名称-值对都做上述处理,直到全部处理完毕。如此累积得到的 String
表示请求的 URL 编码查询 String
。编码后,上述示例变为:

index.jsp?client_id=1824-67-04-F9%234%24&client_name=Big+Box+Company

我们自定义的 <c:url>
标记将做类似处理。在此过程中,请求的各参数经过处理,生成更新过的查询 String
,区别在于请求的各个参数做了加密,而非URL编码。我们看一下代码。

public class UrlEncryptedParameterTag extends UrlTag
{
...
private TextEncryptor textEncryptor = new BCodecTextEncryptor();
...
/**
* Encrypts and adds a parameter under the name/key contained
* within the encryptedParameterName property.
*
* @param name The parameter name.
* @param value The parameter value.
*/
public void addParameter(String name, String value)
{
if (enabled)
{
logger.debug("Encrypting request parameter: " + name);

StringBuilder sb = new StringBuilder(name);
sb.append("=");
sb.append(value);

String encryptedNameValuePair =
textEncryptor.encrypt(sb.toString());

value = URLEncoder.encode(encryptedNameValuePair,
"UTF-8");
name = "enc";
}

super.addParameter(name, value);
}
...

因为我们打算修改参数添加到请求的方式,所以上述代码只需覆盖方法 addParameter(String name, String value)
。该方法属于 <c:url>
的类 UrlTag
。这个覆盖方法调用 StringBuilder
StringBuffer
的 Java 5 增强版)以串接请求参数的名称和值,再将值发送给一个类。这个类执行加密,并将加密好的值传递给 super.addParameter()
。如此生成的加密值,再以参数名enc
绑定到 HttpRequest
。此后,标记处理如常。上文 URL 示例变为:

index.jsp?enc=%3D%3FUTF-8%3FB%3FY2xpZW50X2lkPTE4MjQtNjctMDQtRjklMjM0JTI0%3F%3D&enc=%3D%3FUTF-8%3FB%3FY2xpZW50X25hbWU9QmlnK0JveCtDb21wYW55%3F%3D

注意:应用程序应当避免使用参数名称 enc
,因为它现在指称加密参数。虽然这种用法应用程序发生意外的概率较小,但是参数用什么名称是可以配置的。

由上述代码可以看出,覆盖方法本身并不参与实际加密,而是委托给一个单独的类。这种设计有助于代码紧凑,且方便加密算法的替换。下一节将更加详细地讨论加密组件。

加密组件

自定义 JSTL
标记委托一个单独的组件,对请求的参数和值加密。创建这种组件的第一步,是确定应用程序所需的加密强度。因为大多数应用程序管理的信息没有多么敏感,所以
第一个代码示例将只采用Base64编码。尽管没有真正考虑加密安全,但是 Base64
编码确实将输入转换为人难以识读的格式,而且跟真正的加密安全算法相比,速度颇快。

/**
* Use Apache Commons Codec's BCodec to provide Base64
* encoding and decoding.
*/
public class BCodecTextEncryptor implements TextEncryptor
{
private BCodec bCodec = new BCodec();

/**
* Encrypt the String.
*
* @param plaintext The input String you wish
* to encrypt.
*/
public String encrypt(String plaintext)
{
try
{
return bCodec.encode(plaintext);
}
catch (EncoderException e)
{
throw new EncryptionOperationNotPossibleException(e.getCause());
}
}

/**
* Decrypt the String.
*
* @param encryptedString The encrypted String
* you wish you decrypt.
*/
public String decrypt(String encryptedString)
{
try
{
return bCodec.decode(encryptedString);
}
catch (DecoderException e)
{
throw new EncryptionOperationNotPossibleException(e.getCause());
}
}
}

当然,确有应用程序要求更高的加密强度。如果你仔细阅读上述代码,会注意到 TextEncryptor
,它是该加密组件的主接口,由开源项目 Jasypt
提供。Jasypt 是一款简单易用的库,提供一组有工业强度的加密算法,又不要求深入了解加密工作原理;而且,Jasypt
与众多开源项目集成,例如 BouncyCastle、Hibernate、Spring 和 Spring
Security。尽管这个示例代码默认采用 Base64 编码,但是自定义 JSTL 标记和解密过滤器都调用了方法 setTextEncryptor(TextEncryptor textEncryptor)
,从而能够改变加密算法。通过采用 Spring 的依赖注入,例如管理这种配置,下面的示例在概念上展示了使用 Jasypt 的 StrongTextEncryptor
需要做出的修改:

<bean id="textEncryptor" class="org.jasypt.util.text.StrongTextEncryptor">
<property name="password"
value="servletRequestParameterDecyprtionFilter_password" />
</bean>

<bean id="servletRequestParameterDecryptionFilter"
class="com.spiegssoftware.secureRequestParameters.web.filter.security.ServletRequestParameterDecryptionFilter">
<property name="textEncryptor" ref="textEncryptor" />
</bean>

<bean id="urlEncryptedParameterTagBean"
class="com.spiegssoftware.secureRequestParameters.web.tag.security.UrlEncryptedParameterTag">
<property name="textEncryptor" ref="textEncryptor" />
</bean>

注意:切换加密算法不需要修改代码。

自定义 JSTL 标记和加密组件就位后,我们现在需要 servlet 过滤器,在应用程序处理请求之前,完成解密过程。下面我们将讨论这种解密 servlet 过滤器。

解密 Servlet 过滤器

Java servlet 过滤器起到拦截作用,能够对输入请求或输出响应加以修改或重定向。过滤器在 web.xml
中定义,是一种灵活的方法,能够给任何应用程序添加强大功能。前文有了 JSTL 加密标记作为流程的前半部分,servlet 过滤器则做为后半部分,它转换外来请求,实际完成自定义 JSTL 标记的逆操作。在深入探讨之前,我们先看一下代码。

public void doFilter(ServletRequest req, ServletResponse res, 
FilterChain chain) throws IOException, ServletException
{
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

String encryptedParamtersStrings[] =
ServletRequestUtils.getStringParameters(request,
encryptedParameterName);

for (String encryptedParametersString : encryptedParamtersStrings)
{
if (StringUtils.isNotBlank(encryptedParametersString))
{
String decryptedParametersString =
textEncryptor.decrypt(encryptedParametersString);
decryptedParametersString =
URLDecoder.decode(decryptedParametersString, "UTF-8");

Map<String, String> parameterMap =
MapSupport.delimitedNameValuesStringToMap(decryptedParametersString,
"&", "=");

for (String attributeName : parameterMap.keySet())
{
request.setAttribute(attributeName, parameterMap.get(attributeName));
}
}
}

// Remove the encrypted name-value pairs from the request
request.removeAttribute(encryptedParameterName);

// Continue processing of the chain
chain.doFilter(request, response);
}

因为并非应用程序收到的每个参数都加了密,所以过滤器必须既能够处理加密参数,也能够处理未加密参数。我们知道,既然自定义 JSTL 标记对参数加密,而且将它们以参数名称 enc
绑定给了请求,那么在请求中,这个参数名称下的所有参数都要做解密,而其他参数名称下的参数都不做解密处理。servlet 过滤器与自定义 JSTL
标记类似,委托加密组件做解密处理。解密的结果当是原始请求参数的名称-值对,与自定义 JSTL 标记加密前数据相同。然后,servlet
过滤器以原始参数名称,将该名称-值对绑定给请求。现在,请求的名称-值对已经回到其原始状态,在效果上,仿佛没有经过加密解密处理。从此,应用程序将正
常工作,觉察不到新增了附加的安全层。

性能

当然,此附加安全层并非没有代价。自定义 JSTL 标记中的加密操作和 servlet 过滤器中的解密操作都造成传入请求和传出响应的性能损失。不幸的是,这会显著降低应用程序的性能和扩展能力。

如果应用程序不能承受性能降低,那么可以通过一些预防措施使性能损失最小。首先,评估应用程序的所有参数是否都需要保护。好在不是全部参数都需要加
密,而是介于两个极端,即全都加密和全不加密之间。加密应当仅限敏感信息或者为保证安全值得加密的信息。第二点,如前所述,评估所用加密算法。一旦确定了
应当加密的具体参数,那么是需要高加密强度算法呢,还是计算量较小的算法就足够了呢?

另外,只需稍作努力,就可以创建多重标记-过滤器对。每个标记-过滤器对有不同的加密等级,可以根据加密信息的敏感度,选择恰当的加密等级。一个标
记-过滤器组合可以使用密级较低但是计算较快的算法,处理应用程序的大多数数据;而另一个标记-过滤器组合可以使用性能较慢但是密级较高的算法,处理大多
数敏感数据。采用多重算法将提高整体安全,增加对系统实施欺骗的工作量。

结束语

应用程序的安全性非常重要;系统如古语所云,一着不慎,满盘皆输。通过使用自定义的 JSTL 标记、servlet
过滤器和灵活切换加密算法的方式,本文演示了如何添加一个额外的安全层,这个安全层可以透明地对应用程序的参数和值时行加密和解密,从而防止用户更改浏览
器请求。尽管本文的重点是 JSTL 的应用,但是其思路仍然适用于其他标记库,例如与Struts 2 结合使用的标记库。类似地,虽然本例扩展的是
JSTL 的 <c:url>
标记,用于实现动态 URL 生成,但这种思路同样适用于通过表单生成标记来保护表单数据。

 

转自:http://developers.sun.com.cn/Java/securing-your-applications-request-parameters.html

参考资料

抱歉!评论已关闭.