I. 认证和授权
从基于计算机的应用出现伊始,几乎每个开发者在其职业生涯内都会面对的一个最常见也是最复杂的问题,就是安全性(security)。这类问题意味着要考虑理解由谁提供什么数据/信息,此外还有关乎时间、校验、再校验等诸如此类的很多其他方面的事情。
而和安全性相关的所有关注点都可以被分解成两类问题:认证(Authentication) 和 授权(Authorization)。
认证
认证是这样一种验证过程:通过让用户、网站、应用程序通过提供合法证书或验证方式,以证明他们符合自己所宣称的身份。认证经常通过用户名和密码证实,有时也会辅以一些其他的只为用户所知的信息。这类信息或元素称为因子(factors)。基于这些因子,任何认证机制都可以划分为以下三类:
单因子认证: 只依赖用户名和密码
双因子认证: 除了用户名和密码,也需要一块保密信息(比如银行网站可能要求用户输入一个只有自己知道的 PIN)
多因子认证 (MFA): 使用两个或多个、来自不同类别的安全性因子(如医院系统需要用户名密码 + 用户智能手机收到的安全验证码 + 指纹信息)
授权
授权指的是一个验证某用户能访问什么的过程。在授权过程中,某用户/应用程序的权限级别被确定后,才被允许访问特定的 APIs/模块。通常,授权发生在用户身份被 认证 之后。
授权是通过使用“策略(policies)”和“规则(rules)”来实现的。
认证 vs 授权
虽然认证和授权常常交替着使用,但可以试着用一个“苏打水和鸡尾酒”的比喻来理解:二者殊为不同 – 苏打水作为一种原材料,可以被用来制作多种不同的饮料,也可以单独饮用;而鸡尾酒则是一种由多种成分构成的混合品,苏打水也可能是其中之一,但不会只包含这一种。
如此说来,说苏打水等同于鸡尾酒或鸡尾酒就是冒泡的苏打水都是不正确的。相似的是,认证和授权也不是同样的术语;实现得好的话它们可以相得益彰,但本质上是不同的。
在真实场景中,要结合使用认证和授权以保护资源。当你能证明自己的身份之前,不应该被允许访问资源;而即使证明了身份,若无访问权限,依然应被拒绝。
实现机制
要实现认证和授权有多种途径,但时下最流行的是 “基于令牌(token-based)” 的方法。
什么是基于令牌的认证?
基于令牌的认证和授权(Token-based authentication/authorization)是这样一种技术:当用户在某处输入一次其用户名和密码后,作为交换会得到一个唯一生成的已加密令牌。该令牌随后会替代登陆凭证,用以访问受保护的页面或资源。
这种方式的要点在于确保每个发往服务器的请求都伴随着一个已签名的令牌,服务器利用该令牌核验真实性之后才对当次请求做出响应。
令牌是什么?
一个“令牌”就是服务器生成的一段数据,包含了唯一性识别一个用户的信息,一般被生成为一长串随机字符和数字。
比如看起来可能像这样:cc7112734bbde748b7708b0284233419 ,或更复杂些比如: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXNzYWdlIjoiSldUIFJ1bGVzISIsImlhdCI6MTQ1OTQ0ODExOSwiZXhwIjoxNDU5NDU0NTE5fQ.-yIVBD5b73C75osbmwwshQNRC7frWUYrqa TjTpza2y4。
这个令牌本身是无意义和无用的,但结合适当的令牌化系统,就会变成保证应用安全性的重要一环。
为何要使用令牌?
比之于传统的 cookies 等手段,使用令牌有如下好处:
无状态:令牌是自包含(self-contained)的,其包含了所有用于认证的信息。这对于可扩展性是极佳的,可以让服务器从不得不存储 session 的境地中解脱
可以在任何地方生成:令牌的生成和校验是解耦的,让使用单独的服务器甚至不同的厂商来完成令牌的签名成为了可能的选项,如 Auth0(译注:一家 ‘Identity-as-a-service’ 提供商)
细粒度的访问控制:通过令牌负荷(token payload),不仅是用户可访问的资源这一项,也可以轻易制定更多用户角色和权限
基于令牌的实现
尽管具体实现各有不同,但基本上都涉及以下步骤:
用户通过用户名和密码请求访问
应用验证凭证
应用向客户端发放已签名的令牌
客户端存储令牌,并将其附加在其后的每次请求中一同发送
服务器验证令牌并响应数据
虽说并无关于该如何实现你的应用的限制,但 IETF(Internet Engineering Task Force,互联网工程任务组) 还是定义了一些标准。其中最流行的有两个:
OAuth 2.0 (RFC 6749 and RFC 6750).
JWT (RFC 7519).
II. 了解 OAuth 2.0
我们已经刷新了关于认证和授权的认知,并将了解基于令牌认证的常识。在本章节中,来看看最常用的一种实现:OAuth 2.0。
OAuth 2.0 简介
OAuth 2.0 是一个开放标准,它允许用户提供一个令牌而不是用户名和密码来访问他们存储在第三方服务上的资源。这种机制让用户能够授权第三方应用访问他们存储在另一服务提供者上的信息,而不需要将用户名和密码提供给第三方应用。OAuth 2.0 专注于授权(即决定用户是否有权访问特定资源),而不是认证(即验证用户的身份)。
在传统 C/S 模型中,客户端(client)通过让服务器认证资源拥有者(resource owner)的凭证来请求服务端受保护的资源。资源拥有者会将自己的凭证分享给第三方应用(third-party applications),让后者得以访问受限资源。这种凭证分享行为会造成若干问题和限制,其中的一些如下所列:
第三方应用需要存储资源拥有者的凭证以持续利用,典型的如存储一个明文的密码
尽管存在密码固有的安全性弱点,服务器仍得支持密码认证
对于资源拥有者的受限资源,第三方应用得到了过于宽泛的访问权限;置资源拥有者于无力约束访问时长或限制访问资源子集的境地
资源拥有者无法撤回个别第三方的访问权限,除非改变所有第三方的密码
OAuth 针对这些问题提出了引入一个认证层,并把客户端(client)的角色与资源拥有者的角色分离开来。所以:
OAuth 是一种授权协议,以允许用户将对其在一个站点上的资源的受限访问许可给另一个站点,而不必公开其凭据
OAuth 为客户端提供一种“安全代理访问”能力,用以代表资源拥有者访问服务器资源。OAuth 指定了这样一个过程:资源拥有者在不分享其凭证的前提下授权第三方访问其服务器资源。
OAuth 2.0 术语
角色(Roles): OAuth2.0 规范定义了四种角色。
资源拥有者 Resource Owner:一个有能力对访问受保护资源授权的实体(entity)。当这个实体是一个人时,它就表示终端用户(end-user)。
资源服务器 Resource Server:存储受保护资源的服务器,能接受和响应使用访问令牌的受保护资源请求。
客户端 Client:一个发起对受保护资源请求的应用程序,其代表了资源拥有者并持有其凭证。术语 “client” 并不意味着任何实现特征(如该应用程序是否运行在服务器上、桌面端,或是其他设备上)。
授权服务器 Authorization Server:当资源拥有者认证成功并获得授权之后,该服务器向客户端授予访问令牌。
OAuth 2.0 控制流
3. 了解 JWT
下面来看看 JWT。
JSON Web Token (JWT),通常读作 “jot”,是一个定义了以 JSON 对象紧凑而自包含的在各方之间安全传输信息的标准。其包含了声明方面的信息,特别的被用于如 HTTP 等空间受约束的环境;该信息可被验证,也是可信的,因为经过了数字化签名。JWT 可以用 密钥(如 HMAC) 或 公钥私钥对(RSA 或 ECDSA) 签名。
JWT 的两个特性是:
紧凑 Compact: 因为其相对较小的尺寸,JWT 可以借由 URL 发送, 作为一个 POST 参数,或在一个 HTTP header 内。
自包含 Self-contained: 一个 JWT 包含了所有关于一个实体的所需信息,以避免多次查询数据库。JWT 的接纳者同样无需调用服务器以验证令牌。
这些令牌可以是被签名的、被加密的,或两者皆有。签名过的令牌被用来验证令牌完整性,而加密过的令牌用来隐藏声明。
JWT 术语
JWT 表现为由点(.)分割的三个字符串组成的一个序列,典型的格式看起来如下:
AAAAA.BBBBB.CCCCC
三个子串分别称作 头部(Header) 、 负载(Payload) 和 签名(Signature),下面逐一讲解:
头部 Header
虽说只要相关几方之间有共识,则在头部中放什么是没有限制的,但通常由两部分组成:
typ: 表示令牌类型(type),值为 JWT
alg: 表示签名此令牌的算法(algorithm),如 HMAC、RSA、SHA
负载 Payload
JWT 的第二部分表示负载,这部分由声明(claims)组成。
所谓声明就是关于实体和任意附加数据的信息。在一段 JWT 中,声明由键表示。这些声明是依赖上下文的,且应该相应的被处理和被理解,但依每种规范会有若干标准规则应用于声明:
在一个 JWT 声明集合中,每个声明的名称必须是唯一的
对于 JWT 的处理逻辑,必须 保证这种唯一性,要么拒绝重复的名字,要么用一个 JSON 处理器返回重复项中词法上最后一个名字
使用 JWT 的应用要明确其选用的声明标准,并定义必须项和可选项
因为 JWT 的核心目标之一就是精简,故所有名字也应该简短
一个可能的负载例子:
{
"sub": "1234567890", "name": "Alice", "admin": true
}
负载中的声明又可以细分为以下三种类型:
已注册的声明
有一些声明注册在 IANA(https://dzone.com/refcardz/core-json) 的 “JSON Web 令牌声明” 注册表中。这些声明并非是在所有情况下都要求强制使用或实现的,准确的说它们是作为提供一个有用的集合的起始点而被注册的。
其中一些有必要了解的是:
iss (issuer): 声明了发行人,也就是发行 JWT 的主体。处理此声明通常是因应用而异的。“iss” 值是一个大小写敏感的字符串,包含一个普通字符串或者一个 URL。该声明是可选的
sub (subject): 表示 JWT 的主体 (用户)。值必须要么是全局唯一的,要么在发行人上下文范围内局部唯一。处理该声明通常也是因应用而异的。“sub” 值是一个大小写敏感的字符串,包含一个普通字符串或者一个 URL。该声明是可选的
aud (audience): 表示 JWT 的目标接收方。如果当该声明存在且处理该声明的一方不能通过 “aud” 的值进行自我身份验证时,则 JWT 必须被拒绝。大多数情况下,这个值是由大小写敏感的字符串(包含一个普通字符串或者一个 URL)组成的数组。该声明是可选的
exp (expiration): 表示过期时间,即等于或晚于那个时刻再处理 JWT 则绝不可被接受。其值通常是以秒记的时间戳(译注:按 POSIX 中定义的 “seconds since epoch” 标准,也就是 PHP 等语言中常用的那种)。该声明是可选的
nbf (not before) : 表示一个时间,即早于那个时刻再处理 JWT 则绝不可被接受。其值通常是以秒记的时间戳。该声明是可选的
iat (issued at): 表示发出 JWT 的时刻。可用于判断 JWT 的寿命。必须是一个时间戳。该声明是可选的
jti (JWT ID): 为 JWT 提供一个唯一的身份识别符,其值必须难以重复,以防 JWT 被重复执行。该声明是可选的
公开声明
此类声明的名字可被 JWT 使用者任意定义。但为了预防冲突,任何新名字都应该注册在 IANA “JSON Web Token Claims” 注册表中,或将其定义为包含防冲突命名空间的 URI 等。
在任何情况下,对名字和值的定义都要考虑到合理的预防措施,以确保它们在其定义的命名空间中受控。
私有声明
这可以理解为是创建自定义声明以在应用内共享信息规格,可以是除以上两种外的任意声明名字。与公有声明不同,私有声明受制于冲突问题,要小心使用。
签名
签名先是通过对头部和负载 Base64 编码而生成,其后会与一个密钥联合,最好被头部中指定的算法签名。
签名被用于校验 JWT 的发送者是否名实相符,以及信息在传送过程中是否被更改。比如,如果创建了一个使用 HMAC SHA256 算法之令牌的签名,你会像下面这样做:
HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
一个更完整的例子
观察如下 JWT 签名:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzYXRpc2giLCJhdWQiOiJteWFwcCIsIkNVU1QiOiIxIiwiZXhwIjoxNTY2MjE0NTg1LCJpc3MiOiJhdXRoLWFwcCJ9.WknG6jiM_vAaflLnKyjlXh5BrM4MUJR9dFrVx-XE3zRVWiyXeIVzI-OomFh0vVHRwrK3-Tttg0HyKBTnCA3mSg
该签名使用了 HS512 算法编码,并包含了如下信息:
Header: {
"alg": "HS512",
"typ": "JWT"
}
Payload: {
"sub": "satish",
"aud": "myapp",
"CUST": "1",
"exp": 1566214585,
"iss": "my-auth-app"
}
JWT 优势:
JWT(JSON Web Tokens)作为一种用于在网络应用间安全传输信息的令牌格式,具有以下优势:
自包含性:JWT可以包含用户的所有认证信息,不需要在每次请求时查询用户信息,减少了数据库查询或外部服务的调用。
紧凑性:由于是JSON格式的,JWT非常紧凑,可以通过URL、POST参数或HTTP头部等方式在网络上传输。
跨语言:JWT是基于JSON的,几乎所有的编程语言都能够处理JSON,因此JWT可以在多种语言之间无缝使用。
安全性:
JWT可以使用HMAC、RSA或ECDSA等算法进行签名,确保令牌在传输过程中未被篡改。
可以使用RSA或ECDSA公钥/私钥对进行加密,保证令牌内容的安全性。
无状态:由于JWT是自包含的,服务器不需要存储会话信息,因此是无状态的,这使得JWT非常适合在分布式系统中使用。
可扩展性:JWT可以轻松地添加自定义字段,使得它能够适应各种复杂的需求。
多种用途:JWT不仅可以用于身份验证,还可以用于信息交换,例如在API网关和微服务架构中传递用户信息和权限。
标准化:JWT是一个开放标准(RFC 7519),有许多库和框架支持JWT的生成、解析和验证。
性能:由于不需要在服务器端存储会话状态,JWT可以减少服务器的内存占用,提高应用程序的性能。
跨域认证:JWT可以轻松实现跨域认证,允许单点登录(SSO)解决方案在不同域名或子域之间共享认证状态。