http协议之身份认证
2021-02-03 22:13:48 阿炯

在HTTP中,基本认证英语:Basic access authentication是允许http用户代理如:网页浏览器在请求时,提供用户名和密码的一种方式。

在进行基本认证的过程里,请求的HTTP头字段会包含Authorization字段,形式如下: Authorization: Basic <凭证>,该凭证是用户和密码的组和的base64编码。最初的基本认证是定义在HTTP 1.0规范RFC 1945中,后续的有关安全的信息可以在HTTP 1.1规范RFC 2616和HTTP认证规范RFC 2617中找到。于1999年 RFC 2617 过期,于2015年的 RFC 7617 重新被定义。

原理

这一个典型的HTTP客户端和HTTP服务器的对话,服务器安装在同一台计算机上localhost,包含以下步骤:


客户端请求一个需要身份认证的页面,但是没有提供用户名和密码。这通常是用户在地址栏输入一个URL,或是打开了一个指向该页面的链接。

服务端响应一个401应答码[4],并提供一个认证域英语:Access Authentication[5],头部字段为:WWW-Authenticate,该字段为要求客户端提供适配的资源。[6] WWW-Authenticate: Basic realm="Secure Area" 该例子,Basic 为验证的模式,realm="Secure Area"为保护域,用于与其他请求URI作区别。

接到应答后,客户端显示该认证域给用户并提示输入用户名和密码。此时用户可以选择确定或取消。

用户输入了用户名和密码后,客户端软件将对其进行处理,并在原先的请求上增加认证消息头英语:Authorization然后重新发送再次尝试。过程如下:
将用户名和密码拼接为用户:密码形式的字符串。
如果服务器WWW-Authenticate字段有指定编码,则将字符串编译成对应的编码如:UTF-8。
将字符串编码为base64。
拼接Basic,放入Authorization头字段,就像这样:Authorization Basic 字符串。示例:用户名:FreeOA,密码:OpenSesame,拼接后为FreeOA:OpenSesame,编码后RnJlZU9BOk9wZW5TZXNhbWU=,在HTTP头部里会是这样:Authorization: Basic RnJlZU9BOk9wZW5TZXNhbWU=。Base64编码并非加密算法,其无法保证安全与隐私,仅用于将用户名和密码中的不兼容的字符转换为均与HTTP协议兼容的字符集。
    
在本例中,服务器接受了该认证屏幕并返回了页面。如果用户凭据非法或无效,服务器可能再次返回401应答码,客户端可以再次提示用户输入密码。

注意:客户端有可能不需要用户交互,在第一次请求中就发送认证消息头。


优缺点

HTTP基本认证 是一种十分简单的技术,使用的是 HTTP头部字段强制用户访问网络资源,而不是通过必要的cookie、会话ID、登录页面等非获取访问控制的手段。基本上所有流行的网页浏览器都支持基本认证。基本认证很少在可公开访问的互联网网站上使用,有时候会在小型私有系统中使用如路由器网页管理接口。之后诞生的 HTTP摘要认证用于替代基本认证,允许密钥以相对安全的方式在不安全的通道上传输。

程序员和系统管理员有时会在可信网络环境中使用基本认证。由于基本认证使用的是Base64,可解码成明文,因此使用Telnet等网络协议工具进行监视时,可以直接获取内容,并用于诊断。基本认证并没有为传送凭证英语:transmitted credentials提供任何机密性的保护。仅仅使用 Base64 编码并传输,而没有使用任何加密或散列算法。因此,基本认证常常和 HTTPS 一起使用,以提供机密性。

现存的浏览器保存认证信息直到标签页或浏览器被关闭,或者用户清除历史记录。HTTP没有为服务器提供一种方法指示客户端丢弃这些被缓存的密钥,这意味着服务器端在用户不关闭浏览器的情况下,并没有一种有效的方法来让用户退出。同时 HTTP 并没有提供退出机制,但在一些浏览器上,存在清除凭证credentials 缓存的方法。

HTTP 协议提供了一系列认证(Basic Auth)功能,这些功能只要在 HTTP Web Server 上配置即可,比较便捷。


1、简介
在HTTP中,基本认证Basic access authentication是一种用来允许网页浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式。

2、访问形式
1)、使用浏览器
在使用浏览器访问设置了 HTTP Basic Auth 的服务器时,会弹出对话框,输入用户名和密码即可。

2)、使用 HTTP Client 工具
http://user:passwd@somehost.org/basic-auth/authme/

