HTTP认证及其在Web平台中的实现

一、前言

        HTTP认证是Web服务器对客户端的权限进行认证的一种方式,能够为Web应用提供一定程度的安全保障。目前一些Web应用项目已经提出了采用HTTP认证的需求。虽然一般的Web容器都提供基本认证和摘要认证的API,但不同的Web容器提供的API也互不相同,因此我们在ZX Web平台的工具包中提供了一组API,利用这组API,开发人员可以在应用程序中使用统一的接口轻松实现HTTP认证功能,而不必依赖于Web容器。

二、HTTP认证机制

        HTTP认证采用“质询-响应(challenge-response)”的机制。“质询”是服务器端对客户端的质询,即要求客户端发送认证信息;“响应”是客户端对“质询”的响应,即发送带有认证信息的HTTP请求。
        一般来说,客户端第一次请求一个URI时,并不知道是否需要认证,因此总是不带认证信息的,这时服务器端就会找不到认证信息,认证失败,于是向客户端发出一个“质询”。
        所谓“发出质询”,就是给客户端发送一个HTTP响应,其状态码为401 (Unauthorized),并且包含消息头WWW-Authenticate,客户端看到这个响应就知道这个URI需要认证。WWW-Authenticate消息头格式为
        WWW-Authenticate:challenge
        其中<challenge>是就是质询信息,RFC2617中的定义为:
challenge   = auth-scheme 1*SP 1#auth-param
auth-scheme = token
auth-param = token "=" ( token | quoted-string )
        在challenge的定义中,首先是auth-scheme,即认证方案,它被定义为一个token,即预定义的符号。所谓token,就是一些字符串,但这些字符串不是随意的,而是大家约定的,它们具有特定的含义。auth-scheme的取值只能是Basic或Digest,分别表示基本认证和摘要认证,这两个单词就是token。这里没有把auth-scheme定义为Basic|Digest,而是一个token,说明还可以进行扩展,还可以取其他符号——只要服务器端和客户端互相约定都能理解就行。
        接着,“1*SP”表示1个或多个空格符。其中“1*”表示数量为1个到多个,“SP”即空格符(ASCII码32)。
        然后是“1#auth-param”,表示一个auth-param的列表。其中的“1#”也表示后面的元素是1到多个,但与“1*”不同的是,“1#”表示一个“列表”,即元素之间是用逗号“,”分隔开的。列表中的每个auth-param被定义为一个名值对,即
        符号=符号
        或
        符号=“引号中的字符串”
        这两种形式。
        基本认证和摘要认证中都定义了一个相同的auth-param,即realm,定义为:
    realm       = “realm”“=” realm-value
    realm-value = quoted-string
realm-value是一个两端加引号的大小写相关的字符串,表示要求认证的“领域(realm)”。领域是由服务器自己决定的,不同的服务器可以设置自己的领域,同一个服务器也可以有多个领域。质询中包含领域信息是为了让客户端知道哪个范围的用户名是合法的,RFC2617中建议领域至少包含主机名和有权限的用户组,例如[email protected]”。
        客户端收到质询后,应该给服务器端返回一个“响应”,即重新发送一个新的HTTP请求。这个新的HTTP请求与前一个HTTP请求的差别在于多了一个Authorization消息头,该消息头的格式为Authorization:credentials,其中的credentials就是认证信息,认证信息的格式根据不同的认证方案而有所不同。
        服务器端对认证信息进行判断,只有认证通过,才会响应客户端的请求。
      1. 基本认证
        基本认证的质询中只定义了一种auth-param,即realm,因此基本认证的质询也定义为
challenge = “Basic” realm
质询举例:
        当服务器端认证不通过,将返回一个状态码为401(Unautherized)的响应消息,并带有如下消息头:
       WWW-Authenticate: Basic realm=“My Secret World”
        基本认证的认证信息credentials定义为:
    credentials       = “Basic” basic-credentials
    basic-credentials = base-user-pass
    base-user-pass = <base encoding of user-pass,
                        except not limited to 76 char/line>
    user-pass         = userid “:” password
    userid            = *
    password          = *TEXT
        简单说,认证信息就是“Basic”后面加上“<用户名>:<密码>”的Base编码,只不过这里的Base编码不对每一行的字符数做最大76个的限制。
        认证信息举例:
        如果用户名为“abc”,密码为“abcd”,将“abc:abcd”进行Base编码得到“YWJjOmFiY2Q=”,于是消息头中认证信息为
