背景介绍
主要实现 OAuth2的三种授权模式:密码模式、客户端模式和授权码模式,包括展示授权服务器、资源服务器、客户端等几种角色的交互,以及JWT的整合。并且每个实例都提供两个代码版本:一个是基于旧的 Spring Security OAuth2 组件;一个是基于新的 Spring Authorization Server 组件。
注意的是 password 模式由于 OAuth2.1 不推荐使用所以只能提供旧的组件代码版本,具体请参见 https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-02
OAuth 授权体系设计之初主要是为了解决第三方应用登录和授权的问题,但由于其严格规范的流程定义,广泛的授权通用性,且与具体技术平台无关等诸多优点,逐渐发展成为认证和授权领域的主流技术规范。但其实 OAuth2 规范归纳起来并不复杂,就四种主要的授权模式和五种角色。
OAuth2 体系结构
OAuth授权体系设计之初主要是为了解决第三方应用登录和授权的问题,但由于其严格规范的流程定义,广泛的授权通用性,且与具体技术平台无关等诸多优点,逐渐发展成为认证和授权领域的主流技术规范。但其实 OAuth2 规范归纳起来并不复杂,就四种主要的授权模式和五种角色。
OAuth2.0 的四种授权模式
-
授权码模式(authorization code)
-
简化模式(implicit)
-
密码模式(resource owner password credentials)
-
客户端模式(client credentials)
-
密码模式常用于外部服务的认证、授权和鉴权,客户端模式常用于内部服务的认证、授权和鉴权和开放平台应用的授权,授权码模式常用于社会化登录和SSO,因此OAuth2.0 可作为完整的统一身份认证和授权方案。
-
四种模式都有其特定的使用场景,但是在落地过程中,也可以根据实际情况自行取舍。为了方便接下来的介绍,基于图像的物品分类系统(IBCS,Image-Based Classification System)、相册预览系统(PAPS,Photo Album Preview System)。
授权码模式 vs 密码模式 vs 客户端模式
授权码模式
先来看授权码模式和密码模式之间的比较,
授权码模式是OAuth2体系安全性最高的模式,密码模式与其相比,主要差别是少了一层用户确认授权的动作,缺乏这一动作就导致在授权阶段,用户需要把用户名密码告知客户端,造成潜在的密码泄露风险。
比较项 | 授权码模式 | 密码模式 | 备注 |
---|---|---|---|
适用场景 | 不可信/第三方认证和授权、可信/内部服务认证和授权 | 可信/内部服务认证和授权 | |
开发难度 | 较为复杂 | 相对简单 | |
安全性 | 最高 | 只要不运用于不可信/第三方场景则同样安全 |
使用场景分析
可信/内部服务场景
同一套OAuth2体系的应用和服务,且这些应用和服务是由相同的或者相互信任的团队开发,也可以称作第一方应用。
微服务架构下的 B2C 商城系统,基本组成有前端 H5、无线端 APP、API 网关、认证授权服务、订单服务、商品服务等,由于上述所有组成部分都同属于一套OAuth2 体系,且都由相同团队开发,那么他们全都归属于可信/内部服务场景。
第三方/不可信应用
-
网关属于安全边界,网关以内的认证授权服务、订单服务、商品服务属于内部服务,而前端H5、无线端APP则属于外部应用,如果这些外部应用是其他团队开发,我们也可以定义它们为第三方应用。这样就以网关为边界,划分出了内部服务和外部服务。
-
电商系统的前端H5、无线端APP在认证和授权阶段,采用授权码模式和采用密码模式,授权码模式有一层用户确认授权的动作,从而避免泄露用户名和密码给第三方应用,除此之外,两者之间几乎提供了相同的安全流程。
以此来看,在上述条件的限定下,两种模式的安全性打平了。遵循“简化原则”,采用密码模式即可。
授权码模式
授权码模式主要的应用场景,是在第三方/不可信应用的登录和授权,主要解决在不泄露用户密码的情况下如何安全授权某个应用向另一个应用提供用户资源的问题,举例来说,某第三方应用(客户端)需要获得用户(资源所有者)在另一个不可信应用(资源服务器)上的该用户的用户数据(资源)的场景就特别适合采用授权码模式。
综上,选择授权码模式还是密码模式,具体要根据业务场景来确定,其中的关键决策点是应用或服务之间是否互相信任。
客户端模式
采用何种授权模式?
首先要明确资源是什么,其次该资源是受保护的,最后资源归谁所有,谁就是资源所有者。明确资源所有者的含义后,再根据前文的分析,毫无疑问:如果PAPS的demo应用是第三方的不可信应用,则应该采用授权码模式;如果是第一方可信应用,则可以采用密码模式,当然不怕麻烦也可以用授权码模式。
是密码模式的最精简架构层次,在实际开发中可以此作为基础进行扩展。密码模式涉及到五种主要角色,另外还有一个用户代理/浏览器角色。
OAuth2.0 的五种主要的角色:
- 用户代理/浏览器 User Agent
- 客户端 Client:一般指第三方应用程序,例如用QQ登录豆瓣网站,这里的豆瓣网就是 Client;但在微服务体系里,Client通常是服务本身,如 APP 端的注册登录服务;
- 资源所有者 Resource Owner:一般指用户,例如用 QQ 登录豆瓣网站,这里的所有者便是用户;但在微服务体系里,资源所有者的指向不是一成不变的,要具体分析;
- 资源服务器 Resource Server (受保护资源):一般指资源所有者授权存放用户资源的服务器,例如用 QQ 登录豆瓣网站,这里的 QQ 就是资源服务器;但在微服务体系里,服务提供者本身便是资源服务器;
- 授权服务器 Authorization Server:一般是指服务提供商的授权服务,例如用 QQ 登录豆瓣网站,这里的 QQ 便是授权服务器;类似地,在微服务体系里,IDP 服务便是授权服务器。
整个流程分为两个阶段:
第一阶段:认证授权阶段
- 前端页面将用户输入的用户名和密码,发送给客户端(process-service);
- 客户端(process-service)将用户输入的用户名和密码,连同 client_id + client_secret (由idp分配)一起发送到idp以请求令牌,如果 idp 约定了 scope 则还需要带上 scope 参数;
- idp 首先验证 client_id + client_secret 的合法性,再检查 scope 是否无误,最后验证用户名和密码是否正确,正确则生成 token。这一步也叫“认证”;
- idp 返回认证结果给客户端,认证通过返回 token,认证失败返回 401。如果认证成功则此步骤也叫“授权”;
- 客户端收到 token 后进行暂存,并创建对应的 session;
- 客户端颁发 cookie 给用户代理/浏览器。
至此,认证授权阶段完成。其中步骤 5-6 也有其他会话方案,比如REST型应用可能会将 token 存储在浏览器端,但 session/cookie 方案无疑是最稳妥的选择。
在Web 应用中\用户登录过程。
第二阶段:授权后请求资源阶段
- 用户通过用户代理,访问“我的资源”页面,用户代理携带 cookie 向客户端(process—service)发起请求;
- 客户端通过session找到对应的 token,携带此 token 向资源服务器(resource-service)发起请求;
- 资源服务器(resource-service)向 idp 请求验证 token 有效性;
- idp 校验 token 有效性,再根据 scope 判断客户端(process-service)是否有权限调用此 API,最后返回校验结果给资源服务器。这一步也叫“鉴权”;
- 资源服务器根据 idp 检验结果(true/false 或其他等效手段)决定是否返回用户相册数据给客户端。如果 token 校验失败则返回 401 给客户端,如果 scope 检查不通过则返回 403。这一步也叫“权限控制”。
至此,授权后请求资源阶段完成。
scope 参数是用来约束客户端的权限的,跟用户权限(authorities)是不同的。比如可以在 idp 中利用scope参数约束某客户端只能发起读(GET)型请求,或只能调用指定的几个 API 等,具体业务逻辑自行编写。
密码模式的微服务架构层次和主要流程
-
微服务场景下,增加了一个网关,网关实际上是一个反向代理,将用户的请求转发到内部服务器。类似地,微服务场景下也分为两个阶段,而且第一阶段没什么变化,主要不同在于第二阶段:
-
用户通过用户代理访问“我的资源”页面,用户代理携带cookie向客户端(process—service)发起请求;
-
客户端通过session找到对应的token,携带此token向网关发起对资源服务器(resource-service)的请求;
-
网关截取token连同本次请求的细节,一并向idp请求校验;
-
idp 校验 token 有效性,再根据 scope 和请求细节判断客户端(process-service)是否有权限调用此 API,最后返回校验结果给网关。
- 如果校验全部通过,idp 生成 JWT 并返回给网关;如果 token 校验失败返回 401;如果 scope 检查不通过则返回 403;
- 如果校验通过,网关将得到 JWT,携带此 JWT 转发请求到资源服务器;
-
资源服务器解析 JWT 得到用户信息,查询用户相册数据后返回给网关;
-
网关将用户数据返回给客户端。
-
网关截取 token 后向 idp 请求校验;
-
idp 校验 token 有效性,通过校验则根据 token 查询用户信息和 scope,生成 JWT 返回给网关;如果不通过则返回 401;
-
网关得到 JWT,解析后根据 scope 判断客户端是否有权限调用此 API,如有则携带 JWT 转发请求到资源服务器,否则返回 403 给客户端。
客户端权限检查放到网关,则网关要维护 scope 和客户端权限的逻辑。
授权码模式的微服务架构层次和主要流程
授权码模式在微服务场景下的架构层次和主要流程:
整个流程分为两个阶段:
第一阶段:认证授权阶段
-
用户在用户代理处点击登录按钮,或请求授权登录按钮,此操作将访问客户端的某个 URI;
-
客户端(process-service)将用户导向 idp 提供的认证授权页面,并在页面 ULR 参数中携带 client_id,response_type=code,redirect_uri(可选),scope(可选),state(可选);
-
用户通过用户代理,在 idp 的认证授权页面选择是否给予授权,如用户未登录,则需要先登录后再操作;
-
用户给予授权,idp 将用户导向 redirect_uri 指定的页面,并附加授权码(code);如果未指定 redirect_uri,则导向发起该请求时的 URI,同时附加授权码(code);
-
客户端收到授权码(code),向 idp 发起令牌申请,同时附上 client_id(必填) + client_secret(必填) + state(如有) + scope(如有)。注意这一步是客户端在后台发起的,用户层面无法感知;
-
idp 收到请求后,先核对 client_id + client_secret + scope(如有)是否无误,然后校验授权码(code),全部正确后颁发 token 给客户端。
第二阶段:授权后请求资源阶段
- 用户在用户代理处点击登录按钮或请求授权登录按钮后,通知客户端(process-service),客户端收到通知后返回重定向的指示,以及 scope(可选),state(可选)等;
- 收到响应后,直接将用户导向 idp 提供的认证授权页面,并在页面 URL 参数中携带客户端返回的参数(除了 state 参数外,其他参数可以在页面中写死)client_id,response_type=code,redirect_uri(必选),scope(可选),state(可选),其中 redirect_uri 建议是必选的,而且必须是客户端提供的 URI(回调地址)。
- 用户在用户代理处点击登录按钮或请求授权登录按钮后,直接将用户导向 idp 提供的认证授权页面,并在页面 URL 参数中携带 client_id,response_type=code,redirect_uri(必选),scope(可选),但是不需要携带 state 参数,因为客户端不知道此参数的存在,其中 redirect_uri 建议是必选的,而且必须是客户端提供的 URI(回调地址)。
至此,授权码模式的认证授权全流程完毕。