认证:系统如何正确分辨出操作用户的真实身份?
信息系统为用户提供服务之前,总是希望先弄清楚“你是谁?”(认证)、“你能干什么?”(授权)以及“你如何证明?”(凭证)这三个基本问题。然而,这三个基本问题又并不像部分开发者认为的那样,只是一个“系统登录”功能,仅仅是校验一下用户名、密码是否正确这么简单。账户和权限信息作为一种必须最大限度保障安全和隐私,同时又要兼顾各个系统模块甚至系统间共享访问的基础主数据,它的存储、管理与使用都面临一系列复杂的问题。
架构安全性的经验原则:以标准规范为指导、以标准接口去实现。安全涉及的问题很麻烦,但解决方案已相当成熟,对于 99%的系统来说,在安全上不去做轮子,不去想发明创造,严格遵循标准就是最恰当的安全设计。
主流的三种认证方式:
通信信道上的认证:你和我建立通信连接之前,要先证明你是谁。在网络传输(Network)场景中的典型是基于 SSL/TLS 传输安全层的认证。
通信协议上的认证:你请求获取我的资源之前,要先证明你是谁。在互联网(Internet)场景中的典型是基于 HTTP 协议的认证。
通信内容上的认证:你使用我提供的服务之前,要先证明你是谁。在万维网(World Wide Web)场景中的典型是基于 Web 内容的认证。
HTTP 认证框架提出认证方案是希望能把认证“要产生身份凭证”的目的与“具体如何产生凭证”的实现分离开来,无论客户端通过生物信息(指纹、人脸)、用户密码、数字证书抑或其他方式来生成凭证,都属于是如何生成凭证的具体实现,都可以包容在 HTTP 协议预设的框架之内。
HTTP 认证框架的工作流程:
以HTTP Basic 认证为例来介绍认证是如何工作的。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 还定义了很多种可用于实际生产环境的认证方案,列举如下。
Digest:RFC 7616
,HOBA(HTTP Origin-Bound Authentication)是一种基于自签名证书的认证方案。基于数字证书的信任关系主要有两类模型:一类是采用 CA(Certification Authority)层次结构的模型,由 CA 中心签发证书;另一种是以 IETF 的 Token Binding 协议为基础的 OBC(Origin Bound Certificate)自签名证书模型。在“传输”小节将详细介绍数字证书。
HTTP 认证框架中的认证方案是允许自行扩展的,并不要求一定由 RFC 规范来定义,只要用户代理(User Agent,通常是浏览器,泛指任何使用 HTTP 协议的程序)能够识别这种私有的认证方案即可。因此,很多厂商也扩展了自己的认证方案。
AWS4-HMAC-SHA256:亚马逊 AWS 基于 HMAC-SHA256 哈希算法的认证。
NTLM / Negotiate:这是微软公司 NT LAN Manager(NTLM)用到的两种认证方式。
Windows Live ID:微软开发并提供的“统一登入”认证。
Twitter Basic:一个不存在的网站所改良的 HTTP 基础认证。
……
WebAuthn 规范涵盖了“注册”与“认证”两大流程,先来介绍注册流程,它大致可以分为以下步骤:
用户进入系统的注册页面,这个页面的格式、内容和用户注册时需要填写的信息均不包含在 WebAuthn 标准的定义范围内。
当用户填写完信息,点击“提交注册信息”的按钮后,服务端先暂存用户提交的数据,生成一个随机字符串(规范中称为 Challenge)和用户的 UserID(在规范中称作凭证 ID),返回给客户端。
客户端的 WebAuthn API 接收到 Challenge 和 UserID,把这些信息发送给验证器(Authenticator),验证器可理解为用户设备上 TouchID、FaceID、实体密钥等认证设备的统一接口。
验证器提示用户进行验证,如果支持多种认证设备,还会提示用户选择一个想要使用的设备。验证的结果是生成一个密钥对(公钥和私钥),由验证器存储私钥、用户信息以及当前的域名。然后使用私钥对 Challenge 进行签名,并将签名结果、UserID 和公钥一起返回客户端。
浏览器将验证器返回的结果转发给服务器。
服务器核验信息,检查 UserID 与之前发送的是否一致,并用公钥解密后得到的结果与之前发送的 Challenge 相比较,一致即表明注册通过,由服务端存储该 UserID 对应的公钥。
以上步骤的时序如图所示:
登录流程与注册流程类似,如果你理解了注册流程,就很容易理解登录流程了。登录流程大致可以分为以下步骤:
用户访问登录页面,填入用户名后即可点击登录按钮。
服务器返回随机字符串 Challenge、用户 UserID。
浏览器将 Challenge 和 UserID 转发给验证器。
验证器提示用户进行认证操作。由于在注册阶段验证器已经存储了该域名的私钥和用户信息,所以如果域名和用户都相同的话,就不需要生成密钥对了,直接以存储的私钥加密 Challenge,然后返回给浏览器。
服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。
WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能;公钥是公开的,可以被任何人看到或存储。公钥可用于验证私钥生成的签名,但不能用来签名,除了得知私钥外,没有其他途径能够生成可被公钥验证为有效的签名,这样服务器就可以通过公钥是否能够解密来判断最终用户的身份是否合法。
WebAuthn 还一揽子地解决了传统密码在网络传输上的风险,无论密码是否在客户端进行加密、如何加密,对防御中间人攻击来说都是没有意义的。更值得夸赞的是 WebAuthn 为登录过程带来极大的便捷性,不仅注册和验证的用户体验十分优秀,而且彻底避免了用户在一个网站上泄漏密码,所有使用相同密码的网站都受到攻击的问题,这个优点使得用户无须再为每个网站想不同的密码。
当前的 WebAuthn 还很年轻,普及率暂时还很有限,但相信几年之内它必定会发展成 Web 认证的主流方式,被大多数网站和系统所支持。
在今时今日,实际活跃于 Java 安全领域的是两个私有的(私有的意思是不由 JSR 所规范的,即没有 java/javax.*作为包名的)的安全框架:Apache Shiro和Spring Security。
相较而言,Shiro 更便捷易用,而 Spring Security 的功能则要复杂强大一些。无论是单体架构还是微服务架构的 Fenix's Bookstore,笔者都选择了 Spring Security 作为安全框架,这个选择与功能、性能之类的考量没什么关系,就只是因为 Spring Boot、Spring Cloud 全家桶的缘故。这里不打算罗列代码来介绍 Shiro 与 Spring Security 的具体使用,如感兴趣可以参考 Fenix's Bookstore 的源码仓库。只从目标上看,两个安全框架提供的功能都很类似,大致包括以下四类:
认证功能:以 HTTP 协议中定义的各种认证、表单等认证方式确认用户身份。
安全上下文:用户获得认证之后,要开放一些接口,让应用可以得知该用户的基本资料、用户拥有的权限、角色,等等。
授权功能:判断并控制认证后的用户对什么资源拥有哪些操作许可。
密码的存储与验证:密码是烫手的山芋,存储、传输还是验证都应谨慎处理。