Authorization: Basic YWJjOmFiY2Q=
       2. 摘要认证
        由于基本认证被认为是不安全的认证方式,摘要认证作为替代方案被制定了出来。摘要认证中,用户名和密码不会以明文方式传送,而是经过了加密。从名称可以看出,是生成了信息摘要,客户端和服务器使用各自的密码以同样的算法生成信息摘要,两者比较即可判断客户端的密码是否正确。
        摘要认证仍然采用WWW-Authenticate和Authorization两个消息头,另外还规定了消息头Authentication-Info。消息头Authentication-Info用于认证通过之后,服务器给客户端返回一些信息,例如可以用来指定下一次认证用的临时值,或者也生成一个摘要,表明服务器确实知道用户密码,等等。不过这个消息头并不是必须的,实际应用中一般也用不着,因此Web平台中目前没有实现,这里也不做介绍,若有兴趣请查看RFC2617。

三、HTTP认证的安全性

       1. 基本认证的安全性
        基本认证不是一种安全的认证方式,因为Base编码仅仅是编码,而不是加密,以这种形式在互联网上传递用户名和密码,其危险性是显而易见的。但如果对安全性要求不高,则可以使用这种认证方式做为最简单的安全措施--毕竟比没有安全措施要好。
        当然,如果能够保证中间不会有人截取数据包,例如处于内部局域网,或者底层协议是安全的(如使用SSL或其他一些安全机制),倒是可以弥补HTTP基本认证在安全性方面的不足。
       2. 摘要认证的安全性
        由于基本认证过于危险,人们才使用摘要认证作为一种替代方案。但它也仅仅是作为基本认证的替代品,因为它本身也不是十分安全的,也存在一些弱点。
        (1)摘要认证只能作为权限认证机制,并非保密措施,因为消息体并没有被加密。qop使用“auth-int”只能保证消息体不被修改,不能防止被偷看。
        (2)Replay攻击:攻击者可能截取一次摘要信息,然后利用相同的摘要信息请求相同的URI,如果该URI可以通过POST或PUT方法访问,则攻击者可能修改消息体。控制nonce中的时间戳和nc次数有助于减小replay攻击机会;每次使用新的nonce值(用Authentication-Info消息头)可避免遭受replay攻击,当然也增加了开销。
        (3)MITM(Man in the Middle)攻击:攻击者截取网络数据包,给客户端发送一个假的质询,只要求客户端使用基本认证,从而取得密码。MITM最常见的方式是提供一个“免费”的但其实是恶意的代理服务器。要防止此类攻击,可双方约定只使用摘要认证,不允许使用基本认证,但一般浏览器并不支持指定认证方式,除非是自己开发的客户端。
        其他还有些攻击方式,例如通过“查字典”猜密码等比较野蛮的方式。虽然摘要认证有这些弱点,但在许多情况下还是有它的使用价值的,至少比基本认证是好多了。

四、HTTP认证在ZX Web平台中的实现

        在ZX Web平台的包中提供了HTTP认证的API,这组API包括一个接口HttpAuth以及该接口的两个实现类HttpBasicAuth和HttpDigestAuth,分别实现基本认证和摘要认证。开发人员可以使用接口,也可以直接使用两个实现类。类图如图1所示。
        1. HttpAuth接口
        这个接口应该提供一个方法取得用户提交的用户名和密码,以便应用程序校验其正确性。在基本认证中,这一点是可以做到的,但在摘要认证中服务器端并不能知道客户端提交的密码,所能得到的只是对包含密码的数据进行MD5编码所得到一个摘要,因此为了照顾摘要认证,HttpAuth接口没有设计这样一个方法,而是提供另一个方法authenticate,调用者将用户名和正确的密码作为该方法的参数传入,返回认证结果。该方法还需要另一个参数,即HTTP请求HttpServletRequest的一个实例,因为需要从中取得用户提交的认证信息;HttpAuth接口的所有方法都需要这个参数。
        在大多数情况下,合法的用户不止一个,因此调用authenticate方法之前必须知道客户端提交的用户名。HttpAuth接口的getUserName方法完成此功能。这两个方法描述如下:
        getUserName
        功能:
                取得用户发来的认证信息中的用户名。
原型:
                public String getUserName(HttpServletRequest req)
