如何在微服务架构中实现统一认证与授权

对于一个服务系统,安全是必须需要考虑的方面。应用安全是一个不断追求更强的目标,全面性、全系统的方法很重要,因为我们永远不知道入侵者是如果对系统进行攻击。在系统安全的实现上,一般倡导使用安全层 layers of security
,即多层次安全保证,通过连续层提供额外的安全性。每一层的安全性越强,应用程序的健壮性和安全性就越高。Java EE应用程序位于安全层的高层次,需要为它添加特定的问题域安全配置。
对应用的层次的安全主要关注两个方面,认证(Authentication)和授权(Authoriztion),即你是谁,以及你能做什么。在单体应用中,开发者可以通过简单的拦截器以及session机制对用户的访问进行控制和记录。在分布式系统中,由于业务逻辑封装在各个微服务中,每个微服务都需要对用户的行为进行认证和许可,于是就产生了两种可能的方式:第一种是通过一个中心化的权限管理系统,对用户的身份和权限进行统一的管理,可以做到一次授权,多次多点使用,但是这个独立的安全微服务需要聚合各个微服务中的权限控制逻辑,多一个基于不同业务逻辑实现的微服务可能需要在安全微服务中添加新的实现;第二种是将安全部分分散到各个微服务中,由各个微服务根据自身的业务对用户的访问进行管理和控制,这会导致安全管理过于分散,甚至每个微服务都有自己的一套实现方式,不利于统一管理。这两种方式都有利有弊,如何选择需要根据项目的具体需求进行宏观判断,甚至在一定情况下可以结合使用。
spring-cloud-security提供了一组基本的组件用来构建安全应用程序和服务。它封装了spring-securtiy和spring
-security-oauth2以及spring-securtiy-jwt的相关实现,同时提供自带的安全特性,致力于为spring-cloud中微服务提供快速创建常用的安全模式。
虽然spring-cloud-security文档中对其使用提供的帮助不多,但是通过对spring-security与spring
-security-oauth2的相关使用以及探索,将有助开发者通过spring-cloud-security构建高健壮的安全应用。
在开始对相关的应用以及源码的介绍之前,需要对部分前置知识进行一定的补充,如OAuth2以及JWT。

OAuth2 简介

OAuth2相关理论的介绍主要来自于OAuth2官方文档,相关地址为 https://tools.ietf.org/html/rfc6749

OAuth协议的目的是为了为用户资源的授权提供一个安全的、开放而简易的标准。官网中是这样进行介绍的:
An open protocol to allow secure API authorization in a simple and standard method from web, mobile and desktop applications.
OAuth1由于不被OAuth2兼容,且签名逻辑过于复杂和授权流程的过于单一,在此不过多谈论,我们重点关注OAuth2,现在Web应用的中主流授权版本也是OAuth2
OAuth2是当前授权的行业标准,其重点在于为Web应用程序、桌面应用程序、移动设备以及室内设备的授权流程提供简单的客户端开发方式。它为第三方应用提供对HTTP服务的有限访问,既可以是资源拥有者通过授权允许第三方应用获取HTTP服务,也可以是第三方以自己的名义获取访问权限。

角色

OAuth2 中主要分为了4种角色:

  • resource owner 资源所有者,是能够对受保护的资源授予访问权限的实体,可以是一个用户,这时会被称为end-user。
  • resource server 资源服务器,持有受保护的资源,允许持有访问令牌(access token)的请求访问受保护资源。
  • client 客户端,持有资源所有者的授权,代表资源所有者对受保护资源进行访问。
  • authorization server 授权服务器,对资源所有者的授权进行认证,成功后向客户端发送访问令牌。

在很多时候,资源服务器和授权服务器是合二为一的,在授权交互的时候是授权服务器,在请求资源交互是资源服务器。但是授权服务器是单独的实体,它可以发出被多个资源服务器接受的访问令牌。

协议流程

首先看一张来自官方提供的流程图:

