身份验证的几种方案,应用中的身份验证技术

传统 Web 应用中的身份验证技术

2016/12/13 · 基础技术 ·
WEB,
身份验证

本文作者: 伯乐在线 –
ThoughtWorks
。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

标题中的 “传统Web应用”
这一说法并没有什么官方定义,只是为了与“现代化Web应用”做比较而自拟的一个概念。所谓“现代化Web应用”指的是那些基于分布式架构思想设计的,面向多个端提供稳定可靠的高可用服务,并且在需要时能够横向扩展的Web应用。相对而言,传统Web应用则主要是直接面向PC用户的Web应用程序,采用单体架构较多,也可能在内部采用SOA的分布式运算技术。

一直以来,传统Web应用为构成互联网发挥了重要作用。因此传统Web应用中的身份验证技术经过几代的发展,已经解决了不少实际问题,并最终沉淀了一些实践模式。

www.301.net 1

在讲述多种身份鉴权技术之前,要强调一点:在构建互联网Web应用过程中,无论使用哪种技术,在传输用户名和密码时,请一定要采用安全连接模式。因为无论采用何种鉴权模型,都无法保护用户凭据在传输过程中不被窃取。

标题中的 “传统Web应用”
这一说法并没有什么官方定义,只是为了与“现代化Web应用”做比较而自拟的一个概念。所谓“现代化Web应用”指的是那些基于分布式架构思想设计的,面向多个端提供稳定可靠的高可用服务,并且在需要时能够横向扩展的Web应用。相对而言,传统Web应用则主要是直接面向PC用户的Web应用程序,采用单体架构较多,也可能在内部采用SOA的分布式运算技术。

现在大部分的网站都有用户系统,有些事情只能登陆之后才能做,比如发一条微博。有了用户系统就会有身份验证,本篇记录常用的客户端和服务器的身份验证方案,以备不时之需。

Basic和Digest鉴权

基于HTTP的Web应用离不开HTTP本身的安全特性中关于身份鉴权的部分。虽然HTTP标准定义了好几种鉴权方式,但真正供Web应用开发者选择的并不多,这里简要回顾一下曾经被广泛运用过的Basic
和 Digest鉴权。

不知道读者是否熟悉一种最直接向服务器提供身份的方式,即在URL中直接写上用户名和密码:

1
2
http://user:passwd@www.server.com/index.html
 

这就是Basic鉴权的一种形式。

Basic和Digest是通过在HTTP请求中直接包含用户名和密码,或者它们的哈希值来向服务器传输用户凭据的方法。Basic鉴权直接在每个请求的头部或URL中包含明文的用户名或密码,或者经过Base64编码过的用户名或密码;而Digest则会使用服务器返回的随机值,对用户名和密码拼装后,使用多次MD5哈希处理后再向服务器传输。服务器在处理每个请求之前,读取收到的凭据,并鉴定用户的身份。

www.301.net 2

Basic和Digest鉴权有一系列的缺陷。它们需要在每个请求中提供凭据,因此提供“记住登录状态”功能的网站中,不得不将用户凭据缓存在浏览器中,增加了用户的安全风险。Basic鉴权基本不对用户名和密码等敏感信息进行预处理,所以只适合于较安全的安全环境,如通过HTTPS安全连接传输,或者局域网。

看起来更安全的Digest在非安全连接传输过程中,也无法抵御中间人通过篡改响应来要求客户端降级为Basic鉴权的攻击。Digest鉴权还有一个缺陷:由于在服务器端需要核对收到的、由客户端经过多次MD5哈希值的合法性,需要使用原始密码做相同的运算,这让服务器无法在存储密码之前对其进行不可逆的加密。Basic
和Digest鉴权的缺陷决定了它们不可能在互联网Web应用中被大量采用。

一直以来,传统Web应用为构成互联网发挥了重要作用。因此传统Web应用中的身份验证技术经过几代的发展,已经解决了不少实际问题,并最终沉淀了一些实践模式。

典型的用户身份验证标准(方案):

简单实用的登录技术

