URL 编码简介

在 Web 开发领域,URL 编码是一项至关重要却常被误解的技术。URL 作为互联网资源的地址标识,其编码方式直接影响数据的正确传输与解析。从日常浏览网页开始,URL 就无处不在,例如 http://www.google.com。它看似简单,实则有着严格定义的结构。一个完整的 URL 包含协议(scheme)、主机地址(host)、端口(port)、路径(path)、路径参数(path parameters)、查询参数(query parameters)以及片段(fragment)等部分。然而,在处理 URL 时,开发者往往会遇到诸多陷阱,因此深入理解 URL 编码对于构建稳定、高效的 Web 应用程序至关重要。

通用 URL 语法

  1. 基本结构剖析
    URL 的基本结构由多个部分组成。以 https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third 为例:

    • https:协议,定义了后续部分的结构和通信方式。
    • bob:用户(user)。
    • bobby:密码(password)。这种包含用户和密码的形式在实际中并不常见于普通 URL,但在特定场景下存在。
    • www.lunatech.com:主机地址(host)。
    • 8080:端口号(port)。
    • /file:路径(path),表示资源在服务器上的位置。
    • p=1:路径参数(path parameters)。
    • q=2:查询参数(query parameters)。
    • third:片段(fragment),用于指向 HTML 文件中的特定部分。
  2. 协议的作用
    协议决定了 URL 中其余部分的组织方式。不同协议(如 HTTP、HTTPS、FTP 等)对主机名、端口、路径等的定义和使用方式有所不同。例如,HTTP 协议主要用于 Web 资源的传输,而 FTP 协议用于文件传输。

HTTP URL 语法

  1. 路径结构与示例
    HTTP URL 中的路径部分类似于文件系统的分层结构。以 /photos/egypt/cairo/first.jpg 为例:

    • photos 是根文件夹。
    • egyptphotos 下。
    • cairoegypt 下。
    • first.jpg 是最终的文件。

    每个路径片段可以有可选的路径参数,如 /file;p=1,其中 p 是参数名,1 是参数值。

  2. 查询参数的使用
    查询部分紧跟在路径后,以 ? 隔开,包含多个以 & 分隔的参数。例如 /file?q=2q 是查询参数名,2 是参数值。在提交 HTML 表单或进行搜索时经常使用查询参数,如在 Google 搜索中,用户输入的关键词等信息就是通过查询参数传递给服务器的。
  3. 片段的功能
    片段部分用于指定 HTML 文件中的具体位置。当点击包含片段的链接时,浏览器会自动滚动到页面相应位置,而不是从顶部开始显示。比如在一个长页面中,通过片段可以快速定位到某个章节或特定内容。

URL 常见陷阱

字符编码的选择

  1. 问题阐述
    URL 编码规范未明确规定使用何种字符编码,这导致了混乱。虽然 ASCII 字母数字字符一般无需转义,但 ASCII 之外的保留字(如法语单词 "nœud" 中的 "œ")需要编码。而 Unicode 虽然包含所有字符,但它本身不是一种编码,其多种编码方式(如 UTF-8、UTF-16 等)又让开发者面临选择难题。对于 HTTP URL,其编码方式可能取决于 HTML 页面编码格式或 HTTP 头,这使得编码的确定变得复杂,容易引发错误。
  2. 示例说明
    假设一个 URL 中包含中文字符,如果编码方式选择不当,可能导致服务器无法正确解析该 URL,从而无法获取正确的资源。

保留字符集的复杂性

  1. 不同部分的保留字符差异
    在 URL 的不同部分,保留字符集各不相同。例如:

    • 在路径片段部分,空格被编码为 %20+ 字符可保持不编码。
    • 在查询部分,空格可能被编码为 +(为向后兼容)或 %20+ 作为通配符结果会被编码%2B

    这意味着相同的字符在不同部分编码方式不同,如 blue+light blue 在路径和查询部分会有不同编码形式。

  2. 保留字符的实际情况
    许多开发者对保留字符存在误解,比如 + 在路径部分被允许且表示正号而非空格,? 在查询部分允许不被转义等。像 http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+,=:@-._~!$&'()*+,==?/?:@-._~!$'()*+,;=/?:@-._~!$&'()*+,;=#/?:@-._~!$&'()*+,;= 这样看似混乱的地址,按照规则却是合法的,其路径部分可解析为特定的值。