+--------+                               +---------------+

 |        |--(1)- Authorization Request ->|   Resource    |

 |        |                               |     Owner     |

 |        |<-(2)-- Authorization Grant ---|               |

 |        |                               +---------------+

 |        |

 |        |                               +---------------+

 |        |--(3)-- Authorization Grant -->| Authorization |

 | Client |                               |     Server    |

 |        |<-(4)----- Access Token -------|               |

 |        |                               +---------------+

 |        |

 |        |                               +---------------+

 |        |--(5)----- Access Token ------>|    Resource   |

 |        |                               |     Server    |

 |        |<-(6)--- Protected Resource ---|               |

 +--------+                               +---------------+

这是一张关于OAuth2角色的抽象交互流程图,主要包含以下的6个步骤:

  1. 客户端请求资源所有者的授权;
  2. 资源所有者同意授权,返回授权许可(Authorization Grant),这代表了资源所有者的授权凭证;
  3. 客户端携带授权许可要求授权服务器进行认证,请求访问令牌;
  4. 授权服务器对客户端进行身份验证,并认证授权许可,如果有效,返回访问令牌;
  5. 客户端携带访问许可向资源服务器请求受保护资源的访问;
  6. 资源服务器验证访问令牌,如果有效,接受访问请求,返回受保护资源。

客户端授权类型

为了获取访问令牌,客户端必须获取到资源所有者的授权许可。OAuth2默认定了四种授权类型,当然也提供了用于定义额外的授权类型的扩展机制。默认的四种授权类型为:

  • authorization code 授权码类型
  • implicit 简化类型(也称为隐式类型)
  • resource owner password credentials 密码类型
  • client credential 客户端类型

下面对常用的授权码类型和密码类型进行详细的介绍。

授权码类型
授权码类型(authorization code)通过重定向的方式让资源所有者直接与授权服务器进行交互来进行授权,避免了资源所有者信息泄漏给客户端,是功能最完整、流程最严密的授权类型,但是需要客户端必须能与资源所有者的代理(通常是Web浏览器)进行交互,和可从授权服务器中接受请求(重定向给予授权码),授权流程如下:

+----------+

 | Resource |

 |   Owner  |

 |          |

 +----------+

      ^

      |

     (2)

 +----|-----+          Client Identifier      +---------------+

 |         -+----(1)-- & Redirection URI ---->|               |

 |  User-   |                                 | Authorization |

 |  Agent  -+----(2)-- User authenticates --->|     Server    |

 |          |                                 |               |

 |         -+----(3)-- Authorization Code ---<|               |

 +-|----|---+                                 +---------------+

   |    |                                         ^      v

  (1)  (3)                                        |      |

   |    |                                         |      |

   ^    v                                         |      |

 +---------+                                      |      |

 |         |>---(4)-- Authorization Code ---------'      |

 |  Client |          & Redirection URI                  |

 |         |                                             |

 |         |<---(5)----- Access Token -------------------'

 +---------+       (w/ Optional Refresh Token)
  1. 客户端引导资源所有者的用户代理到授权服务器的endpoint,一般通过重定向的方式。客户端提交的信息应包含客户端标识(client identifier)、请求范围(requested scope)、本地状态(local state)和用于返回授权码的重定向地址(redirection URI);
  2. 授权服务器认证资源所有者(通过用户代理),并确认资源所有者允许还是拒绝客户端的访问请求;
  3. 如果资源所有者授予客户端访问权限,授权服务器通过重定向用户代理的方式回调客户端提供的重定向地址,并在重定向地址中添加授权码和客户端先前提供的任何本地状态;
  4. 客户端携带上一步获得的授权码向授权服务器请求访问令牌。在这一步中授权码和客户端都要被授权服务器进行认证。客户端需要提交用于获取授权码的重定向地址;
  5. 授权服务器对客户端进行身份验证,和认证授权码,确保接收到的重定向地址与第三步中用于的获取授权码的重定向地址相匹配。如果有效,返回访问令牌,以及可能返回的刷新令牌(Refresh Token)。