对于互联网Web应用来说,不采用Basic或Digest鉴权的理由主要有两个:

  1. 不能接受在每个请求中发送用户名和密码凭据
  2. 需要在服务器端对密码进行不可逆的加密

因此,互联网Web应用开发已经形成了一个基本的实践模式,能够在服务端对密码强加密之后存储,并且尽量减少鉴权过程中对凭据的传输。其过程如下图所示:

www.301.net 3

这一过程的原理很简单,专门发送一个鉴权请求,只在这个请求头中包含原始用户名和密码凭据,经服务器验证合法之后,由服务器发给一个会话标识(Session
ID),客户端将会话标识存储在 Cookie
中,服务器记录会话标识与经过验证的用户的对应关系;后续客户端使用会话标识、而不是原始凭据去与服务器交互,服务器读取到会话标识后从自身的会话存储中读取已在第一个鉴权请求中验证过的用户身份。为了保护用户的原始凭据在传输中的安全,只需要为第一个鉴权请求构建安全连接支持。

服务端的代码包含首次鉴权和后续检查并授权访问的过程:

IUser _user_; if( validateLogin( nameFromReq, pwdFromReq, out _user
_)){ Session[“CurrentUser”] = _user_; }

1
2
3
4
5
IUser _user_;  
if( validateLogin( nameFromReq, pwdFromReq, out _user _)){  
  Session["CurrentUser"] = _user_;  
}
 

(首次鉴权)

IUser _user_ = Session[“CurrentUser”] as IUser; if( _user_ == null
){ Response.Redirect( “/login?return_uri=” + Request.Url.ToString() );
return; }

1
2
3
4
5
6
7
IUser _user_ = Session["CurrentUser"] as IUser;  
if( _user_ == null ){  
     Response.Redirect( "/login?return_uri=" +
     Request.Url.ToString() );  
     return;  
}
 

(后续检查并拒绝未识别的用户)

类似这样的技术简易方便,容易操作,因此大量被运用于很多互联网Web应用中。它在客户端和传输凭据过程中几乎没有做特殊处理,所以在这两个环节尤其要注意对用户凭据的保护。不过,随着我们对系统的要求越来越复杂,这样简易的实现方式也有一些明显的不足。比如,如果不加以封装,很容易出现在服务器应用程序代码中出现大量对用户身份的重复检查、错误的重定向等;不过最明显的问题可能是对服务器会话存储的依赖,服务器程序的会话存储往往在服务器程序重启之后丢失,因此可能会导致用户突然被登出的情况。虽然可以引入单独的会话存储程序来避免这类问题,但引入一个新的中间件就会增加系统的复杂性。

www.301.net 4

  1. HTTP BASIC Authentication
  2. HTTP Digest Authentication
  3. Form-based Authentication
  4. Token Based Authentication
  5. X.509 Certificate Authentication

传统Web应用中身份验证最佳实践

上文提到的简单实用的登录技术已经可以帮助建立对用户身份验证的基本图景,在一些简单的应用场景中已经足够满足需求了。然而,用户鉴权就是有那种“你可以有很多种方法,就是不怎么优雅”
的问题。

最佳实践指的是那些经过了大量验证、被证明有用的方法。而用户鉴权的最佳实践就是使用自包含的、含有加密内容的
Cookie
作为替代凭据。其鉴权过程与上文所提到基于会话标识的技术没有什么区别,而主要区别在于不再颁发会话标识,取而代之的是一个代表身份的、经过加密的
“身份 Cookie”。

www.301.net 5

  1. 只在鉴权请求中发送一次用户名和密码凭据
  2. 成功凭据之后,由服务器生成代表用户身份的 Cookie,发送给客户端
  3. 客户端在后续请求中携带上一步中收到的 “身份 Cookie”
  4. 服务器解密”身份 Cookie”,并对需要访问的资源予以授权

这样,我们消除了对服务器会话存储的依赖,Cookie本身就有有效期的概念,因此顺便能够轻松提供“记住登录状态”的功能。

