一旦涉及到用户, 那么安全就上一个层次了.
这篇主要是说说一些安全的基础
1. 用户密码保存
网络上有太多资料说这些基础了, 我就不拉过来了. 大至少记入一些重点就好了.
- 为什么不可以明文保存
因为用户习惯一个密码到处用, 一旦我们的数据库被黑了,hacker 就可以拿密码去其它网站试. 用户把密码交给我们, 我们就必须确保它的安全性. 哪怕是被黑了, 密码也不可以被公开.
- 用什么方式保存密码
identity 使用的保存方式是 pbkdf2, 这个也是目前很 standard 的方案了. 几乎哪里都是用这个.
里头有 3 个概念: 哈希函数, 盐, 慢哈希.
哈希函数
哈希函数 的代表是 sha256, 早年的是 MD5. 许多地方都会用到哈希函数的. 不只是保存密码
它主要的功能就是把内容变成乱码, 另外它有 2 个特性:
- 不可逆, 乱码是无法还原内容的
- unique, 不同的内容哈希出来的结果一定是不相同的,同一个内容哈希出来的结果一定是相同的.
有了特性 1, 那么我们就可以把密码变成乱码, 而 hacker 即使拿到乱码也无法恢复密码了
有了特性 2, 我们在验证用户密码的时候, 只需要把密码进行哈希, 然后和保存的哈希值进行比对, 就可以确定用户是否输入密码正确了.
盐
只使用哈希函数依然是不够安全的. 因为有一个东西叫彩虹表, 里面记入了各种密码的 sha256 版本
hacker只要拿到数据库. 然后通过与彩虹表的匹配就可以查出密码了, 这等于恢复了原文.
为了解决这个问题, 就有了加盐的概念. sha256(sha256(password) + 盐) 大概长这样.
盐就是一个随机值. hacker 无法提前知道盐 (盐是随机的) 就无法做出带了盐的彩虹表, 所以彩虹表就不复存在了.
这里经常会有 3 个疑问 :
1. 盐需要保存吗 ?
需要, 因为在用户登入验证密码时, 我们必须要可以做出一摸一样的乱码来匹配丫
2. 盐要保存在哪里 ?
identity 的盐就放在 PasswordHash column. 就和用户资料放一起就可以了, 不需要特别分表之类的.
3. hacker 拿到盐之后是否可以做出彩虹表来破解 ?
彩虹表的特性是提前做一个笛卡尔积的密码哈希. 它的关键就是提前. 如果没有办法提前做, 那么就等于是用暴力破解了.
慢哈希
慢哈希就是用来防暴力破解的. 它其实就是不停的迭代执行哈希函数. 如果哈希需要 1 秒来完成. 那么 hacker 每试一个密码就需要 1 秒.
hacker 需要笛卡尔积试这么多密码组合, 每个需要 1 秒那就天荒地老了, 也就放弃了.
慢哈希会让用户登入也变慢. 但是影响不会太大, 所以是 ok 的.
2. 对称加密
identity cookie 就使用到了对称加密.
asp.net core 的 data protection 就是用来做对称加密的. 里头运行的是 AES 算法.
对称加密的目的就是防窃听. 没有密钥就推到不出密文. (密钥是很长的字符串, 很难被暴力破解的)
对比非对称加密, 对称加密的优点是 快, 要加密的内容可以很大, 缺点是只有一把钥匙. 如果要让接收者看到内容就必须把密钥给他, 而同时他也具备了加密的能力 (这或许并不是我们想要的)
3. 非对称加密
非对称加密有 2 把密钥
任何 1 把加密, 另一把就解密.
一开始我们会任选出一把叫公钥,另一把就叫私钥.
公钥是可以公开给外人知道的, 私钥则只可以自己持有.
对比对称加密, 它的优点就是能把加密和解密拆分, 我加密, 你只能解密 (不能加密).
缺点是慢, 还有内容不能大. 内容的长度不能超过密钥, 密钥越长加密就越慢
非对称加密可以解决通讯的 3 大难题
1. 防窃听
使用收信人的公钥对内容加密. 这样就只有收件人可以解密看到内容,这样就确保了外人无法窃听到内容.
2. 防冒名 和 3. 防篡改
先说一下什么是"消息摘要", 一个内容被哈希函数过变成的乱码就叫消息摘要
使用发信人的私钥对消息摘要加密 (这个就叫数字签名), 然后把内容和签名一起发出去. 收信人用发信人的公钥解密得到消息摘要, 如果解密成功就确定了发信人身份
然后拿内容做哈希得到另一份消息摘要, 对比 2 个消息摘要如果是一样的就确认了内容没有被篡改过.
这里有一个点要注意, 防窃听使用收信人公钥加密, 而签名则是用发信人的私钥加密. 所以整个过程里面是有 2 pair key (总共 4 把密钥哦)
在 asp.net core 我们要做非对称加密的话会用到 x.509 然后它是 RSA 算法.
4. https 流程
https 是最能体现对称加密的场景,
首先 server 和 client (游览器) 要通讯, 然后要防窃听
那么就需要对通信内容进行加密. 这种场景一般上就是用对称加密 (它就是用来防窃听的丫)
https 确实也是用对称加密来处理的, 但是对称加密需要双方都有密钥啊, 它们没有沟通过又怎么会有相同的密钥呢 ?
这个对称加密的密钥是游览器随机创建的, 并且在第一次加密通讯中传递给服务器 (这个过程叫握手)
而这个第一次的加密通讯使用的是非对称的加密而不是对称加密. 这就是关键了
首先游览器向服务器要求服务器的公钥 (用于第一次通信的加密, 非对称加密是使用收信人公钥来做加密的, 上面有讲过了)
这时游览器就有了公钥...等等这个公钥安全吗 ? 怎样确定这个公钥就是 server 的, 可能第一次通信就被掉包了丫...
这里就需要引入一个新概念叫数字证书了.
服务器要发布公钥可不简单 (以前还需要付费呢). 首先服务器需要向一个 CA 机构申请证书. CA 机构有 2 把密钥. CA 的公钥是 pre install 在 OS 里头的 (这个是关键哦)
当服务器把公钥发给 CA 机构之后, CA 机构用私钥做签名(上面有讲过签名了) 然后给回服务器 (这个东西就叫证书)
所以当游览器向服务器要公钥时,服务器返回的是证书 (公钥 + CA 的签名)
游览器用 CA 的公钥解密 (游览器有 CA 公钥因为它时 pre install 在 OS 里的), 验证签名后, 就可以确定内容是证实的了. 内容里面有 domain 等等服务器信息
如果内容不匹配 (比如 domain 和要访问的地址不一致) 那么游览器就会给用户提示.
如果 ok 的话,那么就拿公钥去加密对称加密的密钥然后发给服务器
服务器收到后就用私钥解密就得到了对称加密的密钥了. 往后的沟通就用这个密钥加密就 ok 了.
5. oauth jwt 中的签名
还有一个比较有名的非对称加密场景就是 json web token
由于 resource server 需要确定 jwt 是由 autho server 签发的. 所以需要做一个身份确认.
和 https 有点像,首先 resource server 去和 autho server 要公钥 (要验证签名就需要发信人的公钥) 这个过程叫 discover
和 https 不同的是这里不需要什么 CA 机构, 因为这个通信是已经建立在 https 基础之上了.
有了 https 的知识,去理解 jwt 就容易很多了。
6. key rotation
密钥要定期更换, 我们知道东西放久了就难免有疏忽的时候, 所以最好的情况是久不久就换一个新 key
我们拿 asp.net core data protection 来举例.
假设我们 3 个月换一次密钥. 替换当天, 所有新的 cookie 都会用新的密钥加密.
而解密则是新的旧的都会尝试, 所以并不可以直接把旧的密钥给丢了哦. 丢了你就没办法解密之前加密的内容了.
非对称加密也是一样的道理. 所以在使用 oauth 库的时候 (identity server 4, openiddict core) 都是可以 fill in 多个 keys 的, 就是让我们搞 key rotation 或者叫 key rollover 的.
7. token self-contained or reference
这里的 token 可以指 oauth 的各种 token 也可以指 identity cookie.
reference 的意思是 token 本身并没有任何有用的资料, 它只是一个 Id, 需要通过去数据库拿才可以获取到有用的资料.
这种做法的好处就是 token 轻, 安全, 数据总是最新的. 坏处就是频密的访问数据库.
为了减少数据库的访问让性能快一些 (尤其是 oauth 的情况, 可能还需要通过 api 来访问而不是直接数据库, 效率会更慢)
于是就有了 self contained 的概念. 也就是把一堆的 claim 放到 token 里头, 使用加密解密的方式确保内容安全.
这个做法最大的缺点就是无法立刻摧毁一个 token, 只要 token 在有效期内, 那么它就一定有效. 这也是为什么 access token 时间一般上都很短.
比如 identity cookie 就是 self contained 的, 它默认 check valid 是 30 分钟, 超过 30 分钟就去检查 securitystamp 是否有修改, 有的话就不 valid 了.
但在 30 分钟内, 你无法改变或注销这个 cookie 的权限.
8. oidc 和 oauth
我刚开始接触 oauth 的时候它就是 2.0 了, 现在估计也没有人用 1.0 了吧. 我所知道的是 2.0 是一定要走 https 的, 而 1.0 可以不需要的 (据说 2.0 没有 https 的话, 安全程度还输给 1.0 没有 https 哦)
早年也是没有 oidc (open id connect) 的, 我们用 oauth 来获取用户数据. 没有 identity token 这个东东, 只有 access token 和 refresh token. 好, 进入正题
oauth 2.0
oauth 最早的用法是 external login, 比如用 facebook account 去注册其它网站.
抽象一点讲就是让 facebook 的用户授权给某网站, 允许访问用户在 facebook 的资料
这个过程是 用户 A 到了 网站 B 然后跳转到 facebook 做一个 login + 允许授权 (依据网站 B 要求的权限) 用户允许后就跳转回网站 B, 这时网站 B 就可以访问到用户 A facebook 的信息了 (甚至是 添加, 修改, 删除, 依据刚才授权的范围)
早年我们一般是属于网站 B 这个的角色. 随着前后端分离. 手机也好, web app 也要. 我们就扮演起了 facebook 这个角色. 让我们来理一下这个关系和它们对应的名称.
facebook login + 授权网站 = autho server
user = 我们网站的用户
网站 B = 我们的手机 app / web app = client
facebook 资料网站 = 我们的 web api server = resource server
虽然感觉有一点点大材小用, 但是现在前后端分离确实就是这样搞的. 自己的用户在自己网站 login page 授权给自己的 web app 去访问自己的 api...
oauth 有几个比较有名的 flow
1. client credentials
它是用于 server to server 的, client 需要 client id + client secret 就可以通过 autho server 获取到 access token 去访问 web api 了.
这个跟用户没有任何关系, 通常用于非常信任的 client
另外需要注意的是 client secret 是不可以外泄的哦, client secret 只能出现在 server 端而且要用 azure key vault 来保护 (它属于这种级别)
2. implicit
这个是一种很轻量的方式, 使用的场景我没有遇过
猜测就是那种需要用户短暂授权的 web app. 这种 flow 是不需要 client secret 的, 它也只返回 access token 不会有 refresh token
如果我有一个 web app 让用户 export 图片, 那么就可以用这个 flow 咯
3. password
这个也是很奇葩的模式, 直接使用用户密码来登入...我猜不出场景
4. authorization code
这个就是最常用的 flow 了, 好好说说这个呗 (前后分离基本上也是用这个 flow 只是在做了一些修改而已)
我用会 B 网站和 facebook 的例子
首先 B 网站 需要向 Facebook 申请成为 client, 那么 B 网站 就有了 client id 和 client secret, 而 facebook 也记入了这个 client 的资料和 redirect url (这个挺重要的哦)
B 网站首先把用户 redirect 到 facebook 附上 client id, scope (要求授权的范围), redirect url (必须和申请的时候一致), state(一个随机数, 等下 facebook 会原封不动返回这个 state)
到了 facebook 用户就 login 授权. 这时 facebook 就会 redirect 用户到 client 的 redirect url. 并且附上一个 code 和 刚才的 state
在网站 B 的后端, 确认一下 state (要确定是我们发出去的申请), 然后拿 code + client secret 去 facebook 换取 access token 和 refresh token
从现在开始就可以用 access token 去访问 facebook 资料了.
通过 refresh token 我们可以一直保持 access token active 直到 refresh token 到期为止.
上面这个 flow 并不完全适合做前后端分离的 web app, 因为 web app 是没有后端的, 没有后端就不可以有 client secret. 上面也就无法搞下去了.
所以后来就有了多一个 flow
5. authorization code + pkce
refer:
https://tonyxu.io/zh/posts/2018/oauth2-pkce-flow/
https://www.cnblogs.com/newton/p/13220207.html
pkce 的关键就是要解决掉 client secret 的问题. 它的思路就是搞一个临时密码.
我自己有一个不清楚的是...中间人拦截了又怎样呢, 我都 https 了他应该什么也看不到吧....搞不明白
这里我想提一下关于 autho server 和 client 需要注意的安全 (虽然一般 oauth 的库都会帮我们搞定好一切)
autho server 最重要的是只能把 code 交给正确的 client, 所以一定要使用数据库里面 client 注册的 redirect url
autho server 在发送 access token 时会需要 client secret 的原因是双重保证. 按理说只有 client 能拿到 code, 和 refresh token
那么只要有 code 和 refresh token 就 ok 了的. 但是如果 refresh token 不小心弄丢了, 那就很麻烦了. 所以为了双重保险使用 client secret 就安全一点, 即使 refresh token 丢了.
对方也不一定有 client secret 哪怕有, 我依然可以换掉我的 secret 来让 refresh token 不 valid. 所以能用 secret 的时候就尽量用呗.
oidc (open id connect)
了解了 oauth 再去了解 oidc 就简单多了.
如果 oauth 是授权, 那么 oidc 就是 authen, 由于 autho 一定要先 authen 所以大家就认为有 autho 就好了丫.
但后来大家发现 external signin 其实不能算是授权. 因为如果我只是单纯要求用户使用 facebook 登入替代密码的话, 我并没有需要任何 facebook 的资料丫.
所以后来就有了 authen 的概念. 然后又有了 identity token 概念 (用来获取用户信息) 它把 access token 理解为比较复杂的操作, 而向获取用户资料这种小操作交给 identity token 来负责.
我觉得这样其实挺好的.
9. JOSE (JWT, JWS, JWE, JWK)
refer:
https://onevcat.com/2018/12/jose-1/
https://www.cnblogs.com/felordcn/p/12142477.html
https://apiacademy.co/2020/01/the-benefits-of-jwt-jws-jwe-on-api-designs/
https://medium.facilelogin.com/jwt-jws-and-jwe-for-not-so-dummies-b63310d201a3
https://www.scottbrady91.com/C-Sharp/JSON-Web-Encryption-JWE-in-dotnet-Core#code
其实, 每次我们说 Json web token (JWT) 讲的是 Json web signature (JWS) 来的
整个 JW 体系是很大的, 叫 JOSE (Javascript Object Signing and Encryption)
我是因为看到关于 jwt payload 加密才一路找过去的. payload 以前是没有加密的. 只是提倡说不要把敏感数据放进去.
但近年来视乎越来越多人会往 payload 放一些权限的资料 (为了 self contained 减少请求, 提高效率嘛) 所以就开始要加密了咯.
JWT 和 JWS 算是继承关系,但是由于大家叫惯了所以可以把它们看成是一样的.
JWE Json web encrypt 就是加密 payload 了.
这里加密的目的是防窃听, autho server 加密, resource server 解密. 通常看到这种情况就少不了对称加密 + 非对称加密一起上了 (原来这还有个名称叫 key wrapping)
如果 autho server 和 resource server 是同一台的话, 是可以只用对称加密的.
这里需要注意, 非对称加密用于防窃听是用收信人的公钥来加密的, 在这里就是说 autho server 需要有 resource server 的公钥才能加密 token.
refer: https://darutk.medium.com/oauth-access-token-implementation-30c2e8b90ff0
这里我有一个比较不太能理解的地方是. 如果我有不同的 resource server 那么该怎么办呢 ? 难道所有的 resoruce server 共享一对公钥私钥吗 ? 还是我不应该有多个 resource server 呢 ?
这里留个悬念以后搞明白了再回来看看, 毕竟目前项目是 autho resource server 在一起的. 并不会遇到这些问题.