第5章 架构安全性
计算机系统的安全,不仅仅是指"防御系统被黑客攻击"这样狭义的安全,还至少应包括以下问题的具体解决方案:
1.认证(Authentication)
系统如何正确分辨操作用户的真实身份。
2.授权(Authorization)
系统如何控制一个用户该看到什么数据,操作哪些功能?
3.凭证(Credential)
系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确的、完整且不可抵赖的。
4.保密(Confidentiality)
系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用?
5.传输(Transport Security)
系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充?
6.验证(Verification)
系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险?
5.1 认证
认证(你是谁)、授权(你能干什么)和凭证(你如何证明)可以说是一个系统中最基础的安全设计。账户和权限的管理往往由专门的基础设施来负责,
如微软的活动目录(Active Directory, AD)或者轻量目录访问协议(LDAP)。
5.1.1 认证的标准
架构安全性的经验原则:以标准规范为指导、以标准接口去实现。安全涉及的问题很麻烦,但解决方案已经很成熟,对于99%的系统来说,在安全上
不去做轮子,不去想发明创造,严格遵守标准,就是最恰当的安全设计。
主流的三种认证方式:
1.通信信道上的认证
你和我建立通信连接之前,要先证明你是谁。在网络传输场景中的典型应用就是基于ssl/tls传输安全层的认证。
2.通信协议上的认证
你请求获取我的资源之前,要先证明你是谁。在万维网场景中典型的应用是HTTP协议的认证。
3.通信内容上的认证
你使用我提供的服务之前,要先证明你是谁。在万维网场景中典型应用是基于web内容的认证。
1.HTTP认证
HTTP协议的通用认证框架,要求所有支持http协议的服务器,在未授权的用户意图访问服务端保护区域的资源时,应返回401 Unauthorized
的状态码,同时应在响应报文头里附带以下两个分别代表网页认证和代理认证的Header头之一,告知客户端应采取何种方式能代表访问者身份的凭证
信息:
WWW-Authenticate:<认证方案> realm=<保护区域的描述信息>
Proxy-AUthenticate:<认证方案> realm=<保护区域的描述信息>
接到该响应后,客户端必须遵守服务端指定的认证方案,在请求资源的报文头中加入身份凭证信息,由服务端核实后通过后才会允许该请求正常
返回,否则返回403 Forbidden错误。请求报文头应包含以下Header项之一:
Authorization:<认证方案><凭证内容>
Proxy-Authorization:<认证方案><凭证内容>
HTTP认证框架提出认证方案是希望能把认证"要产生的身份凭证"的目的与"具体如何产生凭证"的实现分离开来,无论客户端通过生物信息(指纹、
人脸)、用户密码、数字证书亦或者其他方式生成凭证,都属于如何生成凭证的具体实现,都可以包含在http谢雨预设的框架之内。
HTTP Basic 认证是一种主要以演示为目的的认证方案,也应用于一些不要求安全性的场合。Basic 认证产生用户身份凭证的方法是让用户
输入用户名和密码,经过base64编码"加密"后作为身份凭证。大概过程:
a)浏览器收到服务端的响应如下:
HTTP/1.1 401 Unautthorized
WWW-Authenticate: Basic realm="example from icyfenix.cn"
b)弹出对话框,要求用户提供用户名和密码
c)用户输入用户名和密码,如用户名"admin",密码"123456",浏览器会将字符串"admin:123456",经过base64编码,然后发送给服务端;
GET /admin HTTP/1.1
Authorization: Basic base64(xxx)
d)服务端接收到请求后,解码后检查用户名和密码,合法就返回"/admin"的资源,不合符就返回 403 Forbidden错误。
除了Basic认证,还有其他的认证方案:
1.Digest
HTTP摘要认证,可视为Basic认证的改良版。针对base64明文发送的危险,Digest认证把用户名和密码加盐(一个被称为Nonce的
变化值作为盐值)后通过 MD5/SHA 等哈希算法取摘要发送出去。但是这种认证依然是不安全的。
2.Bearer
基于 OAuth 2 规范来完成认证。
3.HOBA(HTTP Origin-Bound Authentication)
一种基于自签名证书的认证方案。基于数字证书的信任关系主要有两类模型:一类采用CA层次结构的模型,由CA中心签发证书;另一种
是以IETF的 Token Binding 协议为基础的 OBC(Origin Bound Certificate, 原产地证书)。
HTTP 认证框架中的认证方案是允许自行扩展的,并不要求一定由RFC规范来定义,只要用户代理能够识别这种私有的认证方案即可。
2.Web认证
如果用户想访问信息系统中的具体服务,肯定是希望身份认证是由系统本身的功能去完成的,而不是由HTTP服务器来负责认证。这种依靠内容而不是
传输协议来实现的认证方式,在万维网被称为"Web认证",由于实现形式上登录表单占了绝对主流,因此通常也被称为"表单认证"。
2019年3月,万维网联盟(w3c)批准了由FIDO(Fast IDentify Online)领导起草的世界首个web内容认证的标准"WebAuthn"。
WebAuthn彻底抛弃了传统的密码登录方式,改为直接采用生物识别(指纹、人脸、虹膜、声纹)或者实体密钥(以USB、蓝牙、NFC连接
的物理密钥容器)作为身份凭证,从根本上消灭了用户输入错误产生的校验需求和防止机器人模拟产生的验证码需求等问题,甚至可以省略了表单界面。
WebAuthn 规范覆盖了 "注册"和"认证"两大流程。注册分为如下步骤:
1.用户进入系统的注册页面,这个页面的格式、内容和用户注册时需要填写的信息均不包含在WebAuthn标准的定义范围;
2.当用户填写完信息,点击提交信息的按钮,服务端首先暂存用户提交的数据,生成一个随机字符串(在规范中称为Challenge)和
用户UserID(在规范中称为凭证ID)并返回客户端;
3.客户端的WebAuthn API接收到Challenge和UserID后,把这些信息发给验证器(Authenticator)。验证器可以理解为用户设备上的
TouchID、FacdID、实体密钥等认证设备的统一接口;
4.验证器提示用户进行验证,如果支持多种验证设备,还会提示用户选择一个想要使用的设备。验证的结果是生成一个密钥对,由验证器存储
私钥、用户信息以及当前的域名。然后使用私钥对Challenge进行签名,并将签名结果、UserID和公钥一起返回给客户端;
5.浏览器将验证器返回的结果转发给服务器;
6.服务器核验信息后,检查UserID与之前发送的是否一致,并用公钥解密后得到的结果与之前发送的Challenge作对比,一致即表明注册
通过,由服务端存储该UserID对应的公钥。
登录流程:
1.用户访问登录页面,填入用户名后即可点击登录按钮;
2.服务器返回随机字符串Challenge、用户UserID;
3.浏览器将Challenge和UserID转发给验证器;
4.验证器提示用户进行认证操作。由于在注册阶段验证器已经超出了该域名的私钥和用户信息,所以如果域名和用户都相同的话,就不需要
生成密钥对了,直接以存储的私钥加密Challenge,然后返回浏览器;
5.服务端接收到浏览器转发来的被私钥加密的Challenge,并以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。
WebAuthn 采用非对称加密的公钥、私钥代替传统的密码,这是非常理想的认证方案。私钥是保密的,只有验证器需要知道它,连用户本人都
不知道,也就没有人为泄露的可能。公钥是公开的,可以被任何人看到或存储。公钥可用于验证私钥生成的签名,但不能用来签名,除了得知私钥外,
没有其他途径能够生成可被公钥验证为有效的签名,这样服务器就可以通过公钥是否能解密来判断用户的身份是否合法。
WebAuthn 还解决了传统密码在网络传输上的问题,无论是否在客户端进行加密以及如何加密,对防御中间人攻击都没有意义的。更值得夸赞的是,
WebAUthn为登录过程带来了极大的便利性,不仅注册和验证用户体验十分优秀,而且彻底避免了用户在一个网站上泄露秘密,所有使用相同密码网站都
受到攻击的问题,这个优点使得用户无需再为每个网站想不同的密码。
5.1.2 认证的实现
5.2 授权
授权这个概念通常伴随着认证、审计、账号一同出现,并成为AAAA(Authentication、Authorization、Audit、Account)。安全领域中所说的授权就更具体
一些了,通常涉及以下两个相对独立的问题:
1.确保授权的过程可靠
对于单一系统,授权的过程比较可控,以前语境上提到授权,实质上都是讲访问控制,但理论上两者应该是分开的。在涉及多方的系统中,授权的过程则是一个比较困难
却必须严肃对待的问题:如何既能让第三方系统访问到所需的资源,又能保证其不泄露用户的敏感数据呢?常用的多方授权协议主要有 OAuth 2 和 SAML 2.0。
2.确保授权的结果可控
授权的结果用于对程序功能或者资源的访问控制,成理论体系的权限控制模型有很多,譬如自主访问控制(DAC)、强制访问控制(MAC)、基于属性的访问控制(ABAC)、
还有最常用的基于角色的访问控制(Role-Based Access Control,RBAC)。
5.2.1 RBAC
所有访问控制模型,实质上都是在解决同一个问题:"谁(User)拥有什么权限(Authority)去操作(Operation)哪些资源(Resource)"。
用户(User) => <隶属> => 角色(Role) => <拥有> => 许可(Permission) => <操作> => 资源(Resource)
许可是抽象权限的具体化体现,权限在RBAC系统中的含义是"允许何种操作用于哪些资源上",这句话的具体实例即为"许可"。提出许可这个概念的目的与提出角色的目的是完全
一样的,只是为了更抽象。角色目的为解耦用户与权限之间的多对多关系,而许可为的是解耦操作与资源之间多对多的关系。
5.2.2 OAuth 2
如果把用户名和密码告诉第三方应用,但导致下面的问题:
1.密码泄露
2.访问范围
3.授权回收
只有修改密码才能回收授权的权利,可修改密码意味着所有第三方应用都失效了。
OAuth 2 给出了很多解决办法,这些办法的共同特征是以令牌(Token)代替用户密码作为授权的凭证。有了令牌后,哪怕令牌被泄露,也不会导致密码泄露;令牌上可以设定
访问资源的范围以及时效性;每个应用都有独立的令牌,任何一个失效都不会波及其他。这样上面三个问题都解决了。
术语:
1.第三方应用
需要得到授权访问我的资源的那个应用。
2.授权服务器
能够根据"我"的意愿提供授权的服务器。
3.资源服务器
能够提供第三方应用所需资源的服务器,它与认证服务器可以是相同的服务器,也可以是不同的服务器。
4.资源所有者
拥有授权权限的人,即"我"。
5.操作代理
指用户用来访问服务器的工具,通常是指浏览器。
第三方应用 => 资源所有者:
1.要求用户给与授权
2.同意给与该应用授权
第三方应用 => 授权服务器:
1.我有用户授权,申请访问令牌
2.同意发放访问令牌
第三方应用 => 资源服务器:
1.我有访问令牌,申请开放资源
2.同意开放资源
OAuth 2 的 4 种不同的授权方式:
1.授权码模式(Authorization Code)
开始进行授权过程以前,第三方应用先要到授权服务器上进行注册。所谓注册,是指向认证服务器提供一个域名地址,然后从授权服务器中获取ClientID和
ClientSecret,以便顺利完成如下过程:
1.第三方应用将资源所有者(用户)导向授权服务器的授权页面,并向授权服务器提供ClientID以及用户同意授权后的回调URI,这是第一次客户端页面转向;
2.授权服务器根据ClientID确认第三方应用的身份,用户在授权服务器中决定是否向该身份的应用进行授权,注意,用户认证的过程未定义在此步骤中,在此之前
应该已经完成;
3.如果用户同意授权,授权服务器将转向第三方应用在第1步调用中提供的回调URI,并附带上一个授权码和获取令牌的地址作为参数,这是第二次客户端页面转向;
4.第三方应用通过回调地址收到授权码,然后将授权码与自己的ClientSecret一起作为参数,通过服务器向授权服务器提供的获取令牌的服务器发起请求,
换取令牌。该服务器的地址与注册时提供的域名同处于一个域中;
5.授权服务器核对该授权码和ClientSecret,确认无误后,向第三方应用授予令牌。令牌可以是一个或者两个,其中必定要有的是访问令牌(Access Token),
可选的是刷新令牌(Refresh Token)。访问令牌用于到资源服务器获取资源,有效期较短;刷新令牌用于在访问令牌失效后重新获取,有效期较长;
6.资源服务器根据访问令牌所允许的权限,向第三方应用提供资源。
这个过程几乎考虑到了所有合理的意外情况,再举几个最容易遇到的意外情况,以便更好的理解为何要这样设计OAuth 2。
1.会不会有其他应用冒充第三方应用骗取授权?
ClientID代表一个第三方应用的"用户名",这项信息是完全公开的。但ClientSecret应当只有应用自己知道,这代表了第三方应用的"密码"。
在第5步发放令牌时,调用者必须能够提供ClientSecret才能成功完成。只要第三方应用妥善保管好ClientSecret,就没有人能冒充它。
2.为什么先发放授权码,再用授权码换令牌?
这是因为客户端转向(通常就是一个http 302重定向)对于用户来说是可见的,换言之,授权码可能会暴露给用户以及用户机器上的其他程序,但由于用户
并没有ClientSecret,而只有授权码是无法换取令牌的,所以避免了令牌在传输过程中被泄露的风险。
3.为什么要设计一个时限较长的刷新令牌和时限较短的访问令牌?不能直接把访问令牌的时间调长吗?
这是为了缓解OAuth 2在实际应用中的一个缺陷,通常访问令牌一旦发放,除非超过了令牌的有效期,否则很难有其他代价让它失效,访问令牌的时效性
一般设计的较短,譬如几个小时,如果还继续用,那就定期用刷新令牌去更新,这样授权服务器就可以在更新的过程中决定是否要继续授权。
缺点:
尽管授权码模式是严谨的,但它不够友好。这不仅仅体现在那繁复的调用过程上,还体现在它对第三方应用提出了一个"貌似不难"的要求:第三方应用必须有
应用服务器,因为在第4步要发起服务端转向,而且要求服务端的地址必须与注册时提供的地址在同一个域内。于是就有了OAuth 2的第二种授权模式:隐式授权。
2.隐式授权模式(Implicit)
隐式模式省略掉了通过授权码换取令牌的步骤,整个授权过程不需要服务端支持,一步到位。代价是在隐式授权中,授权服务器不会再去验证第三方应用的身份,因为
已经没哟应用服务器,ClientSecret没有人保管,也就没有存在的意义了。但其实还是会限制第三方应用的回调URI地址必须与注册的时候提供的域名一致,尽管有可能被
dns污染之类的所攻破。同样,隐式授权也不能避免令牌暴露给资源所有者,不能避免用户机器上可能出现的意图不轨的其他程序、HTTP的中间人攻击等风险。
隐式模式和授权模式显著的区别是授权服务器在得到用户授权后,直接返回了访问令牌,这显著降低了安全性,但OAuth 2仍然努力尽可能的做到相对安全,譬如在
前面的隐式授权中,尽管不需要到服务器,但仍然需要在注册时提供回调域名,此时会要求该域名与接受令牌的服务器处于同一个域内。此外,同样基于安全考虑,在隐式
模式中明确禁止发放刷新令牌。
3.密码模式(Resource Owner Password Credentials)
前面说的授权码模式与隐式模式属于纯粹的授权模式,它们与认证没有直接联系,即认证与授权是互相独立的过程。但在密码模式里,认证和授权就被整合到同一个
过程中。
密码模式原本的设计意图是仅限于用户对第三方应用是高度信任的场景中,因为用户需要把密码明文提供给第三方应用,由第三方以此向授权服务器获取令牌。就是
第三方应用拿着用户名和密码向授权服务器换取令牌而已。密码模式下"如何保障安全"的职责无法由 OAuth 2来承担,只能由用户和第三方应用自行保障。
4.客户端模式(Client Credentials)
客户端模式是四种模式中最简单的,它只涉及两个主体:第三方应用和授权服务器。客户端模式是指第三方应用以自己的名义,向授权服务器申请资源许可。此模式
通常用于管理操作或者自动处理类型的场景中。
微服务架构并不提倡同一个系统的各服务间有默认的信任关系,所以服务之间调用也需要先进行认证授权,然后才能通信。此时,客户端模式便是一种常用的服务间
认证授权的解决方案。
5.设备码模式
OAuth 2 中还有一种与客户端模式类似的授权模式,即"设备码模式"。设备码模式用于在无输入的情况下区分设备是否被允许使用,典型的应用便是手机锁网解锁
或者设备激活的过程。
在进行验证的时候,设备需要从授权服务器获取一个URI地址和一个用户码,然后需要用户手动或者设备自动的验证URI中输入用户码。在这个过程中,设备会一直
循环,尝试去获取令牌,直到拿到令牌或者用户码过期为止。
5.3 凭证
对"如何承载认证授权信息"这个问题的不同看法,代表了软件架构对待共享状态信息的两种不同思路:状态应该维护在服务端,还是客户端中?在分布式系统崛起之前,这个问题
原本已经有了较为统一的结论,即以HTTP协议的cookie-session 机制为代表的服务端状态存储在分布式崛起之前的三十年中都是主流的解决方案。不过,到了最近十年,由于
分布式系统中共享数据必然会受到CAP不兼容原理的打击限制,迫使人们重新审视之前已经放弃的客户端状态存储,这就让原本只在多方系统中采用的JWT令牌方案,在分布式系统中
也有了另一块用武之地。
5.3.1 Cookie-Session
HTTP 是无状态的一种传输协议,无状态是指协议对事务处理没有上下文的记忆能力,每一个请求都是独立的。
在HTTP协议中增加了 Set-Cookie 指令,该指令的含义是以键值对的方式向客户端发送一组信息,此信息将在此后一段时间内的每次http请求中,以名为Cookie的Header
附带着重新发给服务端,以便服务端区分来自不同的客户端的请求。如下:
Set-Cookie: id=icyfenix; Expires=Wed, 21 Feb 07:28:00 GMT; Secure; HttpOnly
收到该指令后,客户端再对同一个域的请求回传时就会自动附带键值对信息"id=icyfenix",如下:
Cookie: id=icyfenix
根据每次请求到服务端的cookie,服务端就能分辨出请求来自哪一个用户。由于cookie是放在请求头上的,属于额外的传输负担,不应该携带过多的内容,而且放在cookie是
不安全的,容易被中间人窃取或者篡改,所以不会设置明文信息。一般来说,系统会把状态信息保存在服务端,在cookie里只传输一个无字面意义的、不重复的字符串,习惯上以
sessionid或jsessionid为名,然后服务端会把这个字符串作为key,以key/Entiry的结构存储每一个在线用户的上下文状态,再辅以一些超时自动清理之类的机制措施。
cookie-session 方案在安全性上其实是有一定的先天优势的:状态信息都存储于服务端,只依靠客户端的同源策略和HTTPS的传输层安全,保证cookie中的键值不被窃取
而出现冒用身份的情况,就能完全规避信息在传输过程中被泄露和篡改的风险。cookie-session 方案的另一大优点是服务端有主动的状态管理能力,可根据自己的意愿随时修改、
清除上下文信息,譬如很轻易就让一个用户下线。
cookie-session 在单节点的单体服务中是最合适的方案,但当需要水平扩展服务能力,要部署集群的时候就比较麻烦了,由于session存储在服务器的内存中,当服务器水平
扩展的时候,设计者就必须考虑以下三种方案之一了:
1.牺牲集群的一致性,让负载均衡采用亲和式的负载均衡算法,譬如根据用户的ip或者session来分配节点,每一个特定用户发出的所有请求都一直被分配到其中某一个节点来
提供服务,每个节点都不重复的保存在一部分用户的状态,如果这个节点崩溃了,里面的用户状态便完全消失;
2.牺牲集群的可用性,让各个节点采用复制式的session,每一个节点中的session变动都会同步到组播地址的其他服务器上,这样即使某个节点挂了,也不会中断某个用户的
服务,但session之间组播复制的同步代价高昂,节点超时多,同步成本高;
3.牺牲集群的分区容忍性,让普通的节点不再保留状态,将上下文集中存放在一个所有节点都能访问到的数据节点中存储。此时的矛盾是数据节点成为单点,一旦数据节点损坏
或者出现网络分区,整个集群都将不能提供服务。
在分布式系统中共享的信息,cap就不可能兼得,所以分布式环境中的状态管理一定会受到cap的限制,无论怎么样都不可能完美。但如果只是解决分布式授权下的认证问题,
并顺带解决少量状态的问题,就不一定只能依靠共享存储去实现。JWT令牌只用来处理认证授权问题,充其量只能携带少量的非敏感的信息。是cookie-session在认证授权问题
上的替代品,而不能说JWT比cookie-session更先进。
5.3.2 JWT
cookie-session 机制在分布式环境下会遇到cap不可兼得的问题,而在多方系统中就更不可能谈session层面的数据共享了,哪怕服务间能共享数据,客户端的cookie
也没办法跨域。所以,当服务器存在多个,客户端只有一个时,把状态信息存储在客户端,每次随着请求发回服务器去。当面说过,这样做的缺点是无法携带大量的信息,而且有
泄露和篡改的风险。信息量受限的问题并没有太好的解决方案,不过要确保信息不被中间人篡改还是可以实现的,jwt就是这个问题的标准答案。
JWT(JSON Web Token)定义于RFC7519标准之中,是目前广泛使用的一种令牌格式,尤其经常与OAuth2配合应用于分布式、涉及多方的系统中。它最常见的使用方式是,
附在名为 Authorization 的Header中发送给服务端,前缀在RFC6750中被规定为 Bearer。它是HTTP认证框架中OAuth2认证方案。
jwt只解决篡改的问题,并不解决泄露的问题,因此令牌默认是不加密的,尽管你自己要加密也不难做到,接收时自行解密就行了,但这样做其实没有什么意义。
jwt令牌是以json结构存储的,该结构总体上可划分为三个部分,每个部分用点号"."分隔开:
第一部分:令牌头(Header)
{
"alg":"HS256",
"typ":"JWT"
}
它描述了令牌的类型(统一为typ:JWT)以及令牌签名的算法。
第二部分:负载(Payload)
这个是真正需要向服务端传递的信息。针对认证问题,负载至少应该包含能够告知服务端"这个用户是谁"的信息;针对授权问题,令牌至少应该包含能够告知服务端
"这个用户拥有什么角色/权限"的信息。JWT的负载部分是完全可以自定义的,根据具体要解决的问题不同,设计自己所需要的信息,只是总容量不能太大。如下:
{
"username":"icyfenix",
"authorities": [
"ROLE_USER",
"ROLE_ADMIN"
],
"scope" [
"ALL"
],
"exp":1584948947,
"jti":"123i9120380-1231-123sfs-123dsr-sfsfs",
"client_id":"bookstore_frontend"
}
JWT 在RFC 7519中推荐(非强制)了7项声明名称(Claim Name),如果需要用到,建议与官方保持一致:
1.iss(Issuer):签发人
2.exp(Expiration Time):令牌过期时间
3.sub(Subject):主题
4.aud(Audience):令牌受众
5.nbf(Not Before):令牌生效时间
6.iat(Issued At):令牌签发时间
7.jti(JWT ID):令牌编号
第三部分:签名(Signature)
签名的意思是:使用在对象头中公开的特定签名算法,通过特定的密钥(由服务器保存,不能公开)对前面的两个部分进行加密计算,以例子中使用的jwt默认的
HMAC SHA256 算法为例,通过以下公式产生签名值:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名的意义在于确保负载中的信息是可信的、没有被篡改的,也没有在传输过程中丢失任何信息的。因为被签名的内容哪怕发生了一个字节的变动,也会导致整个
签名发生显著变化。此外,由于签名这件事情只能由认证服务器完成(只有它知道密钥),任何人都无法篡改后重新计算出合法的签名值,所以服务端才能完全信任客户端
传上来的jwt中的负载信息。
jwt默认的签名算法是 HMAC SHA256 是一种带密钥的哈希摘要算法,加密与验证的过程均只能由中心化的授权服务来提供,所以这种方式一般只适用于授权服务
与应用服务处于同一个进程的单体应用。在多方系统或者授权服务于资源服务分离的分布式应用中,通常会采用非对称加密算法来进行签名,这时候除了授权服务端持有的
可以用于签名的私钥外,还会对其他服务器公开一个公钥,公开方式一般遵守 JSON Web Key规范。公钥不能用来签名,但是能被其他服务用于验证签名是否有私钥所
签发。这样其他服务器就能不依赖授权服务器、无需远程通信即可独立判断jwt中的信息真伪了。
在单体服务中,采用默认的HMAC SHA256算法来加密签名,而在Istio服务网格版本里,终端用户认证会由服务网格的基础设施来完成,此时会采用非对称加密的
RSA SHA256算法来进行签名。
jwt是一种多方系统中一种优秀的凭证载体,它不需要任何一个服务节点保存任何一点状态信息,就能够保障认证服务与用户之间的承若是双方当时真实意图的体现,
是准确、完整、不可篡改、且不可抵赖的。同时,由于jwt本身可以携带少量的信息,这十分有利于restful API的设计,能够较容易的做成无状态服务,在做水平扩展
时就不需要像前面cookie-session方案那样考虑如何部署的问题。现实中也确实有一些项目直接采用jwt来承载上下文以实现完全无状态的服务端,这能获得任意加入
或者移除服务节点的巨大便利,天然具备完美的水平扩展能力。而对于有状态的系统,就必须通过重新登录、进行前置业务操作来为服务端重建状态。尽管大型系统中
只用jwt来维护上下文状态,服务端完全不持有状态是不太现实的,不过将热点的服务单独抽离出来做成无状态,仍是一种提升系统吞吐能力的架构技巧。
jwt缺点:
1.令牌难以主动失效
jwt一旦签发,理论上就和认证服务器没有什么瓜葛了,在到期之前就会始终有效,除非服务器部署额外的逻辑去处理失效问题,这对某些管理功能的实现
很不利。譬如,要求一个用户只能在一台设备上登录,在B设备登录之后,之前已经登录过的A设备就应该自动退出。如果采用jwt,就必须设计一个"黑名单"
的额外逻辑,用来把要主动失效的令牌集中存储起来,而无论这个黑名单是实现在sessioin、redis或者数据库中,都会让服务退化为有状态服务,降低了
jwt本身的价值,但在黑名单中使用Jwt时依然是很常见的做法,需要维护的黑名单一般是很小的状态量,在许多场景中还是有存在价值的。
2.相对更容易遭受重放攻击
首先说明cookie-session也是有重放攻击的问题,只是因为session中的数据控制在服务端上,在应对重放攻击的时候相对主动一些。要在jwt层面
解决重放攻击是需要付出比较大的代价的,无论是加入全局序列化(https的思路)、Nonce字符串(http Digest验证的思路),挑战应答码(当下网银动态
令牌的思路),还是缩短令牌有效期强制频繁刷新令牌,在真正应用时都很麻烦。真正处理重放攻击,建议的解决方案是在信道层次(譬如开启https)上解决,
而不是在服务层次(譬如在令牌或者接口其他参数上增加额外的逻辑)上解决。
3.只能携带相当有限的数据
HTTP协议并没有强制约束header的最大长度,但是,各种服务器、浏览器都会有自己的约束,比如tomcat 就要求header不超过8KB,而在nginx中
默认是4KB,因此在令牌中存储过多的数据不仅耗费传输带宽,还有额外的出错风险。
4.必须考虑令牌在客户端如何存储
严谨的说,这个并不是jwt的问题而是系统设计的问题。如果授权之后,操作完关闭浏览器就结束了,那把令牌放到内存里面,压根就不考虑持久化那是
最理想的方案。但并不是谁都能忍受一个网站关闭之后下次就一定强制要重新登录。这样的话,想想客户端该把令牌放在哪里?cookie?localStorage?
Indexex DB? 它们都是有可能泄露的,别人就可以冒充用户的身份做任何事情。
5.无状态也不总是好的
5.4 保密
保密是加解密的统称。按照要保密的信息所处的环节不同,可以划分为"信息在客户端时的保密","信息在传输时的保密","信息在服务端时的保密"三类,或者进一步概括
为"端的保密"和"链路的保密"两类。
5.4.1 保密的强度
保密是有成本的。以用户登录为例,列举几种不同强度的保密手段,讨论优缺点:
1.以摘要代替明文:如果密码本身比较复杂,那一次简单的哈希摘要至少可以保证即使传输过程中有信息泄露,也不会被逆推出原信息;即使密码在一个系统中泄露了,
也不至于威胁到其他细到的使用。但这种处理不能防止弱密码被彩虹表所破解;
2.先加盐再做哈希是应对弱密码的常用方法:盐值可以为弱密码建立一道防御屏障,一定程度上防御已有的彩虹表攻击,但不能阻止加密结果被监听、窃取后,攻击者
直接发送加密结果给服务端进行冒认;
3.将盐值变为动态值能有效防止冒认:如果每次密码向服务端传输时都掺入了动态的盐值,让每次加密的结果都不同,那即使传输给服务端的加密结果都被窃取了,也
不能冒用来进行另外一次调用。尽管在双方通信均可能泄露的前提下协商出只有通信双方才知道的保密信息是完全可行的,但这样协商盐值的过程将变得极为复杂,而且
每一次协商只保护一次操作,也难以阻止对其他服务的重放攻击;
4.给服务加入动态令牌,在网关或者其他流量公共位置建立校验逻辑,这样服务端在愿意付出集群中分发令牌信息等代价的前提下,可以做到防止重放攻击,但是依然
不能解决传输过程中被嗅探而泄露信息的问题;
5.启用https可以防御链路上的恶意嗅探,也可以在通信层面解决了重放攻击的问题。但是依然有因客户端被攻破产生伪造根证书的风险、因服务端被攻破产生的证书
泄露而被中间人冒认的风险、因CRL更新不及时或者OCSP Soft-fail 产生吊销证书而被冒用的风险,以及因TLS的版本过低或密码套件选用不当产生加密强度不足
的风险;
6.为了抵御上述风险,保密强度还需要进一步加强,譬如银行会使用独立于客户端的存储证书的物理设备(俗称的U盾)来避免证书被客户端的恶意程序窃取伪造;大型
网站涉及账号、金钱等操作时,会使用双重验证开辟一条独立于网络的信息通道来显著提高冒认的难度;甚至一些关键企业或机构会专门建设遍布全国各地的与公网物理
隔离的专用内部网络来保障通信安全。
5.4.2 客户端加密
为了保证信息不被黑客窃取而对客户端加密没有什么意义,对于绝大多数的信息系统来说,启用https可以说是唯一的实际可行的方案。但是,为了保证密码不在服务端被
滥用,在客户端就开始加密还是很有意义的。大网站被脱裤也是很常见的,密码被明文写入数据库、日志屡见不鲜。
为什么在客户端加密对防御泄密没有意义?原因是网络通信并非由发送方和接收方点对点进行的,在客户端无法决定用户发送的信息能不能达到服务端,后者经由什么路线
到达服务端,在传输链路必定是不安全的前提下,无论客户端采取什么措施,都会沦为"马其若防线"。
通常建议是每次从服务端请求动态盐值,在客户端加盐传输的做法通常得不偿失,因为客户端无论是否加盐,都不可能替代https。真正防御性的密码加密存储确实应该在
服务端进行,但这是为了降低服务端被攻破而批量泄露的风险,而不是为了增加传输过程的安全。
5.4.3 密码存储和验证
1.密码创建过程
1)用户在客户端注册,输入明文密码:123456
password = 123456
2)客户端对用户密码进行简单的哈希摘要运算,可选的算法有 MD2/4/5、SHA1/256/512、BCrypt、PBKDF1/2,等等。
client_hash = md5(password)
3)为了防御彩虹表攻击,应加盐处理,客户端加盐只取固定的字符串即可,如果实在不行,也可采用伪动态盐值。
client_hash = md5(md5(password) + salt)
"伪动态"是指服务端不需要额外的通信就可以得到的信息,譬如由日期或用户名等自然变化的内容加上固定字符串组成的信息。
4)假设攻击者截获了客户端发出的信息,得到了摘要结果和采用的盐值,那攻击者就可以枚举遍历所有8位字符以内的弱密码,然后对每个密码进行加盐计算,就可以得到
一个针对固定盐值的对照彩虹表。为了应对这种破解,并不提倡在盐值上做动态化,更理想的方式是引入慢哈希函数来解决。
慢哈希函数是指 执行时间可以调节的哈希函数,通常是以控制调用次数来实现的。BCrypt 算法就是一种典型的慢哈希函数,它做哈希计算时接受盐值Salt和执行
成本Cost两个参数。如果我们将BCrypt的执行时间控制在0.1s完成一次哈希计算的话,按照1s生成10个哈希值的速度,算完所有的10位大小写字母和数字的弱密码大概
需要P(62,10)/(3600*24*365)/0.1=1237204169 年时间。
client_hash = BCrypt(mdt(password) + salt)
5)下一步将哈希值传输到服务端,在服务端只需防御被拖库后针对固定盐值的批量彩虹表攻击。具体做法是为每一个密码(指客户端传来的哈希值)产生一个随机的盐值。
建议采用"密码学安全伪随机数生成器"来生成一个长度与哈希值长度相等的随机字符串。
6)将动态盐值混入客户端传来的哈希值再一次做哈希,产生最终的密文,并和上一步随机生成的盐值一起写入同一条数据库记录中。由于慢哈希算法占用大量处理器资源,
并不推荐在服务端采用。
2.验证过程
1)客户端:用户在登录页面中输入明文密码,123456,经过与注册相同的加密过程,向服务端传送加密的结果;
2)服务端:接收到客户端传来的哈希值,从数据库中取出用户对应的密文和盐值,采用相同的哈希算法,对客户端传来的哈希值、服务端存储的盐值计算摘要结果。
3)比较上一步的结果和数据库存储的哈希值是否相同,如果相同说明密码正确,反之错误。
5.5 传输
5.5.1 摘要、加密与签名
摘要也称为数字摘要(Digital Digest)或数字指纹(Digital Fingerprint)。
理想的哈希算法都具备2个特性:
1.易变性
是指算法的输入端发生了任何一点细微的变动,都会引发雪崩效应,使得输出端的结果产生了极大的变化。这个特性通常被用来做实验,以保证信息未被篡改。
2.不可逆性
摘要的运算过程是单向的,不可从摘要的结果中逆向还原出输入值来。世间的信息有无穷多种,而摘要的结果无论其位数是32,128,512位,甚至更多位,都是一个
有限的数字,因此输入数据与输出的摘要结果必然不是一一对应的关系。经常会听到"破解"的新闻,这里并不是解密的意思,而是指找到了该算法的高效碰撞方法,能够在
合理的时间内生成一个摘要结果为指定内容的输入比特流,但这并不代表这个碰撞产生的比特流就会是原来的输入源。
由这2个特性可以看出来,摘要的意义是在 源信息不泄露的前提下辩其真伪。易变性保证了可以从公开的特征上甄别出信息是否来自于源信息,不可逆性保证了不会从公开的特征
暴露出源信息。这与今天做身份鉴别的指纹、面容和虹膜的生物特征是具有高度可比性的。在一些场合,摘要也会被借用来加密和签名,但在严格意义上来说,摘要和这两者有本质的
区别。
加密与摘要的本质区别在于加密是可逆的,逆过程就是解密。在经典密码学时代,加密的安全性主要依靠机密性来保证,即依靠保护加密算法或算法的执行参数不被泄露来保障
信息的安全。而现代密码学不依靠机密性,加解密算法是完全公开的,它的安全是建立在特定问题的计算复杂度之上的,具体是指算法根据输入端计算输出结果耗费的算力资源很小,
但根据输出端的结果反过来推算原本的输入时耗费的算力就及其庞大。
根据加解密是否采用相同的密钥,可将现代密码学算法分为对称加密和非对称加密。根据2个密钥加解密方式的不同,使得算法可以提供两种不同的功能:
1.公钥加密,私钥解密
这种就是加密,用于向私钥所有者发送信息,这个信息可能被他人篡改,但是无法被他人得知。如果甲想给乙发一个安全保密的数据,那么甲乙应该各有一个私钥,
甲先用乙的公钥加密这段数据,再用自己的私钥加密这段加密后的数据,发送给乙,这样就确保了内容既不会被读取,也不能被篡改。
2.私钥加密,公钥解密
这种就是签名。用于让所有公钥所有者验证私钥所有者的身份,并且防止私钥所有者发布的内容被篡改。但是它不能用于保证内容不被他人获得。
这两种用途在理论上肯定是成立的,但现实中却一般不成立.单靠非对称加密算法,既做不了加密也做不了签名.因为不论是加密还是解密,非对称加密算法的计算复杂度都相当高,
其性能比对称加密要差上好几个数量级.加解密性能不仅影响速度,还导致现行的非对称加密算法都没有支持分组加密模式.这句话的含义是:由于明文长度与密钥长度在安全性上具有
相关性,通俗的说,多长的密钥决定了它能加密多长的明文,如果明文太短就需要进行填充,太长就需要进行分组.因非对称加密本身的效率所限,难以支持分组,所以主流的非对称加密
算法都只能加密不超过密钥长度的数据,这也决定了非对称加密不能直接用于大量数据的加密.
在加密方面,现在一般会结合对称与非对称加密的优点,以混合加密来保护信道安全,具体做法是用非对称加密安全的传递少量数据给通信的另一方,再以这些数据为密钥,采用对称
加密来安全高效的大量加密传输数据,这种由多种加密算法组合的应用形式称为"密码学套件"。非对称加密在这个场景中发挥的作用称为"密钥协商"。
在签名方面,现在一般会结合摘要与非对称加密的优点,以对摘要结果做加密的形式来保证签名的适用性。由于对任何长度的输入源做摘要之后都能得到固定长度的结果,所以
只要对摘要结果进行签名,即相当于对整个输入源进行了背书,保证一旦内容遭到篡改,摘要结果就发生了变化,签名马上失效了。
5.5.2 数字证书
现实中我们如何达成信任:
1.基于共同私密信息的信任
2.基于权威公证人的信任
在网络世界中,主要是第二种,即公开密钥基础设施(Public Key Infrastructure,PKI),又称为 公钥密钥基础架构、公钥基础建设、公钥基础设施、公开密钥基础建设
与公钥基础架构,是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创建、管理、分配、使用、存储以及撤销数字证书。
在密码学中,公钥密钥基础建设凭借着数字证书认证中心(CCertificat Authority,CA)将用户的个人身份跟公开密钥链接在一起,且每个证书中心用户的身份必须是唯一
的。链接关系通过注册和发布过程创建,根据担保级别的差异,创建过程可由CA的各种软件或在人为监督下完成的。PKI的确定链接关系的这一角色称为
注册管理中心(Registration Authority,RA)。RA确保公开密钥和个人身份链接,可以防抵赖。
CA 是负责发放和管理数字证书的权威机构.任何人都可以发放证书,只是不够权威罢了。CA作为第三方受信任机构,承担公钥体系中公钥的合法性验证的责任。权威的CA中心是
可数的,"可数"意味着可以不通过网络,而是在浏览器与操作系统出厂时就预置好的,或者提前安装好。
证书,是权威CA中心对特定公钥信息的一种公证载体,也可以理解为权威CA对特定公钥未被篡改的签名背书。由于我们的客户端已经预置好了CA的证书,所以我们可以在不依靠
网络的前提下,使用根证书里面的公钥信息对其所签发的证书中的签名进行确认。
PKI 中采用的证书格式是 X.509 标准格式,它定义了证书中应该包含哪些信息,并描述了这些信息是如何编码的,其中最关键的就是认证机构的数字签名和公钥信息两项
内容。一个数字证书具体包含如下内容:
1.版本号(Version)
指出该证书使用了哪个版本的X.509标准(版本1,版本2,版本3),版本号会影响证书中的一些特定信息,目前版本为3.
2.序列号(Serial Number)
由证书颁发者分配的证书的唯一标识符。
3.签名算法标识符(Signature Algorithm ID)
用于签发证书的算法标识,由对象标识符加上相关的参数组成,用于说明本证书所用的数字签名算法。
4.认证机构的数字签名(Certificate Signature)
这是使用证书发布者私钥生成的签名,以确保这个证书在发布之后没有被篡改过。
5.认证机构(Issuer Name)
证书颁发者的可是别名。
6.有效期限(Validity Period)
证书起始日期和时间以及终止日期和时间;指明证书在这2个时间内有效。
7.主题信息(Subject)
证书持有人唯一的标识符,这个名字在整个互联网上应该是唯一的,通常使用的是网站的域名。
8.公钥信息(Public Key)
包括证书持有人的公钥、算法的标识符和其他相关的密钥参数。
5.5.3 传输安全层
下面以 TLS 1.2 为例,介绍传输安全层是如何保障所有信息都是第三方无法窃听(加密传输)、无法篡改、无法冒充(证书验证身份)的。tls 1.2 在传输之前的握手过程
一共需要进行上下两轮、共计4次通信。
1.客户端请求:Client Hello
客户端向服务器请求进行加密通信,在这个请求里面,它会以明文的形式,向服务端提供以下信息:
1.支持的协议版本
2.一个客户端生成的32字节的随机数,这个随机数将稍后用于产生加密的密钥
3.一个可选的SessionID。这个SessionID是指传输安全层的SessionID,是为了tls的连接复用设计的。
4.一些列支持的密码学算法套件。
5.一系列支持的数据压缩算法
6.其他可扩展的信息,为了保证协议的隐私性,后续对协议的功能扩展大多都添加到这个变长的结构中。
2.服务器回应:Server Hello
服务端收到客户端的通信请求后,如果客户端声明支持的协议版本和加密算法组合与服务端匹配的话,就向客户端发出回应。如果不匹配,将会返回一个握手失败的警告
提示。这次回应同样以明文发送。包括如下信息:
1.服务端确认使用的tls协议版本
2.第二个32字节的随机数,稍后用于产生加密的密钥
3.一个SessionID,以后可通过连接复用减少一轮握手
4.服务端在列表中选定的密码学算法套件
5.服务端在列表中选定的数据压缩算法
6.其他可扩展信息
7.如果协商出的加密算法组合是依赖证书认证的,服务端还要发送自己的X.509证书,而证书中的公钥是什么,也必须根据协商的加密算法组合来决定
8.密钥协商信息,这部分内容对于不同密码学套件有着不同的价值。
3.客户端确认:Client Handshake Finished
下面以RSA算法为例。
客户端收到服务端的应答后,先要验证服务器的证书合法性。如果证书是不可信的机构颁发的,或者证书中信息存在问题,譬如域名与实际域名不一致、证书已过期、通过
在线证书状态协议得知证书已被吊销,等等,都会向访问者显示一个"证书不可信任"的警告,由用户自行选择是否还要继续通信。如果证书没有问题,客户端就会从证书中取出
服务器的公钥,并向服务器发送以下信息:
1.客户端证书(可选)。部分服务端并不是面向全公众的,而只是对特定的客户端提供服务,此时客户端需要发送它自身的证书来证明身份。如果不发送,或者验证不通过,
服务端可自行决定是否要继续握手,或者返回一个握手失败的信息。客户端需要证书的tls通信也称为"双向tls"(Matual TLS,简写为mTLS),这是云原生基础设施的主要
认证方法,也是基于信道认证的最主流形式。
2.第三个32字节的随机数,这个随机数不再是明文发送,而是以服务端传过来的公钥加密,被称为PreMasterSecret,它将与前两次发送的随机数一起,根据特定的算法
计算出48字节的MasterSecret,这个MasterSecret即后续内容传输时的对称加密算法的私钥。
3.编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
4.客户端握手结束通知,表示客户端握手阶段已经结束。这一项同时也是前面发送的所有内容的哈希值,以供服务端校验。
4.服务端确认:Server Handshake Finished
服务端向客户端回应最后的确认通知,包括以下信息:
1.编码改变通知,表示随后的信息都将用双方的加密方法和密钥发送
2.服务器握手结束通知,表示服务端的握手阶段依据结束。这一项同时也是前面发送的所有内容的哈希值,以供客户端校验。
从上面可知,https 并非只有"启用了https"和"未启用https"的差别,采用不同的协议版本、不同的密码学套件,证书是否有效,服务端/客户端面对无效证书时的处理策略
等都导致了不同https站点的安全强度的不同,因此并不能说只要启用了https就必能安枕无忧。
5.6 验证
关注"你是谁(认证)","你能做什么(授权)","你做的对不对(验证)"。分格式验证,业务校验。
对于那些不带业务含义的注解,运行是不需要其他外部资源参与的,即不会调用远程服务、访问数据库,这种重复执行也不会产生什么成本。
但带业务逻辑的校验,通常就需要外部资源参与,这不仅仅是多消耗一点时间和运算资源的问题,由于很难保证依赖的服务都是幂等的,重复执行校验很可能会带来额外的副作用。