3、原理
这一个典型的HTTP客户端和HTTP服务器的对话,服务器安装在同一台计算机上localhost,包含以下步骤:
客户端请求一个需要身份认证的页面,但是没有提供用户名和口令。这通常是用户在地址栏输入一个URL,或是打开了一个指向该页面的链接。
服务端响应一个401应答码,并提供一个认证域。
接到应答后,客户端显示该认证域通常是所访问的计算机或系统的描述给用户并提示输入用户名和口令。此时用户可以选择确定或取消。
用户输入了用户名和口令后,客户端软件会在原先的请求上增加认证消息头,然后重新发送再次尝试。
其名称与值的形式是这样的:Authorization: Basic base64encode(username+":"+password)

在本例中,服务器接受了该认证屏幕并返回了页面。如果用户凭据非法或无效,服务器可能再次返回401应答码,客户端可以再次提示用户输入口令。

注意:客户端有可能不需要用户交互,在第一次请求中就发送认证消息头。

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合 RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth。来看看其他几种流行的认证方式。


OpenID


OpenID具有开放性以及分散式的特点。它不基于某一应用网站的注册程序,而且不限制于单一网站的登录使用。OpenID帐号可以在任何OpenID应用网站使用,从而避免了多次注册、填写身份资料的繁琐过程。简单言之,OpenID就是一套以用户为中心的分散式身份验证系统,用户只需要注册获取OpenID之后,就可以凭借此 OpenID帐号在多个网站之间自由登录使用,而不需要每上一个网站都需要注册帐号。

OAuth


OAuth开放授权是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源如照片,视频,联系人列表,而无需将用户名和密码提供给第三方应用。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统例如,视频编辑网站)在特定的时段例如,接下来的2小时内内访问特定的资源例如仅仅是某一相册中的视频。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。下面是OAuth2.0的流程:


这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;

Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;

Token Auth


Token Auth的优点

Token机制相对于Cookie机制又有什么好处呢?

支持跨域访问:Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输。
无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息。
更适用CDN::可以通过内容分发网络请求你服务端的所有资料如:javascript,HTML,图片等,而你的服务端只要提供API即可。
去耦:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可。
更适用于移动应用:当你的客户端是一个原生平台iOS, Android,Windows 8等时,Cookie是不被支持的你需要通过Cookie容器进行处理,这时采用Token认证机制就会简单得多。
CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF跨站请求伪造的防范。
性能:一次网络往返时间通过数据库查询session信息总比做一次HMACSHA256计算 的Token验证和解析要费时得多。
不需要为登录页面做特殊处理:如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理。
基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库.NET, Ruby, Java, Python, Perl和多家公司的支持如:Firebase,Google, Microsoft。


基于JWT的Token认证机制实现

JSON Web TokenJWT是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

JWT的组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

载荷Payload

{ "iss": "Online JWT Builder",
  "iat": 1416797419,
  "exp": 1448333419,
  "aud": "www.example.com",
  "sub": "jrocket@example.com",
  "GivenName": "Johnny",
  "Surname": "Rocket",
  "Email": "jrocket@example.com",
  "Role": [ "Manager", "Project Administrator" ]
}

iss: 该JWT的签发者,是否使用是可选的;
sub: 该JWT所面向的用户,是否使用是可选的;
aud: 接收该JWT的一方,是否使用是可选的;
exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;
其他还有:
nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟,是否使用是可选的;

将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload载荷。

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码。

头部Header

JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
"typ": "JWT",
"alg": "HS256"
}

在头部指明了签名算法是HS256算法。当然头部也要进行BASE64编码,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

签名Signature

将上面的两个编码后的字符串都用句号.连接在一起头部在前,就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0

最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥secret。如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

在我们的请求URL中会带上这串JWT字符串:
https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

认证过程

下面我们从一个实例来看如何运用JWT机制实现认证:

登录

第一次认证:第一次登录,用户从浏览器输入用户名/密码,提交后到服务器的登录处理的Action层Login Action;
Login Action调用认证服务进行用户名密码认证,如果认证通过,Login Action层调用用户信息服务获取用户信息包括完整的用户信息及对应权限信息;
返回用户信息后,Login Action从配置文件中获取Token签名生成的秘钥信息,进行Token的生成;
生成Token的过程中可以调用第三方的JWT Lib生成签名后的JWT数据;
完成JWT数据签名后,将其设置到COOKIE对象中,并重定向到首页,完成登录过程;


请求认证

基于Token的认证机制会在每一次请求中都带上完成签名的Token信息,这个Token信息可能在COOKIE中,也可能在HTTP的Authorization头中;