另外,由于解密Cookie、既而检查用户身份的操作相对繁琐,工程师不得不考虑对其抽取专门的服务,最终采用了面向切面的模式对身份验证的过程进行了封装,而开发时只需要使用一些特性标注(Attribute
Annotation)对特定资源予以标记,即可轻松完成身份验证预处理。

在讲述多种身份鉴权技术之前,要强调一点:在构建互联网Web应用过程中,无论使用哪种技术,在传输用户名和密码时,请一定要采用安全连接模式。因为无论采用何种鉴权模型,都无法保护用户凭据在传输过程中不被窃取。

通常情况下用户认证失败在HTTP协议中的表现是:”401,Access Denied”

传统Web应用中的单点登录

单点登录的需求在向用户提供多种服务的企业普遍存在,出发点是希望用户在一个站点中登录之后,在其他兄弟站点中就不需要再次登录。

如果多个子站所在的顶级域名一致,基于上文所述的实践,可以基于Cookie共享实现最简单的单点登录:在多个子站中使用相同的加密、解密配置,并且在用户登录成功后设置身份
Cookie时将domain值设置为顶级域名即可。这样,只要在其中一个网站登录,其身份
Cookie将在用户访问其他子站时也一起带上。不过实际情况中,这个方案的应用场景很有限,毕竟各个子站使用的用户数据模型可能不完全一致,而加密密钥多处共享也增加了服务器应用程序的安全风险。另外,这种方式与“在多个网站中分别存储相同的用户名与密码”的做法相似,可以说是一种“相同的登录”(Same
Sign-On),而不是“单点登录”(Single Sign-On)。

对于单点登录需求来说,域名相同与否并不是最大的挑战,集成登录系统对各个子站点的系统在设计上的影响才是。我们希望便利用户的同时,也期待各个子系统仍拥有独立用户身份、独立管理和运维的灵活性。因此我们引入独立的鉴权子站点。

www.301.net 6

当用户到达业务站点A时,被重定向到鉴权站点;登录成功之后,用户被重定向回到业务站点
A、同时附加一个指示“已有用户登录”的令牌串——此时业务站点A使用令牌串,在服务器端从鉴权子站点查询并记录当前已登录的用户。当用户到达业务站点B时,执行相同流程。由于已有用户登录,所以用户登录的过程会被自动省略。

这样的单点登录系统能够较好地解决在多个站点中共享用户登录状态的需求。不过,如果在编程实践过程中略有差池,就会让用户陷入巨大的安全风险中。例如,在上述重定向过程中,一旦鉴权系统未能验证返回URL的合法性,就容易导致用户被钓鱼网站利用。在传统Web应用开发实践中,被广泛部署的身份验证体系是比较重量级的WS-Federation
和 SMAL 等鉴权协议和相对轻量级的 OpenID 等技术。

Basic和Digest鉴权

基于HTTP的Web应用离不开HTTP本身的安全特性中关于身份鉴权的部分。虽然HTTP标准定义了好几种鉴权方式,但真正供Web应用开发者选择的并不多,这里简要回顾一下曾经被广泛运用过的Basic

Digest鉴权。

不知道读者是否熟悉一种最直接向服务器提供身份的方式,即在URL中直接写上用户名和密码:

 http://user:passwd@www.server.com/index.html

这就是Basic鉴权的一种形式。

Basic和Digest是通过在HTTP请求中直接包含用户名和密码,或者它们的哈希值来向服务器传输用户凭据的方法。Basic鉴权直接在每个请求的头部或URL中包含明文的用户名或密码,或者经过Base64编码过的用户名或密码;而Digest则会使用服务器返回的随机值,对用户名和密码拼装后,使用多次MD5哈希处理后再向服务器传输。服务器在处理每个请求之前,读取收到的凭据,并鉴定用户的身份。

www.301.net 7

Basic和Digest鉴权有一系列的缺陷。它们需要在每个请求中提供凭据,因此提供“记住登录状态”功能的网站中,不得不将用户凭据缓存在浏览器中,增加了用户的安全风险。Basic鉴权基本不对用户名和密码等敏感信息进行预处理,所以只适合于较安全的安全环境,如通过HTTPS安全连接传输,或者局域网。

看起来更安全的Digest在非安全连接传输过程中,也无法抵御中间人通过篡改响应来要求客户端降级为Basic鉴权的攻击。Digest鉴权还有一个缺陷:由于在服务器端需要核对收到的、由客户端经过多次MD5哈希值的合法性,需要使用原始密码做相同的运算,这让服务器无法在存储密码之前对其进行不可逆的加密。Basic
和Digest鉴权的缺陷决定了它们不可能在互联网Web应用中被大量采用。

HTTP BASIC Authentication

什么是 HTTP Basic
Authentication?见Basic_access_authentication
,在真实场景中的表现是:当用访问需要登录验证的页面时,浏览器会自动弹出一个对话框,要求输入用户名/密码,输入正确后可以正常访问。

www.301.net,在这种方式,浏览器会把用户名和密码通过BASE64编码在HTTP HEAD 里面

Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

服务器端解析之后做身份验证,并给客户端返回

WWW-Authenticate: Basic realm="User Visible Realm"

客户端每次请求都会携带用户名密码,需要通过HTTPs来保证安全。另外客户端需要缓存用户名和密码,以保证不必每次请求都要用户重新输入用户名和密码,通常浏览器会在本地保存10分钟左右的时间,超过之后需要用户再次输入用户名密码。

这是基于HTTP协议的比较传统的身份验证方案,现在已经很少使用。

总结

本文简要总结了在传统Web应用中,被广泛使用的几种典型用户登录时的鉴权处理流程。总体来说,在单体
Web
应用中,身份验证过程并不复杂,只要稍加管理,可以较轻松地解决用户鉴权的问题。但在传统
Web
应用中,为了解决单点登录的需求,人们也尝试了多种方式,最终仍然只有使用一些较复杂的方案才能较好地解决问题。

在现代化 Web
应用中,围绕登录这一需求,俨然已经衍生出了一个新的工程。“登录工程”
并不简单,在后续篇目中将会介绍现代化 Web 应用的典型需求及解决方法。

1 赞 4 收藏
评论

简单实用的登录技术

对于互联网Web应用来说,不采用Basic或Digest鉴权的理由主要有两个:

  1. 不能接受在每个请求中发送用户名和密码凭据
  2. 需要在服务器端对密码进行不可逆的加密

因此,互联网Web应用开发已经形成了一个基本的实践模式,能够在服务端对密码强加密之后存储,并且尽量减少鉴权过程中对凭据的传输。其过程如下图所示:

www.301.net 8

这一过程的原理很简单,专门发送一个鉴权请求,只在这个请求头中包含原始用户名和密码凭据,经服务器验证合法之后,由服务器发给一个会话标识(Session
ID),客户端将会话标识存储在 Cookie
中,服务器记录会话标识与经过验证的用户的对应关系;后续客户端使用会话标识、而不是原始凭据去与服务器交互,服务器读取到会话标识后从自身的会话存储中读取已在第一个鉴权请求中验证过的用户身份。为了保护用户的原始凭据在传输中的安全,只需要为第一个鉴权请求构建安全连接支持。

服务端的代码包含首次鉴权和后续检查并授权访问的过程:

IUser _user_;  
if( validateLogin( nameFromReq, pwdFromReq, out _user _)){  
  Session["CurrentUser"] = _user_;  
}

(首次鉴权)

 IUser _user_ = Session["CurrentUser"] as IUser;  
 if( _user_ == null ){  
     Response.Redirect( "/login?return_uri=" + 
     Request.Url.ToString() );  
     return;  
 }

(后续检查并拒绝未识别的用户)

类似这样的技术简易方便,容易操作,因此大量被运用于很多互联网Web应用中。它在客户端和传输凭据过程中几乎没有做特殊处理,所以在这两个环节尤其要注意对用户凭据的保护。不过,随着我们对系统的要求越来越复杂,这样简易的实现方式也有一些明显的不足。比如,如果不加以封装,很容易出现在服务器应用程序代码中出现大量对用户身份的重复检查、错误的重定向等;不过最明显的问题可能是对服务器会话存储的依赖,服务器程序的会话存储往往在服务器程序重启之后丢失,因此可能会导致用户突然被登出的情况。虽然可以引入单独的会话存储程序来避免这类问题,但引入一个新的中间件就会增加系统的复杂性。

HTTP Digest Authentication

具体见维基:Digest Access
Authentication

Digest authentication 是对前面 Basic access authentication
的升级版本,它不再使用Base64的用户名/密码而是对于用户名密码做哈希取得一个摘要
字符串再传给服务器,这样在传输的过程中不会暴露用户名和密码。

关于作者:ThoughtWorks

www.301.net 9

ThoughtWorks是一家全球IT咨询公司,追求卓越软件质量,致力于科技驱动商业变革。擅长构建定制化软件产品,帮助客户快速将概念转化为价值。同时为客户提供用户体验设计、技术战略咨询、组织转型等咨询服务。

个人主页 ·
我的文章 ·
84 ·
  

www.301.net 10

传统Web应用中身份验证最佳实践

上文提到的简单实用的登录技术已经可以帮助建立对用户身份验证的基本图景,在一些简单的应用场景中已经足够满足需求了。然而,用户鉴权就是有那种“你可以有很多种方法,就是不怎么优雅”
的问题。

最佳实践指的是那些经过了大量验证、被证明有用的方法。而用户鉴权的最佳实践就是使用自包含的、含有加密内容的
Cookie
作为替代凭据。其鉴权过程与上文所提到基于会话标识的技术没有什么区别,而主要区别在于不再颁发会话标识,取而代之的是一个代表身份的、经过加密的
“身份 Cookie”。

www.301.net 11

  1. 只在鉴权请求中发送一次用户名和密码凭据
  2. 成功凭据之后,由服务器生成代表用户身份的 Cookie,发送给客户端
  3. 客户端在后续请求中携带上一步中收到的 “身份 Cookie”
  4. 服务器解密”身份 Cookie”,并对需要访问的资源予以授权

这样,我们消除了对服务器会话存储的依赖,Cookie本身就有有效期的概念,因此顺便能够轻松提供“记住登录状态”的功能。

另外,由于解密Cookie、既而检查用户身份的操作相对繁琐,工程师不得不考虑对其抽取专门的服务,最终采用了面向切面的模式对身份验证的过程进行了封装,而开发时只需要使用一些特性标注(Attribute
Annotation)对特定资源予以标记,即可轻松完成身份验证预处理。

Form-based Authentication

目前为止我们在登陆网页时看到的登陆页面基本都是基于Form-based
Authentication,是最流行的身份验证方式。

当用户访问一个未授权网页的时候,服务器会返回一个登陆页面,用户输入用户名/密码并点击提交按钮,浏览器把表单信息发送给服务器,服务器验证之后创建Session,并把Cookie返回给浏览器。在下次请求的时候,浏览器会把Cookie附加在每个请求的HEAD里面,服务器通过验证Cookie来校验接下来的请求。由于表单信息是明文传输的,所以需要额外的措施来保证安全(比如:HTTPS)。

PS:网上有时候叫做 Cookie-Based Authentication

传统Web应用中的单点登录

单点登录的需求在向用户提供多种服务的企业普遍存在,出发点是希望用户在一个站点中登录之后,在其他兄弟站点中就不需要再次登录。

如果多个子站所在的顶级域名一致,基于上文所述的实践,可以基于Cookie共享实现最简单的单点登录:在多个子站中使用相同的加密、解密配置,并且在用户登录成功后设置身份
Cookie时将domain值设置为顶级域名即可。这样,只要在其中一个网站登录,其身份
Cookie将在用户访问其他子站时也一起带上。不过实际情况中,这个方案的应用场景很有限,毕竟各个子站使用的用户数据模型可能不完全一致,而加密密钥多处共享也增加了服务器应用程序的安全风险。另外,这种方式与“在多个网站中分别存储相同的用户名与密码”的做法相似,可以说是一种“相同的登录”(Same
Sign-On),而不是“单点登录”(Single Sign-On)。