解码后的 URL 问题

  1. 无法解析的情况
    URL 的语法在解码前有意义,解码后可能出现保留字导致解析错误。例如 http://example.com/blue%2Fred%3Fand+green

    • 解码前路径片段为 blue%2Fred%3Fand+green
    • 解码后变为 blue/red?and+green

    原本请求的是一个名为 blue%2Fred%3Fand+green 的文件,解码后却被错误解析为 blue 文件夹下名为 red?and+green 的文件,以及查询参数 and green

  2. 无法重新编码为相同形式
    若将 http://example.com/blue%2Fred%3Fand+green 解码为 http://example.com/blue/red?and+green 后再编码,得到的将是 http://example.com/blue/red?and+green,与解码前的 URL 不同。这是因为解码后的 URL 已成为有效 URL,再次编码不会还原为原始形式。

在 Java 中正确处理 URL

避免使用错误的编码类

  1. 问题所在
    java.net.URLEncoderjava.net.URLDecoder 类并非用于编码或解码整个 URL。其 API 文档明确指出,它们主要用于 HTML 表单编码,类似于查询部分的编码方式(application/x-www-form-urlencoded)。使用它们来处理整个 URL 是错误的,尽管许多开发者误以为 JDK 中有标准类可正确处理 URL 编码(实际上是各部分分开处理),从而错用了 URLEncoder
  2. 正确做法示例
    假设要构建一个包含路径片段 a/b?c 的 URL,错误的做法是:

    String pathSegment = "a/b?c";
    String url = "http://example.com/" + pathSegment;

    正确的做法是使用专门的工具类(如自定义的 URLUtils)对路径片段进行编码:

    String pathSegment = "a/b?c";
    String url = "http://example.com/" + URLUtils.encodePathSegment(pathSegment);

    这样可以得到正确编码的 URL http://example.com/a%2Fb%3Fc。同样,对于查询子串也需注意,如:

    String value = "a&b==c";
    String url = "http://example.com/?query=" + value;

    这会得到错误的 URL,应改为:

    String value = "a&b==c";
    String url = "http://example.com/?query=" + URLUtils.encodeQueryParameter(value);

注意 URI 相关方法的使用

  1. URI.getPath() 的问题
    URI.getPath() 方法在处理 URL 时存在缺陷。一旦 URL 被解码,语法信息可能丢失。例如:

    URI uri = new URI("http://example.com/a%2Fb%3Fc");
    for (String pathSegment : uri.getPath().split("/"))
        System.err.println(pathSegment);

    上述代码会先将路径 a%2Fb%3Fc 解码为 a/b?c,然后在不应该分割的地方(如 ? 处)将地址分割为地址片段,导致错误结果。

  2. 正确使用示例
    正确的做法是使用 URI.getRawPath() 方法获取未解码的路径,并在需要时手动处理路径参数:

    URI uri = new URI("http://example.com/a%2Fb%3Fc");
    for (String pathSegment : uri.getRawPath().split("/"))
        System.err.println(URLUtils.decodePathSegment(pathSegment));

Apache Commons HTTPClient 的问题

  1. 问题描述
    Apache Commons HTTPClient 3 的 URI 类使用 Apache Commons CodecURLCodec 进行 URL 编码,这存在问题。它犯了与使用 java.net.URLEncoder 同样的错误,不但使用了错误的编码器,还错误地按照每一部分都具有同样的预定设置进行解码。
  2. 影响及解决方案
    这种错误的编码和解码方式可能导致 URL 在传输和处理过程中出现问题,如无法正确解析或与其他系统交互。开发者在使用时应谨慎,若可能,尽量避免使用该类的默认编码方式,或者寻找替代方案来确保 URL 编码的正确性。

在 Web 应用程序的各层次处理 URL 编码问题

创建 URL 时进行编码

  1. HTML 文件中的编码示例
    在 HTML 文件中,对于动态生成 URL 的地方,要确保正确编码。例如,将:

    var url = "#{vl:encodeURL(contextPath + '/view/' + resource.name)}";

    替换为:

    var url = "#{contextPath}/view/#{vl:encodeURLPathSegment(resource.name)}";

    这样可以对路径片段进行正确编码。对于查询参数也应采用类似的方式,确保每个部分都按照正确的规则进行编码,避免因编码错误导致链接失效或资源获取错误。

