一 引言
再前面的security专题中 我们学习了单体架构基于SpringSecurity实现的授权方案,这种在业务量较小及业务的复杂度较低时比较实用,随着业务的复杂度越来越高,微服务架构也越来越被更多的公司使用,本文就微服务中的主流授权方案及oauth2中基本概念做简要概述。
二 常见的微服务授权方案
2.1 微服务授权存在的问题
在微服务架构下有很多的服务,每个微应用都需要对访问进行认证检查和权限控制,客户端发起一个请求需要考虑如何让用户的认证状态通知到所有的微服务中,尤其是请求来源于多种客户端如浏览器,移动端,三方程序,服务之间访问时,微服务的授权变得更加麻烦,再加上本地Session在微服务(集群/分布式)环境中存在
Session不同步的问题
,所以我们微服务授权是非常非常复杂的
2.2 微服务常见的认证方案
2.2.1 CAS单点登录
CAS是一种基于Cookie实现的单点登录方案,页是一个比较老的解决方案,是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,它分为 CAS Server端和CAS Client端,Server端负责用户的登录流程,Client端需要整合到各个系统中,它的登录流程如下:
系统A的访问流程:
- 浏览器请求系统A,系统A检查到没有登录,重定向到认证服务,并且携带参数service,这个参数的作用是在认证中心认证通过之后会再跳回系统A的重定向地址
- 请求跳转到认证中心,认证中心接收到请求,返回登录页面,用户提交登录信息
- 认证中心执行登录逻辑,认证成功,生成ticket(ST)和 TGC,认证中心会将TGC写到浏览器的cookie,有这个东西说明用户登录过,通过TGC可以找到TGT
- 然后认证中心将请求再重定向到系统A(根据之间的service参数回调地址),并且携带一个ticket参数
- 请求跳转回了系统A,系统A得到ticket,向认证中心发起请求校验ticket的合法性
- 认证中心验证ticket通过,向client返回对应的用户信息
系统B的访问流程
- 浏览器(同一个浏览器)访问另外一个客户端系统B,系统B检查到没有登录,请求重定向到认证中心
- 由于浏览器在访问系统A的时候已经到了登录,cookie中有TGC,所以认证中心可以获取到TGC找到对应的TGT
- 认证中心根据cookie中的TGC找到TGT,代表用户登录过,于是创建ticket,并重定向请求到系统B,带着参数ticket
- 系统B接收到请求,拿着ticket去认证中心校验Ticket
- 认证中心ticket校验通过,返回用户信息
这种方案意味着每个面向用户的服务都必须与认证服务交互
,这会产生大量非常琐碎的网络流量
和重复的工作
,当动辄数十个微应用时,这种方案的弊端会更加明显,而且这种方案不太适合移动端认证方案。
2.2.2 分布式Session(会话)+网关
这种方案是在网关做登录,以及登录检查,登录会话通过Redis进行分布式会话存储,后端微服务可以从共享会话Redis中获取认证信息,当然这里我们可以使用Redis的 key 来生成一个Token然后相应给客户端,客户端存储Token。当访问资源的时候携带者Token去后台,在网关层根据Token检查分布式会话是否完成登录。
- 客户端发起请求,网关实现登录逻辑,登录信息存储到Redis,并且根据Redis的key创建一个Token
- 将Token返回给浏览器,浏览器将Token存储起来
- 浏览器在访问资源的时候请求到网关并携带Token,网关获取到Token,从Redis中获取用户登录信息做检查,没有问题就继续转发Token到后续服务。
- 在后续服务中可以通过Token去Redis中获取认证信息。
2.2.3 客户端Token+网关
客户端Token是一种比较常用的认证方案,有点在于Token中携带了用户信息在服务之间传输,做到了无状态
,可以通过JWT
等安全机制加密Token保证Token的安全性
- 客户端发起认证请求,使用JWT等加密方式生成安全的 Token,Token中携带了认证授权信息,然后返回给客户端,
- 客户端存储Token,后续客户端需要访问资源时,携带者Token请求。
- 客户端发起请求,在网关层对Token进行统一检查,检查通过Token继续携带到后端访问中如果涉及到大量的用户信息的存放,可以使用Redis来进行存储。
- 后续服务获取到Token即可获取到用户信息
客户端Token方案的好处在于可以做到无状态,因为Token中包含了用户信息
,服务端不用考虑存储用户信息
,缺点在于Token过长造成的网络传输的开销
。
2.2.4 SpringCloud+Security+Oauth2(主流的授权微服务方案)
现在比较流程的微服务架构是SpringCloud的微服务架构,比较推荐的授权方案是SpringCloud+SpringSecurity+OAuth2+JWT+Gateway
,其实这是基于客户端Token加Gateway统一授权方案
后续我们采用的是在此基础上抽离一个鉴权组件,不在gateway做统一鉴权,这样做的好处是不用担心需要鉴权的服务被暴露
名词解释:
客户端
: web端,移动端,三方程序认证服务
:Oauth2授权服务:负责认证逻辑(登录)和颁发令牌(token)等网关
:Oauth2资源服务:负责token统一鉴权资源服务
:用户对资源的访问权限检查和返回资源
授权流程:
- 客户端向认证服务发起认证请求,认证服务执行认证流程
- 认证成功,认证服务根据用户认证信息,授权信息颁发Token,Token中包含了认证授权信息
- 客户端存储Token,并向资源服务发起请求,请求头携带Token
- 请求先到达Gateway网关,我们可以在网关层校验Token,当然也可以不在网关层校验Token,而是在每个资源服务器上校验Token
- Token校验通过,资源服务获取到Token中的权限信息对资源进行授权,授权成功返回资源数据,授权失败返回错误
- 如果请求需要多个资源服务共同完成,那么我们还需要考虑在服务调用的使用把
Token通过请求头传递给下一个被调用的微服务进行授权
三 Oauth2 基本概念
3.1 Oauth2概述
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的,oAuth是Open Authorization的简写,目前的版本是2.0版。
3.2 Oauth2的授权流程
为了方便理解,这里以Oauth2授权码模式为例分析一下Oauth2的授权流程
这里以云打印案例举例,张三想要通过云打印平台打印QQ空间的照片,传统的流程是张三把QQ账号交给云打印平台,云打印平台拿着张三的账号去QQ空间认证成功后获取照片进行照片的打印,不难看出这种方式会将张三的账号泄露给云打印平台,有安全隐患。
OAUTH2授权模式是不需要接触到张三的账号的,QQ空间只需要向张三发起授权请求,得到张三的授权许可后向云打印颁发授权码(授权码模式),云打印得到授权码可以根据授权码向QQ空间换取Token,云打印得到Token后又可以使用Token换取张三的照片资源
3.2 Oauth2四种授权模式
Oauth2的授权模式是客户端必须得到用户的授权(authorization grant)才能获得令牌(access token),通过令牌可以换取用户的资源,为此OAuth 2.0定义了四种授权方式
授权码模式
(authorization code)简化模式
(implicit)密码模式
(resource owner password credentials)客户端模式
(client credentials)
3.2.1 Oauth2相关概念
- Third-party application
第三方应用程序,本文中又称"客户端"(client),即栗子中的"云打印"。 - HTTP service
HTTP服务提供商,本文中简称"服务提供商",即上一节例子中的QQ。 - Resource Owner
资源所有者,本文中又称"用户"(user)。 - User Agent
用户代理,如浏览器,移动端等。 - Authorization server
认证服务器,即服务提供商专门用来处理认证的服务器QQ。 - Resource server
资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器QQ
3.2.2 授权码模式(authorization code)
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的认授权流程如下:
它的步骤如下
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
请求参数说明
A步骤中(获取授权码),客户端申请认证的URI,包含以下参数:
- response_type:表示授权类型,必选项,此处的值固定为"code"
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
例如:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
C步骤中(返回授权码),服务器回应客户端的URI,包含以下参数:
- code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
例如:
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
D步骤中(获取令牌),客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
- grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
- code:表示上一步获得的授权码,必选项。
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
- client_id:表示客户端ID,必选项。
例如:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
E步骤中,认证服务器发送的HTTP回复,包含以下参数:
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
例如:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存
3.2.3 简化模式(implicit)
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证
它的步骤如下
(A)客户端将用户导向认证服务器。
(B)用户决定是否给于客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
A步骤中,客户端发出的HTTP请求,包含以下参数:
- response_type:表示授权类型,此处的值固定为"token",必选项。
- client_id:表示客户端的ID,必选项。
- redirect_uri:表示重定向的URI,可选项。
- scope:表示权限范围,可选项。
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
获取令牌URL
例如:
GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
C步骤中,认证服务器回应客户端的URI,包含以下参数:
- access_token:表示访问令牌,必选项。
- token_type:表示令牌类型,该值大小写不敏感,必选项。
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
- scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
下面是一个例子。
HTTP/1.1 302 Found
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
&state=xyz&token_type=example&expires_in=3600
在上面的例子中,认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。
根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送。接下来的E步骤,服务提供商的资源服务器发送过来的代码,会提取出Hash中的令牌。
3.2.4 密码模式(resource owner password credentials)
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
它的步骤如下
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
B步骤中,客户端发出的HTTP请求,包含以下参数:
- grant_type:表示授权类型,此处的值固定为"password",必选项。
- username:表示用户名,必选项。
- password:表示用户的密码,必选项。
- scope:表示权限范围,可选项。
下面是一个例子。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=johndoe&password=A3ddj3w
整个过程中,客户端不得保存用户的密码。
3.2.5 .客户端模式(client credentials)
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
它的步骤如下:
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。
A步骤中,客户端发出的HTTP请求,包含以下参数:
- granttype:表示授权类型,此处的值固定为"clientcredentials",必选项。
- scope:表示权限范围,可选项
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
3.2.6 刷新令牌
如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。
客户端发出更新令牌的HTTP请求,包含以下参数:
- granttype:表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
- refresh_token:表示早前收到的更新令牌,必选项。
- scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。
例子:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA