第08课:Zuul

第08课:Zuul 整合 OAuth 2 实现鉴权

引言

前面几节课中,我分别为各位读者介绍了利用 Consul 实现服务注册与发现,利用 restTemplate、Feign 实现微服务之间调用,利用 Ribbon 实现微服务客户端负载均衡,利用 Hystrix 处理服务的熔断防止故障扩散、服务降级等。

到目前为止,初学者可能觉得自己已经能够结合公司自身业务开发微服务了,但我要告诉各位读者的是,这只是开发微服务前奏,要学习得东西还有很多,比如第三方系统如何访问内部各种各样的微服务。

在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个 API 代理组件(网关)根据请求的 URL 路由到相应的服务。当添加 API 网关后,在第三方应用服务提供方之间就创建了一面墙,这面墙直接对调用方通信进行权限控制,鉴权通过后将请求转发给后台微服务。

上面提到的 API 网关,当前业界中比较常用的组件是 Netflix 开源的 Zuul 组件,而在网关代理组件中通常用于实现授权、鉴权的组件为 OAuth2,下面将为各位读者分别介绍它们。

Zuul 网关

Zuul 是 Netfilx 开源的一个 API Gateway 服务器,本质是一个 Web Servlet 应用。其在微服务架构体系中提供动态路由、监控、弹性、安全等边缘服务。

使用 Zuul 作为网关,其主要原因有以下几点:

  1. Zuul、Ribbon 以及 Consul 客户端结合使用,能够轻松实现智能路由、负载均衡功能;
  2. 在网关层统一对外提供 API 接口,保护了实际提供接口的微服务实现细节,同时也方便测试人员对微服务接口进行测试;
  3. 在网关层能够统一添加身份认证、鉴权等功能,防止对微服务 API 接口的非法调用;
  4. 在网关层可以方便地对访问请求进行记录,实现监控相关功能;
  5. 在网关层实现流量监控,在流量比较大时,方便对服务实施降级。

在微服务架构中,网关的确非常重要,通过下图就能体现其重要性:

enter image description here

Zuul 工作原理

Zuul 的核心是一系列的 Filters,其作用可以类比 Servlet 框架的 Filter,或者 AOP。Zuul 中定义了四种标准过滤器类型,分别是 pre、post、routing 以及 error 过滤器。

  1. pre 过滤器:在请求路由到具体微服务之前执行,其主要用于身份验证、鉴权等功能;
  2. routing 过滤器:其主要功能是将请求路由到具体的微服务实例;
  3. post 过滤器:在对具体微服务调用之后执行,其主要用于收集统计信息、指标以及对请求响应数据进行处理等;
  4. error 过滤器:在以上三种过滤器执行出错时执行。

过滤器生命周期如下图所示,该图详细描述了各种类型过滤器的执行顺序:

enter image description here

Zuul 实践

在采用微服务架构开发项目过程中,通常采用的是前后端分离模式,可以理解为前后端分开部署,它们运行在不同进程中。

为了前端与后端之间能够更加方便地进行数据交互,能够统一的对后端 API 调用进行统一权限认证,往往会在前端与后端之间加一层 API 接口代理,即网关。

接下来,就为大家演示在实际工作中如何利用 Spring Cloud 集成 Zuul 实现网关功能。

1.新建订单微服务,在 pom 文件中添加主要依赖:

<dependency>
<!--spring mvc相关依赖>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--consul 客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

2.修改订单微服务 application.yml 配置文件:

server:
  ##服务暴露端口
  port: 8080
spring:
  cloud:
    consul:
      ##注册中心ip
      host: localhost
      ##注册中心监听端口
      port: 8500
      discovery:
        ##注册到注册中心
        register: true
        ##健康检查地址
        healthCheckPath: /health
        ##调用健康检查接口时间间隔
        healthCheckInterval: 2s
        ##微服务id
        instance-id: order-service

  application:
    ##应用名称
    name: order-service

注意,在以上配置信息中,healthCheckPath 对应接口是需要开发者自己开发的。健康检查项 healthCheckInterval 间隔建议不要配置太长,否则可能当服务进程不存在时还会被服务客户端程序调用,从而引起调用异常。

3.新增网关 apiGateway 项目,并在其 pom 文件中添加主要依赖:

<!--zuul 依赖>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<!--consul 客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

