我们经常会听说登录(login), 验证(authenticate), 授权(authorize)。他们的区别和具体的实现到底是怎么做的呢?以及和单点登录(SSO)的关系又是什么呢?和cookie, session, JWT又有怎样的联系呢?
怎么说起呢?
如果从用户的角度来看,可能他们只知道登录。但是如果用了SSO那么还可能需要授权,比如集成github登录,那么跳转到github界面后,github会弹出界面说某某第三方程序需要访问你的用户名,头像等信息,你是否同意。
如果从开发人员的角度来看,就是前端发来了请求,说是用户要登录了,那当然就是要验证请求里的用户名和密码。登录成功之后用户当然是要访问某些功能,那么访问的请求又来了,而为了安全我们需要再次验证请求里是否包含合法的凭证。因为登录的请求和访问功能的请求是两个独立的http请求,而http是无状态的协议,所以我们得为需要保护的功能反复地验证合法的凭证。验证通过之后,那就是权限控制了。比如有的用户只能查看某一个功能,但是不能求改里面的内容。那么当发来求改请求,并在验证合法之后进行授权的判断。如果不是“编辑”角色,那就禁止进一步操作。同样的因为http是无状态的,所以需要保护的api都要再次授权。
简单来说就是,登录之后,在访问需要保护的接口时,“验证”和“授权”都需要反复进行。
这个过程中,我们当然最关心的问题就是这个凭证的问题了。怎么保存和使用它?如何在每次的请求里都带上?这就需要用上我们的Cookie或JWT。
在浏览器上,每个请求都会自动带一个Cookie;而如果在非浏览器上,比如微服务里面,服务之间互相调用通常来说都是用的JWT并配合HTTP Authorize。
Cookie JWT
Cookie和JWT在传输上并没有什么本质的区别,无非是http请求里的header不太一样。比如Cookie的Header对应Cookie,JWT通常放在Header Authorization里。当然对于JWT你可以自定义一个Header,比如叫X-Token,服务端就可以根据对应的值来验证和授权,但是用HTTP Authorize是更标准的做法。
Cookie出现在上个世纪90年代,正是是浏览器大战的时候,最初的出现是为了解决session状态的问题,即购物车的管理,你需要确认是哪个用户,并且选购了哪些商品。后来慢慢成了标准。Cookie涉及的东西方方面面,以后专门记一篇。
Session
当然此处还必须提到的就是session。session有客户端的,也有服务端的。
比如在浏览器上当你把窗口关掉那么session就结束了,相关的数据也会自动清空。比如有的Cookie的生命周期就是session的周期。
session在服务端,通常用来解析请求里面带的凭证,然后进一步给验证或授权的时候用,当然也可以根据具体业务需求来加以利用;它会有一个唯一的ID,很多安全问题也都是和session里的ID有关。
另外,因为通常服务端接口都是放在反向代理后面,并配置了负载均衡。所以如果一个用户登录了服务端接口实例1,下一个请求却跑到了服务端接口实例2,那就会爆错误了,因为实例2上并没有建立的session。所以通常在配置反向代理的时候需要配置session affinity。当然你也可以专门做一个分布式的session管理服务,或者放在缓存比如redis里等。
而使用JWT通常来说就不用关心这个问题,因为它是无状态的,当然它也有它的局限,比如要撤销某个token就不太方便。
单点登录
最后来说单点登录。目的很简单,就是一个账号来访问多个系统。从WikiPedia上看,真正的单点登录是让用户登录一次,然后访问其它系统的时候不用再输入验证信息。也就是是严格意义来讲,用github的账户来登录我的网站其实不算单点登录,因为需要我在跳转出来的github页面里来点击同意,至少第一次需要。但是呢,我目前理解地还是比较宽泛,或者说是不深入,除了我们公司内部使用的系统是SSO之外,还真没接触过其他场景,缺乏一个更具象的认识。
从不严格的定义来讲,用github账号登陆很多其他非github出品的网站,比如dev.to,掘金,抑或是slaveoftime.fun就是单点登录。而在大公司里面你通常可以用一个账户来使用office, slack, jira等。
我这个小网站的SSO实现可以分为下面几个步骤:
我需要把自己的网站在github里面注册一个应用https://github.com/settings/developers
注册的目的是让github生成相应的密钥和应用Id(用于识别注册的应用),并配置一个回调路径(Authorization callback url),比如我配置的就是https://www.slaveoftime.fun/api/v1/auth2/github
服务端为上面配置的回调路径来实现相应的接口
这个接口里面会获取从github跳转来的请求里面的数据(code, state),结合前面的密钥和应用Id,你可以向github发一个请求获得相应的access token,用这个token你就可以访问用户在跳转到的GitHub页面也授权的资源,比如名字和邮箱。
接着你可以用获得的信息注册到你自己的系统里面,并生成一个属于你自己的应用的token,返回一个302跳转的状态,并把生成的token放在query里(这里会有安全问题,以后再专门讨论)。
而要跳转的地方就是你自己的应用页面,比如我的就类似"/#/login?token=",这样在浏览器里就会跳转到登录界面,我会在这个界面里判断query里是否有token,如果有就把token保存到本地,并直接转入登录成功后应该要去的地方。
当然很多实现可以不用自己去做了,.net生态里有一些nuget包可以帮你做。我的实现主要是去厘清原理,练练手。我的实现可以说是最初级和很不安全的一种了,当然这也是在我学习这个主题的时候发现的。
OAuth2本身是用来做授权的,用来做验证其实是有些勉强的;另外,现在的做法更多是用的OpenID Connect,它是基于OAuth2建立的,那么基于它并配合PKCE(Proof key for code exchange)那就是目前来说比较好的了。等多的就放在优化我这个网站的时候再探讨了。
SSO的实现方式有很多,我上面举的例子是用的OAuth2。很多企业内部会搭建自己的服务器来做授权,比如可以用.net里的identity server4来搭建。
既然是单点登录,那很可能也就有了单点故障。比如一旦账户泄漏,那所有能访问的应用都将被攻破(这个可以通过2FA等来加强);又比如授权的服务器宕机或故障。
所以从这里可以知道,为什么.net里的identity server会那么受欢迎,因为可以省掉很多重复的工作量啊,尤其是让对这个领域没有深入研究的程序员来自己实现一个,那得多不靠谱啊(就比如我先前自己做的那个),毕竟企业里面可能很多应用都指望着这个服务勒。现在还有很多云的解决方案,比如微软提供Azure Active Directory,这样一来你连identity server都不用自己搭建和维护,别人来给你提供,并让其高可用。公司有钱,就是买买买啊,谁想自己弄啊。不过面试要问,自己还是得弄一弄,不然就像我前不久的面试一样,从尴尬到挂。
最后,
在我查资料和学习的过程中,发现是越看内容是越多。这个主题涉及的细节和实现都特别多,还有很多东西需要进一步查资料和学习,尤其是安全方面的🐱🏍。