密码类型
密码类型(resource owner password credentials)需要资源所有者将密码凭证交予客户端,客户端通过自己持有的信息直接向授权服务器获取授权。在这种情况下,需要资源所有者对客户端高度可信任,同时客户端不允许保存密码凭证。这种授权类型适用于能够获取资源所有者的凭证(credentials)(如用户名和密码)的客户端。授权流程如下:

+----------+

 | Resource |

 |  Owner   |

 |          |

 +----------+

      v

      |    Resource Owner

     (1) Password Credentials

      |

      v

 +---------+                                  +---------------+

 |         |>--(2)---- Resource Owner ------->|               |

 |         |         Password Credentials     | Authorization |

 | Client  |                                  |     Server    |

 |         |<--(3)---- Access Token ---------<|               |

 |         |    (w/ Optional Refresh Token)   |               |

 +---------+                                  +---------------+
  1. 资源所有者向客户端提供其用户名和密码等凭证;
  2. 客户端携带资源所有者的凭证(用户名和密码),向授权服务器请求访问令牌;
  3. 授权服务器认证客户端并且验证资源所有者的凭证,如果有效,返回访问令牌,以及可能返回的刷新令牌(Refresh Token)。

令牌刷新

客户端从授权服务器中获取的访问令牌(access token)一般是具备失效性的,在访问令牌过期的情况下,持有有效用户凭证的客户端可以再次向授权服务器请求访问令牌,但是如果不持有用户凭证的客户端可以通过和上次访问令牌一同返回的刷新令牌(refresh token)向授权服务器获取新的访问令牌。

JWT

JWT,JSON Web Token,作为一个开放的标准,通过紧凑(compact,快速传输,体积小)或者自包含(self-contained,payload中将包含用户所需的所有的信息,避免了对数据库的多次查询)的方式,定义了用于在各方之间发送的安全JSON对象。
为什么要介绍JWT,因为JWT可以很好的充当在上一节介绍的访问令牌(access token)和刷新令牌(refresh token)的载体,这是Web双方之间进行安全传输信息的良好方式。当只有授权服务器持有签发和验证JWT的secret,那么就只有授权服务器能验证JWT的有效性以及发送带有签名的JWT,这就唯一保证了以JWT为载体的token的有效性和安全性。

JWT的组成

JWT格式一般如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2FuZyB3dSIsImV4cCI6MTUxODA1MTE1NywidXNlcklkIjoiMTIzNDU2In0.IV4XZ0y0nMpmMX9orv0gqsEMOxXXNQOE680CKkkPQcs

它由三部分组成,每部分通过 .
分隔开,分别是:

  • Header 头部
  • Payload 有效负荷
  • Signature 签名

接着我们对每一部分进行详细的介绍。

Header
头部通常由两部分组成:

  • typ 类型,一般为jwt。
  • alg 加密算法,通常是HMAC SHA256或者RSA。

一个简单的头部例子如下:

{

"alg": "HS256"

"typ": "JWT"

}

然后这部分JSON会被Base64Url编码用于构成JWT的第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Playload
有效负载是JWT的第二部分,是用来携带有效信息的载体,主要是关于用户实体和附加元数据的声明,由以下三部分组成:

  • Registered claims 注册声明,这是一组预定的声明,但并不强制要求,提供了一套有用的、能共同使用的声明。主要有iss(JWT签发者),exp(JWT过期时间),sub(JWT面向的用户),aud(接受JWT的一方)等。
  • Public claims 公开声明 公开声明中可以添加任何信息,一般是用户信息或者业务扩展信息等。
  • Private claims 私有声明 被JWT提供者和消费者共同定义的声明,既不属于注册声明也不属于公开声明。

一般不建议在payload中添加任何的敏感信息,因为Base64是对称解密的,这意味着payload中的信息的是可见的。
以上内容摘自《Spring Cloud微服务架构进阶》一书,经出版方授权发布。