learn-tech/专栏/周志明的架构课/23_认证:系统如何正确分辨操作用户的真实身份?.md
2024-10-16 06:37:41 +08:00

274 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
23 _ 认证:系统如何正确分辨操作用户的真实身份?
你好,我是周志明。
我们应该都很清楚,对于软件研发来说,即使只限定在“软件架构设计”这个语境下,系统安全仍然是一个很大的话题。它不仅包括“防御系统被黑客攻击”这样狭隘的安全,还包括一些与管理、运维、审计等领域主导的相关安全性问题,比如说安全备份与恢复、安全审计、防治病毒,等等。
不过在这门课程里,我们的关注重点并不会放在以上这些内容上,我们所谈论的软件架构安全,主要包括(但不限于)以下这些问题的具体解决方案:
认证Authentication系统如何正确分辨出操作用户的真实身份
授权( Authorization系统如何控制一个用户该看到哪些数据、能操作哪些功能
凭证Credentials系统如何保证它与用户之间的承诺是双方当时真实意图的体现是准确、完整且不可抵赖的
保密Confidentiality系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用
传输Transport Security系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充
验证Verification系统如何确保提交到每项服务中的数据是合乎规则的不会对系统稳定性、数据一致性、正确性产生风险
由于跟安全相关的问题,一般都不会给架构设计直接创造价值,而且解决起来又很繁琐复杂、费时费力,所以可能会经常性地被一部分开发人员给有意无意地忽略掉。
不过庆幸的是,这些问题基本上也都是与具体系统、具体业务无关的通用性问题,这就意味着它们往往会存在一些业界通行的、已经被验证过是行之有效的解决方案,乃至已经形成了行业标准,不需要我们再从头去构思如何解决。
所以在“安全架构”这个小章节里我会花六讲的时间围绕系统安全的标准方案带你逐一探讨以上这些问题的处理办法并会以Fenixs Bookstore作为案例实践。而出于方便你进行动手实操的目的我不会在课程中直接贴出大段的项目代码当然必要的代码示例还是会有的所以我建议你要结合着从Fenixs Bookstore的GitHub仓库中获取的示例代码来进行学习。
好,那么今天这节课,我们就从“认证”这个话题开始,一起来解决“系统如何正确分辨操作用户的真实身份”这个问题。
什么是认证?
认证Authentication、授权Authorization和凭证Credentials这三项可以说是一个系统中最基础的安全设计了哪怕是再简陋的信息系统大概也不可能忽略掉“用户登录”这个功能。
信息系统在为用户提供服务之前,总是希望先弄清楚“你是谁?”(认证)、“你能干什么?”(授权)以及“你如何证明?”(凭证)这三个基本问题的答案。然而,认证、授权与凭证这三个基本问题,又并不像部分开发者认为的那样,只是一个“系统登录”功能而已,仅仅是校验一下用户名、密码是否正确这么简单。
账户和权限信息作为一种必须最大限度保障安全和隐私,同时又要兼顾各个系统模块、甚至是系统间共享访问的基础主数据,它的存储、管理与使用都面临一系列复杂的问题。
因此对于某些大规模的信息系统账户和权限的管理往往要由专门的基础设施来负责比如微软的活动目录Active DirectoryAD或者轻量目录访问协议Lightweight Directory Access ProtocolLDAP跨系统的共享使用问题甚至还会用到区块链技术来解决。
另外,还有一个不少人会先入为主的认知偏差:尽管“认证”是解决“你是谁?”的问题,但这里的“你”并不一定是个人(真不是在骂你),也很有可能是指外部的代码,即第三方的类库或者服务。
因为最初在计算机软件当中对代码认证的重要程度甚至要高于对最终用户的认证比如早期的Java系统里安全中的认证默认是特指“代码级安全”即你是否信任要在你的电脑中运行的代码。
这是由Java当时的主要应用形式Java Applets所决定的类加载器从远端下载一段字节码以Applets的形式在用户的浏览器中运行由于Java的语言操控计算机资源的能力要远远强于JavaScript所以系统必须要先确保这些代码不会损害用户的计算机否则就谁都不敢去用。
这一阶段的安全观念就催生了现在仍然存在于Java技术体系中的“安全管理器”java.lang.SecurityManager、“代码权限许可”java.lang.RuntimePermission等概念。到了现在系统对外部类库和服务的认证需求依然很普遍但相比起五花八门的最终用户认证来说代码认证的研究发展方向已经很固定了基本上都是统一到证书签名上。
不过在咱们这节课里对认证的探究范围只限于对最终用户的认证。关于对代码的认证我会安排在“分布式的基石”模块中的第40讲“服务安全”来讲解。
好,那么在理解了什么是认证、界定了认证的范围之后,我们接下来看一下软件工业界是如何进行认证的。
认证的标准
在世纪之交Java迎来了Web时代的辉煌互联网的迅速兴起促使Java进入了快速发展时期。这时候基于HTML和JavaScript的超文本Web应用就迅速超过了“Java 2时代”之前的Java Applets应用B/S系统对最终用户认证的需求使得“安全认证”的重点逐渐从“代码级安全”转为了“用户级安全”即你是否信任正在操作的用户。
在1999年随J2EE 1.2它是J2EE的首个版本为了与J2SE同步初始版本号直接就是1.2一起发布的Servlet 2.2中添加了一系列用于认证的API主要包括了两部分内容
标准方面添加了四种内置的、不可扩展的认证方案即Client-Cert、Basic、Digest和Form。
实现方面添加了与认证和授权相关的一套程序接口比如HttpServletRequest::isUserInRole()、HttpServletRequest::getUserPrincipal()等方法。
到这儿你可能会觉得这都是一项发布超过20年的老旧技术了为啥还要专门提一嘴呢这是因为我希望从它包含的两部分内容中引出一个架构安全性的经验原则以标准规范为指导、以标准接口去实现。
因为安全涉及的问题很麻烦但它的解决方案也相当的成熟。对于99%的系统来说,在安全上不去做轮子,不去想发明创造,严格遵循标准就是最恰当的安全设计。
然后我之所以引用J2EE 1.2对安全的改进还有一个原因就是它内置支持的Basic、Digest、Form和Client-Cert四种认证方案都很有代表性刚好分别覆盖了通讯信道、协议和内容层面的认证这三种层面的认证又涵盖了主流的三种认证方式下面我们分别来看看它们各自的含义和应用场景
通讯信道上的认证你和我建立通讯连接之前要先证明你是谁。在网络传输Network场景中的典型是基于SSL/TLS传输安全层的认证。
通讯协议上的认证你请求获取我的资源之前要先证明你是谁。在互联网Internet场景中的典型是基于HTTP协议的认证。
通讯内容上的认证你使用我提供的服务之前要先证明你是谁。在万维网World Wide Web场景中的典型是基于Web内容的认证。
关于第一点“信道上的认证”由于它涉及的内容较多又与后面第28、29讲要介绍的微服务安全方面的话题关系密切所以这节课我就不展开讲了而且J2EE中的Client-Cert其实并不是用于TLS的以它引出TLS并不合适
那么接下来,我们就针对后两种认证方式,来看看它们各自都有什么样的实现特点和工作流程。
基于通讯协议HTTP认证
前面我在介绍J2EE 1.2这项老技术的时候已经提前用到了一个技术名词“认证方案”Authentication Schemes。它是指生成用户身份凭证的某种方法这个概念最初是来源于HTTP协议的认证框架Authentication Framework
IETF在RFC 7235中定义了HTTP协议的通用认证框架要求所有支持HTTP协议的服务器当未授权的用户意图访问服务端保护区域的资源时应返回401 Unauthorized的状态码同时要在响应报文头里附带以下两个分别代表网页认证和代理认证的Header之一告知客户端应该采取哪种方式产生能代表访问者身份的凭证信息
WWW-Authenticate: <认证方案> realm=<保护区域的描述信息>
Proxy-Authenticate: <认证方案> realm=<保护区域的描述信息>
而在接收到该响应后客户端必须遵循服务端指定的认证方案在请求资源的报文头中加入身份凭证信息服务端核实通过后才会允许该请求正常返回否则将返回403 Forbidden。其中请求报文头要包含以下Header项之一
Authorization: <认证方案> <凭证内容>
Proxy-Authorization: <认证方案> <凭证内容>
由此我们其实可以发现HTTP认证框架提出的认证方案是希望能把认证“要产生身份凭证”的目的与“具体如何产生凭证”的实现给分开来。无论客户端是通过生物信息指纹、人脸、用户密码、数字证书还是其他方式来生成凭证都是属于如何生成凭证的具体实现都可以包容在HTTP协议预设的框架之内。
HTTP认证框架的工作流程如下面的时序图所示
不过只有这种概念性的介绍你可能还是会觉得有点儿枯燥和抽象接下来我就以最基础的认证方案HTTP Basic Authentication为例来给你解释下认证具体是如何工作的。
HTTP Basic认证是一种以演示为目的的认证方案在一些不要求安全性的场合也有实际应用比如你家里的路由器登录有可能就是这种认证方式。
Basic认证产生用户身份凭证的方法是让用户输入用户名和密码经过Base64编码“加密”后作为身份凭证。比如请求资源“GET/admin”后浏览器会收到服务端如下响应
HTTP/1.1 401 Unauthorized
Date: Mon, 24 Feb 2020 16:50:53 GMT
WWW-Authenticate: Basic realm="example from icyfenix.cn"
此时浏览器必须询问最终用户要求提供用户名和密码并会弹出类似下图所示的HTTP Basic认证窗口
然后用户在对话框中输入密码信息比如输入用户名“icyfenix”密码123456浏览器会将字符串“icyfenix:123456”编码为“aWN5ZmVuaXg6MTIzNDU2”然后发送给服务端HTTP请求如下所示
GET /admin HTTP/1.1
Authorization: Basic aWN5ZmVuaXg6MTIzNDU2
服务端接收到请求,解码后检查用户名和密码是否合法,如果合法就允许返回/admin的资源否则就返回403 Forbidden禁止下一步操作。
这里要注意一点Base64只是一种编码方式而并不是任何形式的加密所以Basic认证的风险是显而易见的它只能是一种以演示为主要目的的认证方案。
那么除Basic认证外IETF还定义了很多种可用于实际生产环境的认证方案比如
DigestRFC 7616HTTP摘要认证你可以把它看作是Basic认证的改良版本针对Base64明文发送的风险Digest认证把用户名和密码加盐一个被称为Nonce的变化值作为盐值再通过MD5/SHA等哈希算法取摘要发送出去。这种认证方式依然是不安全的无论客户端使用何种加密算法加密无论是否采用了Nonce这样的动态盐值去抵御重放和冒认当遇到中间人攻击时依然存在显著的安全风险。在第27“保密”一讲中我还会跟你具体讨论加解密方面的问题。
BearerRFC 6750基于OAuth 2.0规范来完成认证OAuth 2.0是一个同时涉及到认证与授权的协议。在下节课讲解“授权”的时候我会详细介绍OAuth 2.0。
HOBARFC 7486 HOBA是HTTP Origin-Bound Authentication的缩写这是一种基于自签名证书的认证方案。基于数字证书的信任关系主要有两类模型一类是采用CACertification Authority层次结构的模型由CA中心签发证书另一种是以IETF的Token Binding协议为基础的OBCOrigin Bound Certificates自签名证书模型。同样在后面讲“传输”的时候我会给你详细介绍数字证书。
还有在HTTP认证框架中认证方案是允许自行扩展的也并不要求一定要由RFC规范来定义只要用户代理User Agent通常是浏览器泛指任何使用HTTP协议的程序能够识别这种私有的认证方案即可。
因此,很多厂商也扩展了自己的认证方案,比如:
AWS4-HMAC-SHA256相当简单粗暴的名字就是亚马逊AWS基于HMAC-SHA256哈希算法的认证。
NTLM / Negotiate这是微软公司NT LAN ManagerNTLM用到的两种认证方式。
Windows Live ID这个顾名思义即可。
Twitter Basic一个不存在的网站所改良的HTTP基础认证。
……
说完了基于通讯协议的认证方案我们再来看看基于通讯内容的Web认证是如何实现的。
基于通讯内容Web认证
IETF为HTTP认证框架设计了可插拔Pluggable的认证方案原本是希望能涌现出各式各样的认证方案去支持不同的应用场景。尽管前面我也列举了一些还算常用的认证方案但目前的信息系统尤其是在系统对终端用户的认证场景中直接采用HTTP认证框架的比例其实是非常低的。
这也不难理解HTTP是“超文本传输协议”传输协议的根本职责是把资源从服务端传输到客户端至于资源具体是什么内容只能由客户端自行解析驱动。所以说以HTTP协议为基础的认证框架也只能面向传输协议而不是具体传输内容来设计。
如果用户想要从服务器中下载文件弹出一个HTTP服务器的对话框让用户登录是可以接受的但如果用户访问信息系统中的具体服务身份认证肯定希望是由系统本身的功能去完成的而不是由HTTP服务器来负责认证。
那么这种依靠内容而不是传输协议来实现的认证方式在万维网里就被称为“Web认证”由于在实现形式上登录表单占了绝对的主流因此它通常也被称为“表单认证”Form Authentication
实际上直到2019年之前表单认证都没有什么行业标准可循表单长什么样子、其中的用户字段、密码字段、验证码字段、是否要在客户端加密、采用何种方式加密、接受表单的服务地址是什么等等都完全由服务端与客户端的开发者自行协商决定。
可“没有标准的约束”,反倒成了表单认证的一大优点,表单认证允许我们做出五花八门的页面,各种程序语言、框架或开发者本身,都可以自行决定认证的全套交互细节。
到这里你可能要说了,在前面讲认证标准的时候,我说“遵循规范、别造轮子就是最恰当的安全”,这里我又把表单认证的高自由度说成是一大优点,好话都让我给说全了。
其实啊,我提倡用标准规范去解决安全领域的共性问题,这条原则完全没有必要与界面是否美观合理、操作流程是否灵活便捷这些应用需求对立起来。
比如,想要支持密码或扫码等多种登录方式、想要支持图形验证码来驱逐爬虫与机器人、想要支持在登录表单提交之前进行必要的表单校验,等等,这些需求都很具体,不具备写入标准规范的通用性,但它们都具备足够的合理性,应当在实现层面去满足。
同时,如何控制权限保证不产生越权操作、如何传输信息保证内容不被窃听篡改、如何加密敏感内容保证即使泄漏也不被逆推出明文,等等,这些问题也已经有了通行的解决方案,明确定义在规范之中,因此也应当在架构层面去遵循。
所以说表单认证与HTTP认证不见得是完全对立的它们分别有不同的关注点可以结合使用。就以Fenixs Bootstore的登录功能为例这个项目的页面表单是一个自行设计的Vue.js页面但认证的整个交互过程就遵循了OAuth 2.0规范的密码模式来完成。
2019年3月万维网联盟批准了由FIDOFast IDentity Online一个安全、开放、防钓鱼、无密码认证标准的联盟领导起草的世界首份Web内容认证的标准“WebAuthn”在这节课里我们只讨论WebAuthn不会涉及CTAP、U2F和UAF。如果你的思维很严谨的话可能又会觉得奇怪和矛盾了不是才说了Web表单长什么样、要不要验证码、登录表单是否在客户端校验等等是十分具体的需求不太可能定义在规范上的吗
确实如此所以WebAuthn彻底抛弃了传统的密码登录方式改为直接采用生物识别指纹、人脸、虹膜、声纹或者实体密钥以USB、蓝牙、NFC连接的物理密钥容器来作为身份凭证从根本上消灭了用户输入错误产生的校验需求以及防止机器人模拟产生的验证码需求等问题甚至连表单界面都可能省略掉所以这个规范不关注界面该是什么样子、要不要验证码、是否要前端校验等这些问题。
不过由于WebAuthn相对比较复杂在学习后面的内容之前我建议如果你的设备和环境允许的话可以先在GitHub网站的2FA认证功能中实际体验一下通过WebAuthn完成的两段式登录。
在硬件方面需要你用带有TouchBar的MacBook或者其他支持指纹、FaceID验证的手机均可现在应该在售的移动设备基本都带有生物识别的装置了。在软件方面直至iOS13.6iPhone和iPad都不支持WebAuthn但Android和macOS系统中的Chrome以及Windows的Edge浏览器都已经可以正常使用WebAuthn了。
WebAuthn规范涵盖了“注册”与“认证”两大流程我先来介绍下注册流程的大致步骤
用户进入系统的注册页面这个页面的格式、内容和用户注册时需要填写的信息都不包含在WebAuthn标准的定义范围内。
当用户填写完信息点击“提交注册信息”的按钮后服务端先暂存用户提交的数据生成一个随机字符串规范中称为Challenge和用户的UserID在规范中称作凭证ID返回给客户端。
客户端的WebAuthn API接收到Challenge和UserID把这些信息发送给验证器Authenticator这个验证器你可以理解为用户设备上TouchBar、FaceID、实体密钥等认证设备的统一接口。
验证器提示用户进行验证如果你的机器支持多种认证设备还会提示用户选择一个想要使用的设备。验证的结果是生成一个密钥对公钥和私钥验证器自己存储好私钥、用户信息以及当前的域名。然后使用私钥对Challenge进行签名并将签名结果、UserID和公钥一起返回给客户端。
浏览器将验证器返回的结果转发给服务器。
服务器核验信息检查UserID与之前发送的是否一致并对比用公钥解密后得到的结果与之前发送的Challenge是否一致一致即表明注册通过服务端存储该UserID对应的公钥。
你可以参考一下这个注册步骤的时序图:
登录流程其实跟注册流程差不多,如果你理解了注册流程,登录就比较简单了,大致可以分为这样几个步骤:
用户访问登录页面,填入用户名后即可点击登录按钮。
服务器返回随机字符串Challenge、用户UserID。
浏览器将Challenge和UserID转发给验证器。
验证器提示用户进行认证操作。由于在注册阶段验证器已经存储了该域名的私钥和用户信息所以如果域名和用户都相同的话就不需要生成密钥对了直接以存储的私钥加密Challenge然后返回给浏览器。
服务端接收到浏览器转发来的被私钥加密的Challenge以此前注册时存储的公钥进行解密如果解密成功则宣告登录成功。
WebAuthn采用非对称加密的公钥、私钥替代传统的密码这是非常理想的认证方案。私钥是保密的只有验证器需要知道它连用户本人都不需要知道也就没有人为泄漏的可能公钥是公开的可以被任何人看到或存储。
另外,公钥可用于验证私钥生成的签名,但不能用来签名,除了得知私钥外,没有其他途径能够生成可被公钥验证为有效的签名,这样服务器就可以通过公钥是否能够解密,来判断最终用户的身份是否合法。
而且WebAuthn还一揽子地解决了传统密码在网络传输上的风险在“保密”一节课中我们还会讲到无论密码是否在客户端进行加密、如何加密对防御中间人攻击来说都是没有意义的。
更值得夸赞的是WebAuthn还为登录过程带来了极大的便捷性不仅注册和验证的用户体验十分优秀而且彻底避免了用户在一个网站上泄漏密码所有使用相同密码的网站都受到攻击的问题这个优点可以让用户不需要再为每个网站想不同的密码。
当然现在的WebAuthn还很年轻普及率暂时还很有限但我相信几年之内它必定会发展成Web认证的主流方式被大多数网站和系统所支持。
认证的实现
OK在了解了业界标准的认证规范以后我们再来看看在Java技术体系内通常都是如何实现安全认证的。
Java其实也有自己的认证规范第一个系统性的Java认证规范发布于Java 1.3时代Sun公司提出了同时面向代码级安全和用户级安全的认证授权服务JAASJava Authentication and Authorization Service1.3处于扩展包中1.4纳入标准包。不过尽管JAAS已经开始照顾了最终用户的认证但相对而言该规范中代码级安全仍然占更主要的地位。
可能今天用过、甚至是听过JAAS的Java程序员都已经不多了但是这个规范提出了很多在今天仍然活跃于主流Java安全框架中的概念。比如说一般把用户存放在“Principal”之中、密码存在“Credentials”之中、登录后从安全上下文“Context”中获取状态等常见的安全概念都可以追溯到这一时期所定下的API
LoginModule javax.security.auth.spi.LoginModule
LoginContext javax.security.auth.login.LoginContext
Subject javax.security.auth.Subject
Principal java.security.Principal
Credentialsjavax.security.auth.Destroyable、javax.security.auth.Refreshable
可是虽然JAAS开创了这些沿用至今的安全概念但其规范本身实质上并没有得到广泛的应用。我认为主要有两大原因。
一方面是由于JAAS同时面向代码级和用户级的安全机制使得它过度复杂化难以推广。在这个问题上Java社区一直有做持续的增强和补救比如Java EE 6中的JASPIC、Java EE 8中的EE Security
JSR 115Java Authorization Contract for ContainersJACC
JSR 196Java Authentication Service Provider Interface for ContainersJASPIC
JSR 375 Java EE Security APIEE Security
而另一方面也可能是更重要的一个原因就是在21世纪的第一个十年里以“With EJB”为口号、以WebSphere、Jboss等为代表J2EE容器环境与以“Without EJB”为口号、以Spring、Hibernate等为代表的轻量化开发框架产生了激烈的竞争结果是后者获得了全面胜利。
这个结果就导致了依赖于容器安全的JAAS无法得到大多数人的认可。在今时今日实际活跃于Java安全领域的是两个私有的私有的意思是不由JSR所规范的即没有java/javax.*作为包名的的安全框架Apache Shiro和Spring Security。
那么相较而言Shiro更加便捷易用而Spring Security的功能则要复杂强大一些。因此在后面课程中要介绍的Fenixs Bookstore项目中无论是单体架构、还是微服务架构我都选择了Spring Security作为安全框架这个选择与功能、性能之类的考量没什么关系就只是因为Spring Boot、Spring Cloud全家桶的缘故这里我不打算罗列代码来介绍Shiro与Spring Security的具体使用如果你感兴趣可以参考Fenixs Bookstore的源码仓库
只从目标上来看,两个安全框架提供的功能都很类似,大致包括以下四类:
认证功能以HTTP协议中定义的各种认证、表单等认证方式确认用户身份这也是这节课所探讨的主要话题。
安全上下文:用户获得认证之后,要开放一些接口,让应用可以得知该用户的基本资料、用户拥有的权限、角色,等等。
授权功能:判断并控制认证后的用户对什么资源拥有哪些操作许可,这部分内容我会在下一节课讲“授权”时介绍。
密码的存储与验证:密码是烫手的山芋,不管是存储、传输还是验证,都应该谨慎处理,这部分内容我会放到“保密”一讲去具体讨论。
小结
这节课我们了解了信道、协议和内容这三种主要标准化认证类型的其中两种分别是HTTP认证协议和Web认证内容。现在你应该就很清楚HTTP认证和Web认证的特点了那就是认证的载体不一样决定了认证的形式和功能范围都有不同。
另外我还给你介绍了它们各自的工作流程,其中你要关注的重点是认证框架的整体的运作,不必一下子陷入到具体的认证方案上去。
除此之外我还介绍了认证标准在Java中的落地实现。在Java技术体系中原本也有自己的认证标准与实现那就是依赖于JAAS的面向代码级和用户级的安全机制不过目前应用更广泛的反而是两个私有的安全框架这又是一个官方标准被民间草根框架击败的例子可见软件中设计必须贴近实际用户才能达到实用的效果。
一课一思
我相信你公司的系统一定也会使用用户登录功能,那么它是标准化的认证吗?是如何实现的呢?
欢迎给我留言,分享你的答案。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。