客户端APP客户端或浏览器通过GET或POST请求访问资源页面或调用API;
认证服务作为一个Middleware HOOK 对请求进行拦截,首先在cookie中查找Token信息,如果没有找到,则在HTTP Authorization Head中查找;
如果找到Token信息,则根据配置文件中的签名加密秘钥,调用JWT Lib对Token信息进行解密和解码;
完成解码并验证签名通过后,对Token中的exp、nbf、aud等信息进行验证;
全部通过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
如果权限逻辑判断通过则通过Response对象返回;否则则返回HTTP 401;

对Token认证的五点认识

对Token认证机制有5点直接注意的地方:

一个Token就是一些信息的集合;
在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率;
服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查;
基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;
因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;


再来回头看看Web服务器的原生的认证功能(以经典apache为例)

httpd对web身份认证的支持很丰富,提供的控制也非常细致。无疑,功能丰富意味着模块多。关于完整的模块见此处http://httpd.apache.org/docs/2.4/mod/ ,其中mod_authX_XXX都是和认证有关的模块。要实现最基本的帐户认证访问控制,只需几个常见的模块即可:mod_authz_core,mod_authz_user,mod_authz_host...。

1.1、htpasswd命令

htpasswd用于为指定用户生成基于网页用户身份认证的密码,由httpd-tools软件包提供。支持3种加密算法:MD5、SHA和系统上的crypt()函数,不指定算法时,默认为md5。
htpasswd [ -c ] [ -m ] [ -D ] passwdfile username
htpasswd -b [ -c ] [ -m | -d | -p | -s ] [ -D ] passwdfile username password
htpasswd -n [ -m | -d | -s | -p ] username
htpasswd -nb [ -m | -d | -s | -p ] username password

选项说明:
passwdfile:包含用户名及其密码的用户密码文件。如果使用了"-c"选项,则会创建或覆盖文件。不使用"-n"选项时必须指定passwdfile参数。
username:为指定的用户名创建密码。如果该用户记录已存在,则更新。
-c:创建用户密码文件passwdfile,如果文件已经存在则会覆盖已存在的文件。不能和"-n"一起使用。
-n:在标准输出中输出结果,而不是将其写入到用户密码文件中。该选项会忽略用户密码文件passwdfile参数。不能和"-c"选项一起使用。
-m:使用MD5加密算法。默认。
-d:使用crypt()函数计算密码,不安全。
-s:使用SHA加密算法。安全。
-P:强制不加密密码,保持明文状态,不安全。
-B:强制bcrypt加密密码,非常安全。
-D:从用户密码文件中删除指定的用户及其密码。
-b:使用批处理模式,即非交互模式,可以直接待加密的传递明文密码。
password:指定要输入的明文密码。只能在批处理模式中使用,即和"-b"一起使用。

例如:
(1).使用"-n"选项直接将结果输出到标准输出而不创建passwdfile。
# htpasswd -n Jim
New password:
Re-type new password:
Jim:ZKHud9tziGucY

(2).使用批处理模式直接传递密码。
# htpasswd -nb Jim 123456 ; htpasswd -nb Jim 654321
Jim:r.BF8RVw56BOA
Jim:xXoNgOS8nN3LQ

可以发现密码完全是随机的,这是因为加了盐。

(3).创建用户密码文件passwdfile。
# htpasswd -cb Bobfile Bob 123456
# cat Bobfile
Bob:fvUxzB3kcnDPk

(4).删除用户文件中的某用户。
# htpasswd -D Bobfile Bob

(5).使用sha和md5加密算法计算密码。
# htpasswd -mb Bobfile Bob 123456
# cat Bobfile
Bob:$apr1$bllkodFt$GUmeb000ngOAschs1SBgq0
# htpasswd -sb Bobfile Bob 123456
# cat Bobfile  
Bob:{SHA}fEqNCco3Yq9h5ZUglD3CZJT4lBs=

1.2、身份认证类基本指令

AuthType:指定web身份认证的类型。有效值为none、basic、digest以及form。通常最基本的认证使用的是文件认证,所以通常使用basic。
AuthName:设置身份认证时的提示信息。
AuthUserFile file-path:指定web用户认证列表。由htpasswd命令生成。
AuthGroupFile file-path:指定组认证文件,文件中分组格式为"mygroup: Jim Bob Alice"。如果文件路径为相对路径,则相对于ServerRoot

基于basic类型的认证就这么几个指令,最主要的还是require指令的使用。更多的认证方法见官方手册的auth类模块。

1.3、Require指令

该指令只能放在Directory容器中,用于控制对目录的访问权限。它的主要功能是由mod_authz_core模块提供,但有些身份认证类模块也提供它额外的功能,这时它可以放在< Directory >、< Files >或< Location >容器中。

主要功能:
Require all granted
无条件允许所有人访问该目录

