认证(Authentication):系统如何正确分辨出操作用户的真实身份?
授权( Authorization):系统如何控制一个用户该看到哪些数据、能操作哪些功能?
凭证(Credential):系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的?
保密(Confidentiality):系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用?
传输(Transport Security):系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充?
验证(Verification):系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险?
认证
认证的标准
- 通信信道上的认证: 建立通信连接之前,要先证明你是谁。典型的是基于 SSL/TLS 传输安全层的认证。
- 通信协议上的认证: 获取我的资源之前,要先证明你是谁。典型是基于 HTTP 协议的认证。
- 通信内容上的认证: 提供的服务之前,要先证明你是谁。典型是基于 Web 内容的认证。
HTTP 认证
未授权的用户访问服务端保护区域的资源时,应返回 401 Unauthorized 的状态码,同时应在响应报文头里附带以下两个分别代表网页认证和代理认证的 Header 之一,告知客户端应该采取何种方式产生能代表访问者身份的凭证信息:
WWW-Authenticate: <认证方案> realm=<保护区域的描述信息>
Proxy-Authenticate: <认证方案> realm=<保护区域的描述信息>
接收到该响应后,客户端必须遵循服务端指定的认证方案,在请求资源的报文头中加入身份凭证信息,由服务端核实通过后才会允许该请求正常返回,否则将返回 403 Forbidden 错误。请求头报文应包含以下 Header 项之一:
Authorization: <认证方案> <凭证内容>
Proxy-Authorization: <认证方案> <凭证内容>
Basic 认证: 用户名:密码 进行Base64 编码, Authorization: Basic aWN5ZmVuaXg6MTIzNDU2
Digest(HTTP 摘要认证): 将Basic的用户名和密码加盐,通过MD5/SHA等哈希摘要发送(依然不太安全)
Bearer(基于 OAuth 2 规范来完成认证)
HOBA(基于自签名证书的认证方案)
授权
RBAC——访问控制
OAuth2—— 认证授权协议
OAuth2 是面向于解决第三方应用(Third-Party Application)的认证授权协议
OAuth2 给出了多种解决办法,这些办法的共同特征是以令牌(Token)代替用户密码作为授权的凭证。有了令牌之后,哪怕令牌被泄漏,也不会导致密码的泄漏;令牌上可以设定访问资源的范围以及时效性;每个应用都持有独立的令牌,哪个失效都不会波及其他。
- 第三方应用(Third-Party Application):需要得到授权访问我资源的那个应用。
- 授权服务器(Authorization Server):能够根据我的意愿提供授权(授权之前肯定已经进行了必要的认证过程,但它与授权可以没有直接关系)的服务器。
- 资源服务器(Resource Server):能够提供第三方应用所需资源的服务器,它与认证服务可以是相同的服务器,也可以是不同的服务器
- 资源所有者(Resource Owner): 拥有授权权限的人
- 操作代理(User Agent):指用户用来访问服务器的工具,通常是指浏览器、HttpClient、RPCClient等
oauth2有四种具体的授权方式
- 授权码模式(Authorization Code)
- 隐式授权模式(Implicit)
- 密码模式(Resource Owner Password Credentials)
- 客户端模式(Client Credentials)
授权码模式
开始进行授权过程以前,第三方应用先要到授权服务器上进行注册,所谓注册,是指向认证服务器提供一个域名地址,然后从授权服务器中获取 ClientID 和 ClientSecret,以便能够顺利完成如下授权过程:
- 第三方应用将资源所有者(用户)导向授权服务器的授权页面,并向授权服务器提供 ClientID 及用户同意授权后的回调 URI,这是一次客户端页面转向。
- 授权服务器根据 ClientID 确认第三方应用的身份,用户在授权服务器中决定是否同意向该身份的应用进行授权,用户认证的过程未定义在此步骤中,在此之前应该已经完成。
- 如果用户同意授权,授权服务器将转向第三方应用在第 1 步调用中提供的回调 URI,并附带上一个授权码和获取令牌的地址作为参数,这是第二次客户端页面转向。
- 第三方应用通过回调地址收到授权码,然后将授权码与自己的 ClientSecret 一起作为参数,通过服务器向授权服务器提供的获取令牌的服务地址发起请求,换取令牌。该服务器的地址应与注册时提供的域名处于同一个域中。
- 授权服务器核对授权码和 ClientSecret,确认无误后,向第三方应用授予令牌。令牌可以是一个或者两个,其中必定要有的是访问令牌(Access Token),可选的是刷新令牌(Refresh Token)。访问令牌用于到资源服务器获取资源,有效期较短,刷新令牌用于在访问令牌失效后重新获取,有效期较长。
- 资源服务器根据访问令牌所允许的权限,向第三方应用提供资源。
会不会有其他应用冒充第三方应用骗取授权?
- ClientID 代表一个第三方应用的“用户名”,这项信息是可以完全公开的。但 ClientSecret 应当只有应用自己才知道,这个代表了第三方应用的“密码”。在第 5 步发放令牌时,调用者必须能够提供 ClientSecret 才能成功完成。只要第三方应用妥善保管好 ClientSecret,就没有人能够冒充它。
为什么要先发放授权码,再用授权码换令牌?
- 这是因为客户端转向(通常就是一次 HTTP 302 重定向)对于用户是可见的,换而言之,授权码可能会暴露给用户以及用户机器上的其他程序,但由于用户并没有 ClientSecret,光有授权码也是无法换取到令牌的,所以避免了令牌在传输转向过程中被泄漏的风险。
为什么要设计一个时限较长的刷新令牌和时限较短的访问令牌?不能直接把访问令牌的时间调长吗?
- 这是为了缓解 OAuth2 在实际应用中的一个主要缺陷,通常访问令牌一旦发放,除非超过了令牌中的有效期,否则很难(需要付出较大代价)有其他方式让它失效,所以访问令牌的时效性一般设计的比较短,譬如几个小时,如果还需要继续用,那就定期用刷新令牌去更新,授权服务器就可以在更新过程中决定是否还要继续给予授权。
授权码模式第三方应用必须有应用服务器,所以引出了隐式授权。
隐式授权模式
直接返回访问令牌,不能避免令牌暴露给资源所有者
隐式授权中令牌必须是“通过 Fragment 带回”的。
Fragment 就是地址中#号后面的部分 http://bookstore.icyfenix.cn/#/detail/1
- Fragment是不会被发送到服务端的,只能在客户端读取,减少了令牌泄露的可能性。
- 认证服务器到操作代理之间的这一段链路的安全只能通过HTTPS来保证安全。
密码模式
密码模式最好就是自己公司的应用之间相互调用使用。
客户端模式
客户端模式便是一种常用的服务间认证授权的解决方案。
凭证
Cookie-Session
- HTTP是无状态的协议,每次请求是独立的,也带来了请求直接不需要协调的好处
- HTTP可以携带cookie头,让服务端区分来自不同客户端的请求(不应该携带过多内容,容易被篡改,所以一般用
jsessionid
key的value作为传输,信息存储在服务端) - 服务端以这个为key存储在线用户的上下文
- Cookie-Session是有一定安全性的,通过同源策略和HTTPS,保证key不被窃取,就能规避掉上下文信息泄露问题。
- cookie-session还可以随时修改、限制上下文信息
Session-Cookie 在单节点的单体服务环境中是最合适的方案,水平扩展就比较麻烦了
- 牺牲集群的一致性(Consistency),采用Session或ip哈希,但是如果某一节点奔溃,用户信息都失效了
- 牺牲集群的可用性(Availability),让各个节点之间采用复制式的 Session,成本太大
- 牺牲集群的分区容忍性(Partition Tolerance),将信息放到都能访问的数据节点,引发单点故障
JWT
- 最常见的使用方式是附在名为 Authorization 的 Header 发送给服务端,前缀在RFC 6750中被规定为 Bearer。
- JWT 只解决防篡改的问题,并不解决防泄漏的问题。
第一部分是令牌头(Header)
{
"alg": "HS256", // 签名算法,HMAC SHA256
// HMAC 哈希与普通哈希算法的差别是普通的哈希算法通过 Hash 函数结果易变性保证了原有内容未被篡改,HMAC 不仅保证了内容未被篡改过,还保证了该哈希确实是由密钥的持有人所生成的。
"typ": "JWT" //类型
}
第二部分是负载(Payload)
{
"username": "icyfenix",
"authorities": [
"ROLE_USER",
"ROLE_ADMIN"
],
"scope": [
"ALL"
],
"exp": 1584948947,
"jti": "9d77586a-3f4f-4cbb-9924-fe2f77dfa33d",
"client_id": "bookstore_frontend"
}
/*
iss(Issuer):签发人。
exp(Expiration Time):令牌过期时间。
sub(Subject):主题。
aud (Audience):令牌受众。
nbf (Not Before):令牌生效时间。
iat (Issued At):令牌签发时间。
jti (JWT ID):令牌编号。
*/
第三部分是签名(Signature)
- 使用在对象头中公开的特定签名算法,通过特定的密钥(Secret,由服务器进行保密,不能公开)对前面两部分内容进行加密
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)
- 签名确保负载中的信息是可信的、没有被篡改的
- 可以携带少量信息,利于RESTful API 的设计,利于水平扩展
缺点
- 令牌难以主动失效: JWT签发后失效前认证服务器就很难处理,
- 相对更容易遭受重放攻击: 解决重复攻击比较复杂(全局序列号、Nonce字符串、频繁刷新令牌),建议启用HTTPS解决重放攻击
- 只能携带相当有限的数据: Header存储的数据长度有限
- 必须考虑令牌在客户端如何存储: Cookie?localStorage?Indexed DB?它们都有泄漏的可能
- 无状态也不总是好的: 很难实现用户在线统计
保密
分为端的保密和链路的保密
保密的强度
- 以摘要代替明文:不会被逆推出原信息,但是弱密码可能被彩虹表破解
- 先加盐值再做哈希是应对弱密码的常用方法:盐值可以一定上防止彩虹表,但是不能防止窃听,可能冒认
- 将盐值变为动态值能有效防止冒认:每次加密结果不同,比较复杂,无法阻止对其他服务的重放攻击
- 给服务加入动态令牌,在公共位置建立校验逻辑,防止重放攻击,但是传输过程中容易被嗅探
- 启用 HTTPS 可以防御链路上的恶意嗅探,也能在通信层面解决了重放攻击的问题。客户端和服务端证书伪造和泄露的风险
- 银行使用U盾避免证书被客户端窃取和伪造。大型网站设计金融操作开启双重验证开启独立信道(验证码,邮件)。关键企业建设遍布全国的专用网络保障安全
客户端加密
- 为了保证信息不被黑客窃取而做客户端加密没有太多意义,对绝大多数的信息系统来说,启用 HTTPS 可以说是唯一的实际可行的方案。
- 客户端加密对防御泄密没有意义(传输链路必定是不安全的)
传输
摘要、加密与签名
摘要也称之为数字摘要(Digital Digest)或数字指纹(Digital Fingerprint)
- 一是易变性,算法输入端修改会引发雪崩效应。
- 二是单向的,不能从结果还原出输入
加密:加密是可逆的,分为对称加密和非对称加密
- 对称加密:密钥数量与成员数量成平方,面临密钥管理的难题。
- 非对称加密复杂性高,主流的非对称加密算法都只能加密不超过密钥长度的数据,不能直接用于大量数据的加密。
公钥加密,私钥解密:加密,可能被篡改。甲用乙公钥加密,再用自己的私钥加密,防止内容被读取,被篡改
私钥加密,公钥解密:签名,验证私钥所有者的身份,不能防止内容被读取
签名:对摘要结果做加密的形式来保证签名的适用性。
数字证书
无法通过签名达成信任,此时就需要数字证书
公开密钥基础设施(Public Key Infrastructure,PKI)
一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。
公开密钥基础建设借着数字证书认证中心(Certificate Authority,CA)将用户的个人身份跟公开密钥链接在一起。
注册管理中心(Registration Authority,RA) 确保公开密钥和个人身份链接,可以防抵赖。
数字证书包含以下内容
- 版本号(Version):使用什么版本的 X.509 标准
- 序列号(Serial Number): 由证书颁发者分配的本证书的唯一标识符。
- 签名算法标识符(Signature Algorithm ID):用于签发证书的算法标识,由对象标识符加上相关的参数组成。
- 认证机构的数字签名(Certificate Signature)
- 认证机构(Issuer Name): 证书颁发者的可识别名。
- 有效期限(Validity Period)
- 主题信息(Subject):证书持有人唯一的标识符(Distinguished Name)。
- 公钥信息(Public-Key): 包括证书持有人的公钥、算法。
传输安全层
隔离复杂性的最有效手段就是分层。
TLS(Transport Layer Security)
1.0: 两轮四次(2-RTT)握手
1.3 首次连接仅需一轮(1-RTT)握手即可完成
1.3 在有连接复用支持时,甚至将 TLS 1.2 原本的 1-RTT 下降到了 0-RTT,显著提升了访问速度
以TLS 1.2 为例,上下两轮(四次通信)
客户端请求:Client Hello
客户端向服务器请求进行加密通信,在这个请求里面,它会以明文的形式
- 支持的协议版本,1.0 至 3.0 分别代表 SSL1.0 至 3.0,TLS1.0 则是 3.1,一直到 TLS1.3 的 3.4
- 客户端生成的 32 Bytes 随机数(加密的密钥)
- 一个可选的 SessionID(传输安全层的,为了 TLS 的连接复用)
- 一系列支持的密码学算法套件 ,
TLS_RSA_WITH_AES_128_GCM_SHA256:密钥交换算法是 RSA,加密算法是 AES128-GCM,消息认证码算法是 SHA256
- 一系列支持的数据压缩算法。
- 其他可扩展的信息(后续扩展都加到这个边长结构中)
服务器回应:Server Hello
客户端声明支持的协议版本和加密算法组合与服务端相匹配的话,就向客户端发出回应。如果不匹配,将会返回一个握手失败的警告提示。这次回应同样以明文发送的,包括以下信息:
- 服务端确认使用的 TLS 协议版本。
- 第二个 32 Bytes 的随机数,稍后用于产生加密的密钥。
- 一个 SessionID(通过连接复用减少握手)。
- 服务端在列表中选定的密码学算法套件。
- 服务端在列表中选定的数据压缩方法。
- 其他可扩展的信息。
- 如果协商出的加密算法组合是依赖证书认证的,服务端还要发送出自己的 X.509 证书,而证书中的公钥是什么,也必须根据协商的加密算法组合来决定。
- 密钥协商消息,这部分内容对于不同密码学套件有着不同的价值,譬如对于 ECDH + anon 这样得密钥协商算法组合(基于椭圆曲线的ECDH 算法可以在双方通信都公开的情况下协商出一组只有通信双方知道的密钥)就不需要依赖证书中的公钥,而是通过 Server Key Exchange 消息协商出密钥。
客户端确认:Client Handshake Finished
客户端收到服务器应答后,先要验证服务器的证书合法性。如果有问题会显示一个“证书不可信任”的警告,由用户自己选择是否通信。如果证书没有问题,客户端就会从证书中取出服务器的公钥,并向服务器发送以下信息:
- 客户端证书(可选)。(某些服务端只对提供客户端证书的提供),这种成为双向TLS,这是云原生基础设施的主要认证方法,也是基于信道认证的最主流形式。
- 第三个 32 Bytes 的随机数,这个随机数不再是明文发送,而是以服务端传过来的公钥加密的,它被称为 PreMasterSecret,将与前两次发送的随机数一起,根据特定算法计算出 48 Bytes 的 MasterSecret ,这个 MasterSecret 即为后续内容传输时的对称加密算法所采用的私钥。
- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
- 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的哈希值,以供服务器校验。
服务端确认:Server Handshake Finished
服务端向客户端回应最后的确认通知,包括以下信息。
- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
- 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的哈希值,以供客户端校验。
到这里一个安全的连接才建立,每一次建立都产生了很多的信息:随机密钥、加密算法、压缩算法等,后面该通道的通信都要通过这个密钥进行加密、解密、通信,性能有所下降,但对其他层完全透明。建立在安全层上的HTTP协议(HTTP over SSL/TLS),采用不同的协议、证书、策略等也会导致安全强度的不同,所以也不能说用了HTTPS不会有问题了。
验证
Java Bean Validation
- 对于无业务含义的格式验证,可以做到预置。
- 对于有业务含义的业务验证,可以做到重用,一个 Bean 被用于多个方法用作参数或返回值是很常见的,针对 Bean 做校验比针对方法做校验更有价值。利于集中管理,譬如统一认证的异常体系,统一做国际化、统一给客户端的返回格式等等。
- 利于多个校验器统一执行,统一返回校验结果,避免用户踩地雷、挤牙膏式的试错体验。