确保 URL 重写过滤器正确处理

  1. URL 重写过滤器的作用与问题
    URL 重写过滤器用于将漂亮的地址转换为应用依赖的网址,但在处理过程中涉及 URL 解码和重新编码,容易出现问题。例如,将 http://beta.visiblelogistics.com/view/resource/FOO/bar 转换为 http://beta.visiblelogistics.com/resources/details.seam?owner=FOO&name=bar 的过程中,需要从路径部分解码并重新编码为查询值部分。
  2. 正确配置示例
    最初的规则可能如下:

    <urlrewrite decode-using="utf-8">
        <rule>
            <from>^/view/resource/(.*)/(.*)$</from>
            <to encode="false">/resources/details.seam?owner=$1&name=$2</to>
        </rule>
    </urlrewrite>

    这种方式在重写过滤器中只有两种处理网址重写的方法:先解码网址做规则匹配或不进行解码直接处理。更好的选择是后者,特别是在移动网址部分或包含 URL 解码路径分隔符的匹配路径部分时。同时,可以使用内建函数 escape(String)unescape(String) 处理网站转码和解码,但在撰写文章时,Url Rewrite Filter Beta 3.2 存在一些限制,如网址解码使用 java.net.URLDecoder(错误的方式),escape(String)unescape(String) 内建函数使用 java.net.URLDecoderjava.net.URLEncoder(不够强大,无法处理所有情况)。

    经过修正后的规则可以这样写:

    <urlrewrite decode-using="null">
        <rule>
            <from>^/view/resource/(.*)/(.*)$</from>
            <to encode="false">/resources/details.seam
    ?owner=${escape:${unescapePath:$1}}
    &name=${escape:${unescapePath:$2}}</to>
        </rule>
    </urlrewrite>

    不过,仍存在一些问题需要进一步解决,如内建函数的编码功能应更完善,需要从 HTTP 请求确定编码方式,保留的旧函数仍有问题,需要增加更多局部特定的编码和解码函数以及鉴别 per-rule 解码行为的方法等。

正确使用 Apache mod_rewrite

  1. Apache mod_rewrite 的功能与问题
    Apache mod_rewrite 是 Apache Web 服务器的网址重写模块,可用于流量代理等操作,如将 http://beta.visiblelogistics.com/foo 的流量代理到 http://our-internal-server:8080/vl/foo。但它默认会解码网址并重新编码重写后的网址,这是错误的,因为解码后的网址不能被重新编码为原始形式。
  2. 解决方法示例
    一种避免错误的方法是通过 THE_REQUEST 来进行网址匹配。例如:

    # 允许路径片段中的 URL 编码斜杠
    AllowEncodedSlashes On
    # 启用 mod_rewrite
    RewriteEngine on
    # 使用 THE_REQUEST 不解码 URL,因为不需要将 URI 部分移动到其他部分,所以无需解码/重新编码
    RewriteCond %{THE_REQUEST} "^[a-zA-Z]+ /(.*) HTTP/\d\.\d$"
    RewriteRule ^(.*)$ http://our-internal-server:8080/vl/%1 [P,L,NE]

    这样可以在不进行不必要的解码和重新编码的情况下完成网址重写操作,确保 URL 的正确性和稳定性。

总结与展望

URL 编码在 Web 开发中虽然看似基础,但其中的细节和陷阱众多。开发者需要深入理解 URL 的语法结构、编码规则以及在不同场景下的正确处理方式,从 Java 代码中的 URL 构建到 Web 应用程序各个层次(如 HTML 文件、URL 重写过滤器、Apache 服务器模块等)的处理,都要确保编码的准确性。只有这样,才能构建出稳定、可靠的 Web 应用程序,避免因 URL 编码问题导致的各种错误和故障。同时,也希望相关技术标准能够不断完善,为开发者提供更便捷、准确的 URL 编码处理支持。

说明:文中提及的 Apache Commons HTTPClient 3 及 Url Rewrite Filter Beta 3.2 等组件版本较旧,实际开发中建议使用更新的稳定版本或现代框架提供的 URI 构建工具,但核心编码原则依然适用。