参数:
                req - HTTP请求
        返回:
                返回认证信息中的用户名。若未取到(请求中未包含认证信息或认证信息格式不正确),返回null。
        authenticate
        功能:
                判断一个HTTP请求的认证信息中用户名和密码是否与指定的相符。
原型:
        public boolean authenticate(HttpServletRequest req,
                            String userName,
                            String password)
参数:
                req - HTTP请求
                userName - 指定的用户名
                password - 指定的密码
        返回:
                true - 认证通过
                false - 认证未通过
        当调用authenticate方法发现认证未通过,应该给客户端发回一个质询,即设置响应的状态码为401,并设置消息头WWW-Authenticate。HttpAuth接口的setUnauth方法负责完成此功能。
SetUnauth
        功能:
                设置 401 Unauthorized 状态码,并添加WWW-Authenticate消息头。
        原型:
                public void setUnauth(HttpServletRequest req,
                           HttpServletResponse rsp,
                           String realm)
        参数:
                req - HTTP请求
                rsp - HTTP响应
                realm - WWW-Authenticate消息头中的realm值。若为null,则默认使用 servername:port
        返回:无
setUnauth方法设置HTTP响应对象rsp的状态码为401,并添加WWW-Authenticate消息头,至于消息头的内容,HttpBasicAuth类和HttpDigestAuth类根据基本认证和摘要认证的规范不同而有不同的实现。设置WWW-Authenticate消息头需要realm值,调用者通过realm参数指定realm值。调用者如果不想指定realm值,可以置realm参数为null,或直接调用另一种参数形式的setUnauth方法:
        public void setUnauth(HttpServletRequest req, HttpServletResponse rsp)
        如果不指定realm值,setUnauth方法将从请求对象req中取得服务器名和端口,按servername:port组合,以此作为realm的值。
最后,HttpAuth接口提供getAuth方法,以取得HTTP请求中用户提交的认证信息,即Authorization消息头中"Basic"或"Digest"标志之后的信息;若是基本认证,此字符串是BASE编码的,则返回解码后的字符串。对开发人员来说,此方法不是必须的,但可以用于调试。
getAuth
        功能:
                取得用户提交的认证信息。
        原型:
                public String getAuth(HttpServletRequest req)
        参数:
                req - HTTP请求
        返回:
                用户提交的摘要认证信息中"Digest"或"Basic"标识之后信息;若是BASE编码,返回解码后的字符串。
       2. HttpBasicAuth类
        HttpBasicAuth类实现HTTP基本认证。
        HttpBasicAuth除了实现HttpAuth接口的所有方法外,还增加一个方法getUserNamePwd,用以取得客户端提交的用户名和密码。在某些情况下,服务器端的数据库或文件中并没有保存用户密码的明文,而是保存对密码经过某种不可逆加密算法(MD5或其他)而得到的信息摘要(如UNIX系统)。如果是这样,服务器端无法调用authenticate方法来进行认证,只能先调用getUserNamePwd方法取得HTTP请求中的用户名和密码,然后按照数据库或文件中的相同加密算法计算其摘要,最后比较所得到的摘要是否与数据库或文件中的相同。这种情况下不能使用摘要认证,只能使用基本认证。
        getUserNamePwd
        功能:
                取得用户名密码字符串数组。
原型:
                public String[] getUserNamePwd(HttpServletRequest req)
参数:
                req - HTTP请求
        返回:
                字符串数组,第一个元素为用户名,第二个元素为密码;若未取到用户名和密码,返回null。
        HttpBasicAuth只有一个成员变量:
    protected static BASEDecoder baseDecoder = new BASEDecoder();
即sun.misc.BASEDecoder类的一个实例,用于BASE解码。
        3. HttpDigestAuth类
        HttpDigestAuth类实现HTTP摘要认证。HttpDigestAuth类除了实现HttpAuth接口外,根据摘要认证的特点,还提供了其他一些方法。
        客户端提交的认证信息中,有一个nc值,表示临时值nonce已被使用的次数;HttpDigestAuth类的方法getNonceCount可取得此值。
        getNonceCount
        功能:
                取得用户发来的认证信息中的nc(NonceCount)值。
原型:
                public int getNonceCount(HttpServletRequest req)
