《Spring OAuth2.0 开发指南》是系列文章,详细介绍基于 Spring 生态(包括 Spring Cloud) OAuth2.0 的实战开发。本系列将由五篇文章组成:
本系列第一篇主要聚焦 OAuth2.0 体系的概念介绍,其余各篇偏向于实战,主要实现 OAuth2.0 的三种授权模式:密码模式、客户端模式和授权码模式,包括展示授权服务器、资源服务器、客户端等几种角色的交互,以及 JWT 的整合。并且每个实例都提供两个代码版本:一个是基于旧的 Spring Security OAuth2 组件;一个是基于新的 Spring Authorization Server 组件。
需要注意的是 password 模式由于 OAuth2.1 不推荐使用所以只能提供旧的组件代码版本,具体请参见 https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-02
如果对认证和授权平台的相关理论感兴趣,可以移步本人早前的两篇文章:
OAuth 授权体系设计之初主要是为了解决第三方应用登录和授权的问题,但由于其严格规范的流程定义,广泛的授权通用性,且与具体技术平台无关等诸多优点,逐渐发展成为认证和授权领域的主流技术规范。但其实 OAuth2.0 规范归纳起来并不复杂,就四种主要的授权模式和五种角色。
四种授权模式,分别是:
五种主要的角色,分别是:
建议熟练掌握以上概念,理清其中的关系,有助于对整个体系有一个全局性的了解。倘若一上来就撸代码,那造出来的系统必然不健康。须知身份管理体系是任何平台的重要基础设施,一旦建造的不牢固,必然会种下隐患,好似大厦地基埋下的一颗延迟炸弹。
回到主题,四种模式都有其特定的使用场景,但是在落地过程中,也可以根据实际情况自行取舍。为了方便接下来的介绍,我们假设两套演示案例:基于图像的物品分类系统(IBCS,Image-Based Classification System)、相册预览系统(PAPS,Photo Album Preview System)。
为什么假设两套?主要是为了分析对比各种授权模式的适用场景。
假设团队正在开发一套“基于图像的物品分类系统(Image-Based Classification System)”,该系统采用 RESTful API 对外交互,主要功能是允许用户通过 H5 应用或 APP 上传图片,系统分析后返回分类结果。为了表演 OAuth2.0 体系认证、授权、鉴权和权限控制的全流程,我们构建一个最简化的 IBCS 演示项目:
服务名 | 类别 | 描述 | 技术选型
这里设计了一个 demo 应用,对于 IBCS 来说,demo 应用就是一个外部应用,是一个消费者,包括前端 demo-h5 和后端 demo-service。但是从整个演示项目来说,ibc-service、idp、demo-h5 和 demo-service 都是自家应用,是相互信任的。
值得注意的是,在现实世界里,应用有两种类型:一是有 Server 端的应用;另一种是无 Server 端的应用。前者比较好理解,它可能是前后端分离的 MVC 单体应用,也有可能是 REST 型的前后端应用,这些都是常见的开发结构。除此之外,还有一些没有或者不需要后端的应用,比如 SinglePage H5,由 Vue 或 React 开发的简单 H5,整个应用仅由 JavaScript 代码组成,前端即应用的全部。这种类型的应用,有一个最大的安全问题,即 client_secret 如何安全存储,在无 Server 场景中无论是经典的授权码模式还是密码模式,都无法有效解决这个问题,因为一个全部由 JavaScript 写成的程序,本质上是完全透明的,client_secret 存在泄露的风险。client_secret 将导致客户端被伪造,即攻击者在拿到 client_id 和 client_secret 后伪造一个 App 发起虚假请求。当然许多平台会在 idp 端增加 redirect_uri 的绑定或 IP/域名白名单机制,从而有效降低伪造/劫持等安全风险。
有人说 OAuth 2.0 规范提出的 PKCE(Proof Key for Code Exchange by OAuth Public Clients)协议可以解决这个问题,这是一个错误的观点,PKCE 在技术原理上并不能解决 client_secret 泄露的风险,所以对安全性要求高的情况下最好不要采用无 Server 端方式。不过 PKCE 作为一种增强协议可以搭配 OAuth2.0 组合使用以提高整体安全性。其主要流程为:由暴露在外的“前端”自身生成一串随机的验证码,再根据验证码生成挑战码,然后用挑战码向授权服务器请求授权码,授权服务器保存挑战码后返回授权码给“前端”,“前端”拿到授权码后用第一步生成的随机验证码+授权码向授权服务器申请令牌,授权服务器拿到验证码+授权码后根据此验证码生成挑战码,如果生成的挑战码跟上一步保存的挑战码一致则颁发令牌。有兴趣的同学可以自行查阅相关资料。
PAPS 是一个社交平台的子系统,与 IBCS 类似,采用 RESTful API 对外交互,主要功能是允许用户预览自己的相册,我们构建一个最简化的 PAPS 演示项目:
服务名 | 类别 | 描述 | 技术选型
两个演示案例唯一的不同就在于核心服务不同,一个是图像分类服务,一个是相册预览服务。看起来似乎没什么差别,实际上他们的业务模型还很不同的,接下来会分析这一点。
大家思考一下,授权码模式、密码模式和客户端模式,在不同的业务场景下该如何选择?
无非有两种做法:
第一种做法,简单粗暴,看起来好像不可取,但也不能一棍子打死,具体还是看项目的业务模型、设计目标和具体要求。但仅从科学严谨或者技术研究角度来说,肯定是第二种方式更加可取。接下来,我们就以第二种做法展开,详细分析下模式选型的问题。
我们先来看授权码模式和密码模式之间的比较,大家知道,授权码模式是 OAuth2.0 体系安全性最高的模式,密码模式与其相比,主要差别是少了一层用户确认授权的动作,缺乏这一动作就导致在授权阶段,用户需要把用户名密码告知客户端,造成潜在的密码泄露风险。我们看一下对比:
比较项 | 授权码模式 | 密码模式
在这里,可信/内部服务场景的定义是相对的概念,指纳入同一套 OAuth2.0 体系的应用和服务,且这些应用和服务是由相同的或者相互信任的团队开发,也可以称作第一方应用。打个比方,微服务架构下的 B2C 商城系统,基本组成有前端 H5、无线端 APP、API 网关、认证授权服务、订单服务、商品服务等,由于上述所有组成部分都同属于一套 OAuth2.0 体系,且都由相同团队开发,那么他们全都归属于可信/内部服务场景。
那么为什么又说是相对概念?我们把视角聚焦到整个商城系统,毫无疑问,网关属于安全边界,网关以内的认证授权服务、订单服务、商品服务属于内部服务,而前端 H5、无线端 APP 则属于外部应用,如果这些外部应用是其他团队开发,我们也可以定义它们为第三方应用。这样就以网关为边界,划分出了内部服务和外部服务,这就是所说的相对概念。
那么,电商系统的前端 H5、无线端 APP 在认证和授权阶段,采用授权码模式和采用密码模式,有什么差别吗?授权码模式有一层用户确认授权的动作,从而避免泄露用户名和密码给第三方应用,除此之外,两者之间几乎提供了相同的安全流程。这里所指的第三方应用不正是前端 H5 和无线端 APP 吗?都是自家开发的应用,自然是可信的,因此无须担忧泄露不泄露的风险。
以此来看,在上述条件的限定下,两种模式的安全性打平了。遵循“简化原则”,采用密码模式即可,当然喜欢挑战复杂的同学选择授权码模式也是可以的。
但是也不是说授权码模式就可以被密码模式取代了,授权码模式主要的应用场景,是在第三方/不可信应用的登录和授权,主要解决在不泄露用户密码的情况下如何安全授权某个应用向另一个应用提供用户资源的问题,举例来说,某第三方应用(客户端)需要获得用户(资源所有者)在另一个不可信应用(资源服务器)上的该用户的用户数据(资源)的场景就特别适合采用授权码模式。
综上,选择授权码模式还是密码模式,具体要根据业务场景来确定,其中的关键决策点是应用或服务之间是否互相信任。
PAPS 相册预览系统演示案例应该采用何种授权模式?
回答这个问题之前,大家先思考一个问题:在 PAPS 中,资源所有者所指代的对象是什么?
首先要明确资源是什么,其次该资源是受保护的,最后资源归谁所有,谁就是资源所有者。在 PAPS 中,很明显受保护的资源是用户的相册,资源所有者自然是用户本人。
明确资源所有者的含义后,再根据前文的分析,毫无疑问:如果 PAPS 的 demo 应用是第三方的不可信应用,则应该采用授权码模式;如果是第一方可信应用,则可以采用密码模式,当然不怕麻烦也可以用授权码模式。
接下来,我们来分析 IBCS 图片分类系统演示案例该用何种模式。
同样地,回答这个问题之前,大家再思考一下:在 IBCS 中,资源所有者所指代的对象是什么?
首先资源所有者所指代的对象不是一成不变的。在 IBCS 演示案例中,demo 应用向 ibc-service 传送一张图片,并希望返回分类结果,那么这里面的受保护资源具体是什么呢?不似 PAPS 相册预览服务有实体资源概念,其受保护资源是用户的相册,而 IBCS 难以抽象出一个实体的资源来。
可以这么理解,IBCS 提供的核心能力是图片分类算法,这就是它的受保护资源,图片分类算法的所有权人显然是持有此算法的实体组织或个人,因此资源所有者是该实体组织或个人。那么矛盾点来了,以密码模式为例,按照 OAuth2.0 的设计,资源所有者向客户端提供用户名和密码,客户端将 client_id 和 client_secret 连同该用户名和密码,向授权服务器申请令牌,此处的资源所有者是 IBCS 的普通用户,但是按照刚才所述,资源所有者应该是持有图片分类算法的实体组织或个人,这不就矛盾了吗?
因此,从资源所有者的角度来分析,IBCS 演示案例不适合采用授权码模式,也不适合采用密码模式,客户端模式才是最佳选择。IBCS 并没有用户的资源,授权码模式和密码模式都是需要用户授权才能跑通的,而 IBCS 提供的资源或服务并不属于用户,所以法理上来说不需要用户的授权,IBCS 提供的分析服务是跟用户无关的。
以上就是选择何种授权模式的一种分析模型,从资源所有者的维度切入。总结一下:
案例 | 场景 | 适合模式
其实资源所有者的具体指代对于实际开发来说,并不是一个重要的方面,关键是开发人员要有自己的理解,能够灵活巧用。说句题外话,虽然 OAuth2.0 是当今领域内的权威,但我们也不应盲目地无条件相信权威,技术发展一定是有漏洞和不完美之处的。
我们以 PAPS 相册预览系统为例,介绍密码模式的架构层次和主要流程。
如图所示,是密码模式的最精简架构层次,在实际开发中可以此作为基础进行扩展。密码模式涉及到五种主要角色,另外还有一个用户代理/浏览器角色:
整个流程分为两个阶段:
至此,认证授权阶段完成。其中步骤 5-6 也有其他会话方案,比如 REST 型应用可能会将 token 存储在浏览器端,但 session/cookie 方案无疑是最稳妥的选择。
在一般的 Web 应用中,可以将此阶段看作是一次用户登录过程。
至此,授权后请求资源阶段完成。
事实上 scope 参数不是核心的内容,实际工作中为了简化开发步骤甚至可以忽略它。scope 参数是用来约束客户端的权限的,跟用户权限(authorities)是不同的。比如可以在 idp 中利用 scope 参数约束某客户端只能发起读(GET)型请求,或只能调用指定的几个 API 等,具体业务逻辑自行编写。
我们仍以 PAPS 相册预览系统为例,介绍密码模式在微服务场景下的架构层次和主要流程。
微服务场景下,增加了一个网关,网关实际上是一个反向代理,将用户的请求转发到内部服务器。类似地,微服务场景下也分为两个阶段,而且第一阶段没什么变化,主要不同在于第二阶段:
此流程有两项重大变化:一是加入网关使得整个流程复杂了一些;二是网关以内使用 JWT 作为令牌。关于这两点的解读,本文不再赘述,感兴趣的同学可以参阅本人早前的文章《微服务架构下的统一身份认证和授权》。
值得注意的是,步骤 9-11,还有另一种处理方法,即将 scope 客户端权限检查放到网关进行:
客户端权限检查放到网关,则网关要维护 scope 和客户端权限的逻辑。
我们以 IBCS 图片分类系统为例,介绍客户端模式在微服务场景下的架构层次和主要流程。
可以看到,客户端模式流程比较简单,这里就不再叙述具体流程了,不过请注意第 2 步:
我们仍以 PAPS 相册预览系统为例,介绍授权码模式在微服务场景下的架构层次和主要流程。
整个流程分为两个阶段:
该阶段的流程,与密码模式的微服务场景流程一致,此处不在赘述。
如果 demo 应用是一个 REST 型应用,则在第 1、2 步骤中,还可以这么处理:
还可以这么处理:
至此,授权码模式的认证授权全流程完毕。
讨论:客户端第一次将用户导向 idp 提供的认证授权页面时,idp 是否需要验证客户端的身份呢?或者说需不需要提供 client_secret 呢?
在微服务场景下,大量的请求和响应都经过网关转发,我们设想一下,如果类似 PAPS 相册系统那样,返回的是图片数据,而且不采用 CDN 分发网络或文件存储服务等技术,那么图片流通过网关再返回给用户,网关的负载是不是将会非常庞大呢?
当然,网关本身可以做负载均衡,可以引入缓存,数据流可以做 CDN 处理等,这些都是非常好的高性能方案,除此之外,有没有其他办法呢?
这里引入一个网络技术领域的概念——负载均衡有三种模式:反向代理、透传模式和三角模式。这里简单介绍一下三角模式:
假设一种网络结构,包括 Web 服务器、PC 客户端和负载均衡器。PC 通过负载均衡器发起对 Web 服务器的访问。在三角模式下的访问流程如下:
PC、Web 服务器和负载均衡器三者呈三角形状,因此叫做三角模式。这个模式的优点就是负载均衡器只负责转发请求,而响应数据包则不需要接收和转发,从而大副降低负载均衡器自身的数据流通压力。三角模式也类似于 LVS 的 DR 模式,LVS 调度器只负责接收和转发请求,后端的集群服务器返回的真实数据包将直接发往请求的客户端。
按照这个思路,我们可以将三角模式引入网关,从而大副降低网关的负载压力。
我们设想一个场景,团队研发的平台同时包含了 IBCS 图片分类服务和 PAPS 相册预览服务,那么用户在登录平台(用密码模式认证授权)后,先访问“我的相册”,然后从中选择一张照片发起物品识别的请求。
根据文章的模式选型分析,IBCS 服务应采用客户端模式,PAPS 服务应采用密码模式,那么,客户端是否应该申请两套令牌?
回答这个问题,我们还是要从具体场景切入分析:
授权码模式是最严格的,密码模式次之,客户端模式最差,因此一般情况下,授权码模式的令牌可以给其他模式使用,密码模式令牌可以给客户端模式使用,客户端模式只能自己使用。
好了,从本节开始我们脱离枯燥的理论环节,进入一样枯燥的实战开发频道。
目前构建 OAuth2.0 授权系统有三种方式:一是基于主流的开源组件构建;二是接入第三方授权服务(如 Google、GitHub OAuth2.0);三是根据 OAuth2.0 的标准规范自行开发相关授权组件。
常用的开源组件有 RedHat Keycloak、Spring Security、Spring Security OAuth2,以及刚起步的 Spring Authorization Server 等。值得一提的是 RedHat Keycloak,它是一款开源、成熟的 IAM 解决方案,功能强大且可私有化部署。
在 Spring 的开发生态里,建议采用 Spring Security 方向的开源组件方案,可扩展性好,定制性强,用户广泛且社区活跃。
Spring Security OAuth2 目前已停止更新,官方不推荐继续使用。相关功能已经迁移到 Spring Security,但授权服务器(Authorization Server)功能并未包含。Authorization Server 功能将由 Spring Security 团队主导开发的 Spring Authorization Server 开源组件提供,详细信息请查看官方通告:https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server
Spring Authorization Server 项目目前还在迭代中,该项目的开发计划托管在 ZenHub 上,感兴趣的同学可以自行了解: https://app.zenhub.com/workspaces/authorization-server-5e8f3182b5e8f5841bfc4902/board?repos=248032165
核心组件,当前几乎所有 Spring OAuth2.0 开源技术方案都依赖于它。核心模块: spring-security-core。
org.springframework.security
spring-security-web
5.5.1
org.springframework.security
spring-security-config
5.5.1
依赖于 spring-security。该组件现已合并到 spring-security 中,官方已不建议使用。在此之前是 Spring Security 团队官方维护的唯一且被广泛使用的组件。
org.springframework.security.oauth
spring-security-oauth2
2.5.1.RELEASE
依赖于 spring-security。在 Spring Security 官方宣布停止 spring-security-oauth2 组件更新后,推出的授权服务器 Authorization Server 的替代组件。 spring-security 整合 spring-security-oauth2 后,并不包括 Authorization Server 功能,因此如果需要开发此功能,则要搭配 spring-security-oauth2-authorization-server 一起使用。
org.springframework.security.experimental
spring-security-oauth2-authorization-server
0.1.2
依赖于 spring-security。相当于 spring-boot + spring-security ,默认包含 SecurityAutoConfiguration.class ,因此会执行一些自动化配置,可以简化开发步骤。如果想关闭自动配置,可以修改 Spring Boot 启动注解为 @SpringBootApplication(exclude = { SecurityAutoConfiguration.class })
org.springframework.boot
spring-boot-starter-security
依赖于 spring-boot-starter-security + spring-security-oauth2,并额外提供了许多功能实现,在微服务场景下比较实用。
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR12
pom
import
org.springframework.cloud
spring-cloud-starter-oauth2
由于 spring-security-oauth2 已经迁移到 spring-security,而 spring-boot-starter-security 集成了 spring-security,且做了许多简化配置,特别适合于构建 spring-boot 程序。截至 2021年8月,spring-authorization-server 的最新版本是 0.1.2,最新的消息请关注官方动态 https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server
org.springframework.boot
spring-boot-starter-security
org.springframework.security.experimental
spring-security-oauth2-authorization-server
0.1.2
Spring Authorization Server 项目遵守 OAuth2.1 规范而不支持 password 模式,因此需要是用此模式的项目可以考虑备选方案。
https://app.zenhub.com/workspaces/authorization-server-5e8f3182b5e8f5841bfc4902/issues/spring-projects/spring-authorization-server/459
此方案采用了 spring-security-oauth2,已被 Sprnig Security 官方停止更新,因此不再推荐。另外请注意的 spring-security-oauth2 2.4.0.RELEASE 及之后的版本会提示 deprecated。
org.springframework.security.oauth
spring-security-oauth2
2.3.8.RELEASE
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.2.RELEASE