用户与鉴权服务开发思路整理
这鉴权服务是一秒也写不下去了。
作为一个 web 服务,很大程度上要依赖一个稳定的用户与鉴权服务,整个服务才能跑得起来。最近在搞 xcpc-team-reg,想以此搭一个脚手架作为以后项目的服务基础。但是用户与鉴权服务基本都是通用的,所以就在这里整理一下。
什么是用户服务
简而言之,用户服务就是服务用户用的。
任何一个服务的背后都是 CRUD,也就是增删查改。根据权限控制可能对不同用户有所不同。比如从一个普通用户的视角来说,他可以:
- 增:注册
- 删:注销
- 查:查询自己的信息
- 改:修改自己的信息
但是从一个管理员的视角来说,他可以:
- 增:注册
- 删:注销
- 查:查询自己和其他所有人的信息
- 改:修改自己和其他所有人的信息
什么是鉴权服务
很多时候我们要判定用户是否有权限进行某个操作,比如非登录用户不能查看某些信息或进行某些操作,登录用户不能修改其他用户的信息等。
鉴权服务设计
鉴权服务在系统中频繁使用,因此一般作为中间件。
让我们考虑一个最简单的形式,比如门禁卡形式的登录。用户从一个机构拿到一张门禁卡,然后每次进出都刷一下门禁就可以通过。反映到这个服务里,就相当于用户拿到了一个凭证,每次检验这个凭证即可。
当然,HTTP 是无状态的,用户凭证不借助其他方法是不能持久化的。每次请求都要携带凭证的话,只能塞到 Cookies 里。
现在我们获得了一种方法:
- 登录的时候服务端下发凭据给客户端;
- 每次鉴权的时候客户端带着凭据,服务端检验;
- 登出的时候服务端销毁这个凭据。
我们也说过,任何一个服务的背后都是 CRUD,那么这里的 CRUD 体现在:
- 增:登录
- 删:登出
- 查:校验凭据
- 改:凭据一经发出便不能修改,保持权威性,所以不涉及改
这个系统的目标为:
- 微观:标记为某用户做的操作都是确实是这个用户做的,而不是被替代的;
- 宏观:所有操作都是符合对应权限的。
也就是说:
- 刷的门禁卡是具有权威性的,不可复制的,甚至说粘在某个人身上撕不下来的;
- 所有刷门禁卡放行的操作都是正确的,符合对应权限的。
第二点在逻辑实现上很好操作,因为主要还是业务实现的问题。第一点的实现就很困难,因为复制一个凭证是很简单的,通过中间人攻击或者重放攻击都可以轻松实现复制一个用户的凭证。一个简单的方法是使用 HTTPS,一个复杂的方法是在 HTTP 层自己加一个 TLS。
众所周知 HTTP 是无法防止中间人攻击的,也就是 HTTP 劫持,对于中间人攻击的防范其实也类似写个 TLS,综合重放攻击,其实就用 HTTPS 解决了一切传输过程中安全问题。
这下我们可以在 HTTPS 上明文传密码了,毕竟还是没有办法透过 HTTPS 拿到明文密码。
现在传输是安全的了,下面就是凭据本身的问题了。
凭据是一个 payload,每次请求都要带着这个东西,所以要尽可能小。一个选择是用 JWT,但是根据这篇文章的说法,还不如直接用一个 Session 管理。我们有内存级别的 Session 管理,并发控制利用 sync.Map 也可以做,效率还挺高的,但是客户端每加一个就多个 Session,高并发场景直接爆炸了,于是考虑 Redis 管理 Session,也是一个选择。
JWT 鉴权工作原理:
- 登录的时候服务端给客户端发一个签名的 JWT,存在 Cookies 里;
- 鉴权时,认证签名,并判断 JWT 是否过期。
这里的问题就有:
- 存在 Cookies 的数据可能会被 XSS 攻击拿到;
- 存在 Cookies 的数据可能会被 CSRF 攻击利用。
解决办法是服务端把 JWT 发到客户端,客户端扔到 Session Storage 里,然后在 Cookies 里加一个服务指纹,将指纹的 SHA256 放到 JWT 里。客户端的所有请求把 JWT 放在 HTTP 请求头里,鉴权时连同 Cookies 里的指纹一并检查。不要把 JWT 放在 Local Storage 里。
关于签名失效的问题,可以把 secret 存到 Redis 里并设置过期时间,这里管理也类似 Session。也可以使用 exp
配合 Cookies 过期时间使用。但是根据无状态原则,应采用后者。
使用 RS256 的话只使用一对公私钥即可,但是使用 HS256 只使用一个 secret 也是可以的。不知道为什么推荐 RS256……
总结来说就是:
- 使用 HTTPS 传输;
- JWT 放在 Session Storage 里,在服务端只检验 Header 里的 Authentication 字段;
- JWT 设置
exp
,Cookies 设置过期时间。
参考资料
以上。