参数:
                req - HTTP请求
        返回:
                返回nc值。若未取到,返回0。
        对于这个nc值,服务器端根据自己的策略可以选择不作限制,也可限定一个最大值。如果发现摘要正确但nonce使用次数超过上限,可以给客户端返回一个“过期”质询响应,其WWW-Authorize消息头中包含一个新的nonce值,并设置stale字段为true,以此要求客户端使用新的nonce值重新计算摘要。此时客户端不会重新弹出对话框让用户输入密码,而是用原来的密码和新的nonce值重新计算摘要,然后重新发出请求。对客户端用户来说,这一过程是透明的。HttpDigestAuth提供一种参数形式的setUnauth方法,用以给客户端返回“过期”质询。
        setUnauth
        功能:
                设置 401 Unauthorized 状态码,并添加WWW-Authenticate消息头。
原型:
                public void setUnauth(HttpServletRequest req,
                      HttpServletResponse rsp,
                      String realm,
                  boolean stale)
参数:
                req - HTTP请求
                rsp - HTTP响应
                realm - 指定realm字段的值,若为null则采用默认值servername:port
                stale - 指定stale字段的值,true表示客户端摘要正确,只是nonce值过期导致鉴权失败;false表示并非因为nonce值过期才鉴权失败。
        返回:
                无
        调用此方法,置参数stale为true,即向客户端发挥“过期”质询。HttpDigestAuth中还有其他几种形式的setUnau方法,凡是未指定stale的,均默认为false。
        无论哪种参数形式的setUnauth方法,都必须每次生成一个新的nonce值。生成nonce的算法RFC2617并没有做规定,HttpDigestAuth类在generateNonce方法中生成nonce,算法为
        NOnce = BCD( MD5( <client-IP>:<time-stamp>:<private-key> ) )
        即:取得客户端IP地址、当前时间(毫秒数),以及一个私有的key,将他们用冒号连接起来,取其MD5摘要,然后将所得的字节数组转换为十六进制字符串(即BCD码)。

五、应用举例

        这里通过一个例子来说明如何使用Web平台提供的API实现HTTP认证。
        新建一个JSP文件AuthTest.jsp,源代码如下(读者可以将表格的右边一列拷贝到一个文本文件中,另存为AuthTest.jsp即可)。将此文件部署到一个应用服务器中(如Tomcat等),启动服务器后,用IE请求这个JSP,即可看到浏览器弹出对话框提示输入用户名和密码。
        验证时,可以在IE中尝试输入正确或错误的用户名、密码,并可在对话框中选择“取消”,看看效果如何。注意如果输入了正确的用户名和密码,认证通过之后,IE不会再弹出对话框,除非重新启动一个IE窗口,或者修改JSP文件中的用户名、密码字符串导致认证失败。
        第11行生成HttpAuth接口的一个实例,即一个HttpBasicAuth对象,用以测试基本认证。第12行取得用户名,然后判断用户名是否是“abc”,若不是,则在第15行设置未认证响应,指定realm值为“BasicAuthUser”,客户端的对话框里会看到这个字符串。如果用户名正确,第17行调用authenticate方法判断用户名/密码是否为“abc”/“abcd”,若不是,在第19行再设置未认证响应,这次指定realm为“BasicAuthPwd”(这里两次指定不同的realm值只是为了做验证,实际应用中一般应该是一个相同的值)。若认证通过,则第22行在页面上输出一个字符串表明已认证。最后不管是哪种情况,都在第51行给客户端返回它自己提交的认证信息。
        从第25行到第49行是测试HTTP摘要认证,这里被注释掉了,验证时可将其恢复,同时注释掉第11到23行测试基本认证的部分。与基本认证相似,首先仍然是生成一个HttpAuth接口的实例,但这次是HttpDigestAuth对象;然后判断用户名和密码是否正确。如果都正确,在第38行取出nc值,判断其是否超过上限10,若超过则在第45行设置认证未通过(nonce过期)响应。

七、小结

        本文介绍了HTTP认证的概念、规范以及ZX Web平台提供的API。RFC2617中还涉及到使用代理服务器时HTTP认证的一些规范,这里就不作介绍了。
        HTTP认证虽然不是安全的认证方式,但仍不失为一种简单易用的安全措施,在许多地方被采用。项目开发中,在决定是否采用HTTP认证时,一定要考虑方案的安全性,以及项目本身对安全性的要求;另一方面还要考虑浏览器对HTTP认证的支持。RFC2617发布时,一般的浏览器都支持基本认证,而只有微软的IE支持摘要认证。

你可能感兴趣的:(http)