Require all denied
无条件拒绝所有人访问该目录

Require env env-var [env-var] ...
只有给定的环境变量var-env已经定义才允许访问该目录

Require method http-method [http-method] ...
只有给定的HTTP请求方法才允许访问该目录,如只允许GET才能访问

Require expr expression
只有给定的表达式为true才允许访问该目录

身份认证类模块提供的require指令功能包括:

mod_authz_user为require指令提供的功能:
    Require user userid [userid] ...:认证列表中只有指定的userid才能访问
    Require valid-user:认证列表中的所有用户都可以访问

mod_authz_groupfile为require指令提供的功能:
    Require group group1 [group2] ...:指定组内的用户都可以访问

本地文件系统身份参考类:
    Require file-owner:要求web用户名必须和请求文件的uid对应的username完全相同
    Require file-group:要求web用户名必须为请求文件的gid组中的一员

mod_authz_host为require指令提供的ip和host功能:
    Require ip 192.168.1.104 192.168.1.205
    Require ip 10.1
    Require ip 10 172.20 192.168.2
    Require ip 10.1.0.0/255.255.0.0
    Require ip 10.1.0.0/16
    Require host www.example.org
    Require host example.org
    Require host .net example.edu
    Require local

可以在require指令后紧跟not关键字,表示取反。例如"require not group group1"、"require not local"等。还支持require条件容器,包括< RequireAll >、< RequireAny >和< RequireNone >,当require指令没有写在任何Require容器中时,它们隐式包含在一个< RequireAny >容器中。

< RequireAll >:其内封装的Require指令必须全都不能失败,且至少有一个成功时,该容器成功。如果其内所有指令既不成功又不失败,则该容器中立。其余所有情况都会导致该容器失败。
< RequireAny >:其内封装的Require指令只要有一个成功,该容器就成功。如果其内所有指令既不成功又不失败,则该容器中立。其余所有情况(即全部失败时)都会导致该容器失败。
< RequireNone >:其内封装的Require指令只要有一个成功时该容器就失败,否则就中立。

1.4、web身份认证示例

以最常见的Basic认证方式为例。支持基于用户的认证和基于组的认证。

1.4.1 基于用户的认证
先创建一个web用户及其密码列表文件。其内有4个用户:Jim、Bob、Alice和Tom。
# htpasswd -cb /usr/local/apache/freeoa.pass Jim 123456
# htpasswd -b /usr/local/apache/freeoa.pass Bob 123456
# htpasswd -b /usr/local/apache/freeoa.pass Alice 123456
# htpasswd -b /usr/local/apache/freeoa.pass Tom 123456

修改httpd配置文件,假设只有www.a.com中的a.com目录才需要认证且只有Jim和Bob可以认证,而其他目录以及www.b.com不需要认证,其他用户认证不通过。
<VirtualHost 192.168.10.10:80>
    ServerName www.a.com
    DocumentRoot /usr/local/apache/htdocs/a.com
    <Directory /usr/local/apache/htdocs/a.com>
                AllowOverride Authconfig
                AuthType Basic
                AuthName "please enter your name & passwd"
                AuthUserFile freeoa.pass
                Require user Jim Bob
    </Directory>
</VirtualHost>

<VirtualHost 192.168.10.10:80>
    ServerName www.b.com
    DocumentRoot /usr/local/apache/htdocs/b.com
</VirtualHost>

此处AuthUserFile使用的相对路径,所以该文件必须放在ServerRoot(我的测试环境ServerRoot为/usr/local/apache)下。且Require user行可以替换为"Require valid-user"表示freeoa.pass中的所有用户都允许认证。然后重启httpd,并修改客户端hosts文件,再测试访问。

1.4.2 基于组的认证
基于组的认证只需创建一个组文件,文件中包含的是组名和组中用户成员。例如,将Tom和Alice加入到allow组,使它们也可以访问a.com目录。
# echo 'allow:Tom Alice' >/usr/local/apache/auth_group

修改配置文件,例如:
<VirtualHost 192.168.100.14:80>
    ServerName www.a.com
    DocumentRoot /usr/local/apache/htdocs/a.com
    <Directory /usr/local/apache/htdocs/a.com>
                AllowOverride Authconfig
                AuthType Basic
                AuthName "please enter your name & passwd"
                AuthUserFile freeoa.pass
                AuthGroupFile auth_group
                Require user Jim Bob
                Require group allow
    </Directory>
</VirtualHost>

<VirtualHost 192.168.100.14:80>
    ServerName www.b.com
    DocumentRoot /usr/local/apache/htdocs/b.com
</VirtualHost>

再重启进行测试。