4.修改网关 apiGateway 项目配置文件 application.yml:

server:
  ##网关对外暴露端口
  port: 8081


spring:
  application:
    ##应用名称
    name: apiGateway
  cloud:
    consul:
      ##注册中心ip
      host: localhost
      ##注册中心监听端口
      port: 8500
      discovery:
        ##注册到注册中心
        register: true
        ##健康检查地址
        healthCheckPath: /health
        ##调用健康检查接口时间间隔
        healthCheckInterval: 2s
        ##微服务id
        instance-id: apiGateway

zuul:
  routes:
    order-api:
      ##请求url包含order-api路由到订单微服务
      path: /order-api/**
      serviceId: order-service

5.启动注册中心 Consul、订单服务 orderService、网关服务 apiGateway,如下图所示:

enter image description here

6.利用 Postman 调用接口:

http://localhost:8081/order-api/order/getOrderDetailInfoById?orderId=100

测试通过网关调用订单服务,如下图所示:

enter image description here

以上为各位读者介绍了利用 Zuul 实现网关的过程,希望大家结合源码加以理解。

接下来,为各位读者介绍 OAuth2 相关内容。

OAuth2

OAuth2 是一个安全相关的协议,作用在于使用户授权第三方的应用程序访问用户的 Web 资源,并且不需要向第三方应用程序透露自己的密码。其主要场景有服务器端 WebApp、浏览器单页应用、移动应用、原生 App 及服务器对服务器的调用等。

它具有以下特征:

  1. 主要用于 Rest APIs 的代理授权框架;
  2. 基于令牌(Token)进行授权;
  3. 将认证和授权进行解耦。

OAuth2 主要包含角色

OAuth 2 标准中定义了以下四种角色:

  1. 资源拥有者:这个很好理解,就是资源的所有者;
  2. 资源服务器:是一个 Web 站点,用于保存用户数据;
  3. 客户端应用:通常指 Web 应用或者移动应用;
  4. 授权服务器:对客户端应用认证成功后,对其颁发用于授权的令牌。

以上四种角色之间数据交互流程,简要表述如下:

enter image description here

OAuth2 典型模式

在 OAuth2 协议中规定客户端必须得到用户的授权(Authorization Grant),才能获得令牌(Access Token)。OAuth 2.0 定义了四种授权方式。

授权码模式

授权码模式(Authorization Code)是功能最完整、流程最严密的授权模式。其主要特点就是通过客户端的后台服务器,与“服务提供商”的认证服务器进行互动。

enter image description here

其授权、认证步骤如下:

  1. 用户访问客户端,后者将前者导向认证服务器;
  2. 用户选择是否给予客户端授权;
  3. 假设用户给予授权,认证服务器将用户导向客户端事先指定的“重定向 URI”(Redirection URI),同时附上一个授权码;
  4. 客户端收到授权码,附上早先的“重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见;
  5. 认证服务器核对授权码和重定向 URI,确认无误后,向客户端发送访问令牌(Access Token)和更新令牌(Refresh Token)。
简化模式

简化模式(Implicit Grant Type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了“授权码”这个步骤。所有步骤均在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

enter image description here

其授权、认证步骤如下:

  1. 客户端将用户导向认证服务器;
  2. 用户决定是否给予客户端授权;
  3. 假设用户给予授权,认证服务器将用户导向客户端指定的“重定向URI”,并在 URI 的 Hash 部分包含了访问令牌;
  4. 浏览器向资源服务器发出请求,其中不包括上一步收到的 Hash 值;
  5. 资源服务器返回一个网页,其中包含的代码可以获取 Hash 值中的令牌;
  6. 浏览器执行上一步获得的脚本,提取出令牌;
  7. 浏览器将令牌发给客户端。
密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码,客户端使用这些信息,向“服务商提供商”索要授权。

enter image description here

其授权、认证步骤如下:

  1. 用户向客户端提供用户名和密码;
  2. 客户端将用户名和密码发给认证服务器,向后者请求令牌;
  3. 认证服务器确认无误后,向客户端提供访问令牌。
客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向“服务提供商”进行认证。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求“服务提供商”提供服务,其实不存在授权问题。

enter image description here

其授权、认证步骤如下:

  1. 客户端向认证服务器进行身份认证,并要求一个访问令牌;
  2. 认证服务器确认无误后,向客户端提供访问令牌。

基于 OAuth2“授权码模式”实现授权、认证功能

实际工作中,利用 OAuth2“授权码模式”实现授权、认证场景比较多,接下来,我将为各位读者演示如何利用 OAuth2 实现授权、认证过程。

1.新建项目 authCode,然后在其 pom 文件中添加如下主要依赖:

<!-- spring security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--OAuth2 依赖-->
<dependency>            <groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>

2.在配置文件 application.yml 文件中添加如下配置:

server:
    ##服务监听端口
    port: 8080

security:
  user:
    ##资源拥有者用户名 
    name: hcb
    ##资源拥有者密码
    password: hcb

注意,配置文件中 name 表示资源拥有者用户名,password 表示资源拥有者密码。资源拥有者授权时输入用户名、密码需要和此处配置的信息一致才能对资源进行授权。

3.添加授权服务功能:

//配置授权服务器
@Configuration
@EnableAuthorizationServer
public class OAuthAuthServer extends
        AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()
            //设置客户端名称
            .withClient("clientapp")
            //设置客户端密码
            .secret("123456")
            //设置返回令牌回调地址
            .redirectUris("http://localhost:8081/callback")
            //授权码模式
            .authorizedGrantTypes("authorization_code")
            //设置资源拥有者授权范围
            .scopes("read_userinfo", "read_contacts");
    }
}

注意,此处为了向各位读者演示如何利用 OAuth2 实现授权功能,便将客户端相关信息存储在了内存中。在实际工作中此处配置的应用客户端信息通常存储在数据库中,而不能像上面这样配置。

4.添加资源服务器功能:

//资源服务配置
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest()
            .authenticated()
            .and()
            .requestMatchers()
            //设置对如下url地址需要进行鉴权
            .antMatchers("/api/**");
    }
}

5.新增一个接口作为资源模拟鉴权过程:

@Controller
public class UserController {
    //资源API
    @RequestMapping("/api/userinfo")
    public ResponseEntity<UserInfo> getUserInfo() {
        //从请求上下文中获取用户信息
        User user = (User) SecurityContextHolder.getContext()
                .getAuthentication().getPrincipal();
        String email = user.getUsername() + "@zte.com.cn";

        UserInfo userInfo = new UserInfo();
        userInfo.setName(user.getUsername());
        userInfo.setEmail(email);

        return ResponseEntity.ok(userInfo);
    }
}

6.启动程序,然后在浏览器中输入请求:

http://localhost:8080/oauth/authorize?clientid=clientapp&redirecturl=http://localhost:8081/callback&responsetype=code&scope=readuserinfo

得到如下页面:

enter image description here

接着输入资源拥有者的用户名、密码,此处输入的用户名、密码需要与 application.yml 配置文件中配置的用户名、密码一致:

enter image description here

点击登录,进入资源拥有者授权选择页面:

enter image description here

资源拥有者选择同意(Approve)授权,获得授权码“1wuiuB”:

enter image description here

7.利用 Postman 发送请求获取令牌 Token:

enter image description here

各位读者需要注意以下几点:

  1. 请求方式为“post”;
  2. Authorization Type 选择“Basic Auth”;
  3. Headers 中输入一个参数 Content-Type,其值为 application/x-www-form-urlencoded
  4. Params 输入参数“code”,其值为授权码“3enUpO”;
  5. 参数 grant_type,其值为 authorization_code
  6. 参数 redirect_uri,其值为 http://localhost:8081/callback
  7. 参数“scope”,其值为 read_userinfo

8.利用第7步骤中生成的 Token 获取用户信息:

enter image description here

在本次请求中需要注意以下几点:

  1. 请求方式是“get”请求;
  2. Authorization Type 为“Bear Token”,其值为第7步中生成的 Token 值 6038af35-94b4-4dd2-9b57-712e356a04b7
  3. 请求地址为:http://localhost:8080/api/userinfo

结语

本文主要为读者介绍了 Zuul、OAuth2 相关知识。本课实战阶段还将为大家进一步介绍实际工作中如何对 Zuul 与 OAuth2 进行整合。

参考资料

下面给出了达人课源码 Github 地址,需要的读者请自行下载:

Github

上一篇
下一篇
目录