对于单点登录需求来说,域名相同与否并不是最大的挑战,集成登录系统对各个子站点的系统在设计上的影响才是。我们希望便利用户的同时,也期待各个子系统仍拥有独立用户身份、独立管理和运维的灵活性。因此我们引入独立的鉴权子站点。

www.301.net 12

当用户到达业务站点A时,被重定向到鉴权站点;登录成功之后,用户被重定向回到业务站点
A、同时附加一个指示“已有用户登录”的令牌串——此时业务站点A使用令牌串,在服务器端从鉴权子站点查询并记录当前已登录的用户。当用户到达业务站点B时,执行相同流程。由于已有用户登录,所以用户登录的过程会被自动省略。

这样的单点登录系统能够较好地解决在多个站点中共享用户登录状态的需求。不过,如果在编程实践过程中略有差池,就会让用户陷入巨大的安全风险中。例如,在上述重定向过程中,一旦鉴权系统未能验证返回URL的合法性,就容易导致用户被钓鱼网站利用。在传统Web应用开发实践中,被广泛部署的身份验证体系是比较重量级的WS-Federation
和 SMAL 等鉴权协议和相对轻量级的 OpenID 等技术。

Token Authentication

这种授权方式源于OAuth,现在在单页面应用(SPA)中逐渐流行起来(普遍应用在移动App中)。它的大概过程和基于Form/Cookie的授权方式一致,客户端
发送用户名/密码给服务器,服务器返回一个Token(token包含一个过期时间)给客户端

{
    "refresh_token":"xxxx"
    "token": "xxxxx"
}

客户端拿到Token之后被缓存在本地,以后每次请求的时候在HEAD里面带上Token,这样服务器便可以验证客户端,
如果Token过期客户端可以通过RefreshToken再次获取新的Token。。

Authorization: xxxx

通常在基于Cookie的身份验证中,Cookie存储的是SessionId,服务器端需要通过Session来维护会话的状态。但是在SPA或者移动类的REST应用中,状态在本地维护一般使用token来实现无状态的服务器,简化服务器端的逻辑。

更多关于Token和Cookie的对比看下面两篇文章:

  1. Token Based Authentication for Single Page Apps
    (SPAs)
  2. Cookies vs Tokens. Getting auth right with
    Angular.JS

总结

本文简要总结了在传统Web应用中,被广泛使用的几种典型用户登录时的鉴权处理流程。总体来说,在单体
Web
应用中,身份验证过程并不复杂,只要稍加管理,可以较轻松地解决用户鉴权的问题。但在传统
Web
应用中,为了解决单点登录的需求,人们也尝试了多种方式,最终仍然只有使用一些较复杂的方案才能较好地解决问题。

在现代化 Web
应用中,围绕登录这一需求,俨然已经衍生出了一个新的工程。“登录工程”
并不简单,在后续篇目中将会介绍现代化 Web 应用的典型需求及解决方法。

X.509 Certificate Authentication

这种验证方式在面向普通大众的Web服务中很少见到,但是在开发人员中比较流行。比如使用Git给Github上的Repo提交代码,需要提前在Github网站上配置公钥并在本地~/.ssh目录配置私钥。这就是典型的证书验证配置。

另外一种典型应用是HTTPS,但是这里证书的配置并不是为了验证用户身份,而是为了验证服务器的身份同时建立安全的连接(SSL/TLS)。一般情况下,服务器提供的证书是由特殊的CA结构(持有根证书)认证的,而浏览器在发布的时候都会提前预值根证书,这样当用户用浏览器访问一个网页的时候,浏览器会用根证书验证服务器端的证书以确认服务器不是伪造的(或者是中间人)。

Post Author: admin

发表评论

电子邮件地址不会被公开。 必填项已用*标注