Spring微服务实战第7章 保护微服务

文章目录

  • 第7章 保护微服务
    • 7.1 OAuth2简介
    • 7.2 从小事做起:使用Spring和OAuth2来保护单个端点
      • 7.2.1 建立EagleEye OAuth2验证服务
      • 7.2.2 使用OAuth2服务注册客户端应用程序
      • 7.2.3 配置EagleEye用户
      • 7.2.4 验证用户
    • 7.3 使用OAuth2保护组织服务
      • 7.3.1 将Spring Security和OAuth2 jar添加到各个服务
      • 7.3.2 配置服务以指向OAuth2验证服务
      • 7.3.3 定义谁可以访问服务
        • 1.通过验证用户保护服务
        • 2.通过特定角色保护服务
      • 7.3.4 传播OAuth2访问令牌
    • 7.4 JSON Web Token与OAuth2
      • 7.4.1 修改验证服务以颁发JWT令牌
      • 7.4.2 在微服务中使用JWT
      • 7.4.3 扩展JWT令牌
      • 7.4.4 从JWT令牌中解析自定义字段
    • 7.5 关于微服务安全的总结
        • 1.为所有业务通信使用HTTPS/安全套接字层(SSL)
        • 2.使用服务网关访问微服务
        • 3.将服务划分到公共API和私有API
        • 4.通过封锁不需要的网络端口来限制微服务的攻击面
    • 7.6 小结
    • 源码地址
        • 4.通过封锁不需要的网络端口来限制微服务的攻击面
    • 7.6 小结
    • 源码地址

第7章 保护微服务

本章主要内容

  • 了解安全在微服务环境中的重要性
  • 认识OAuth2标准
  • 建立和配置基于Spring的OAuth2服务
  • 使用OAuth2执行用户验证和授权
  • 使用OAuth2保护Spring微服务
  • 在服务之间传播OAuth2访问令牌

提到“安全”这个词往往会引起开发人员不由自主地痛苦沉吟。你会听到他们咕哝着低声诅咒:“它迟钝,难以理解,甚至是很难调试。”然而,没有任何开发人员(除了那些没有经验的开发人员)会说他们不担心安全问题。

一个安全的应用程序涉及多层保护,包括:

  • 确保有正确的用户控制,以便可以确认用户是他们所说的人,并且他们有权执行正在尝试执行的操作;
  • 保持运行服务的基础设施是打过补丁且最新的,以让漏洞的风险最低;
  • 实现网络访问控制,让少量已授权的服务器能够访问服务,并使服务只能通过定义良好的端口进行访问。

本章只讨论上述列表中的第一个要点:如何验证调用微服务的用户是他们所说的人,并确定他们是否被授权执行他们从微服务中请求的操作。另外两个主题是非常宽泛的安全主题,超出了本书的范围。

要实现验证和授权控制,我们将使用Spring Cloud Security和OAuth2(Open Authentication)标准来保护基于Spring的服务。OAuth2是一个基于令牌的安全框架,允许用户使用第三方验证服务进行验证。如果用户成功进行了验证,则会出示一个令牌,该令牌必须与每个请求一起发送。然后,验证服务可以对令牌进行确认。OAuth2背后的主要目标是,在调用多个服务来完成用户请求时,用户不需要在处理请求的时候为每个服务都提供自己的凭据信息就能完成验证。Spring Boot和Spring Cloud都提供了开箱即用的OAuth2服务实现,使OAuth2安全能够非常容易地集成到服务中。

注意

本章将介绍如何使用OAuth2保护微服务。不过,一个成熟的OAuth2实现还需要一个前端Web应用程序来输入用户凭据。本章不会讨论如何建立前端应用程序,因为这已经超出了本书关于微服务的范围。作为代替,本章将使用REST客户端(如POSTMAN)来模拟凭据的提交。有关如何配置前端应用程序,我建议读者查看以下Spring教程:https://spring.io/blog/2015/02/03/sso- with-oauth2-angular-js-and-spring-security-part-v。

OAuth2背后真正的强大之处在于,它允许应用程序开发人员轻松地与第三方云服务提供商集成,并使用这些服务进行用户验证和授权,而无须不断地将用户的凭据传递给第三方服务。像Facebook、GitHub和Salesforce这样的云服务提供商都支持将OAuth2作为标准。

在讨论使用OAuth2保护服务的技术细节之前,让我们先看看OAuth2架构。

7.1 OAuth2简介

OAuth2是一个基于令牌的安全验证和授权框架,它将安全性分解为以下4个组成部分。

(1)受保护资源——这是开发人员想要保护的资源(在我们的例子中是一个微服务),需要确保只有已通过验证并且具有适当授权的用户才能访问它。

(2)资源所有者——资源所有者定义哪些应用程序可以调用其服务,哪些用户可以访问该服务,以及他们可以使用该服务完成哪些事情。资源所有者注册的每个应用程序都将获得一个应用程序名称,该应用程序名称与应用程序密钥一起标识应用程序。应用程序名称和密钥的组合是在验证OAuth2令牌时传递的凭据的一部分。

(3)应用程序——这是代表用户调用服务的应用程序。毕竟,用户很少直接调用服务。相反,他们依赖应用程序为他们工作。

(4)OAuth2验证服务器——OAuth2验证服务器是应用程序和正在使用的服务之间的中间人。OAuth2验证服务器允许用户对自己进行验证,而不必将用户凭据传递给由应用程序代表用户调用的每个服务。

这4个组成部分互相作用对用户进行验证。用户只需提交他们的凭据。如果他们成功通过验证,则会出示一个验证令牌,该令牌可在服务之间传递,如图7-1所示。OAuth2是一个基于令牌的安全框架。针对OAuth2服务器,用户通过提供凭据以及用于访问资源的应用程序来进行验证。如果用户凭据是有效的,那么OAuth2服务器就会提供一个令牌,每当用户的应用程序使用的服务试图访问受保护的资源(微服务)时,就可以提交这个令牌。

Spring微服务实战第7章 保护微服务_第1张图片

图7-1 OAuth2允许用户进行验证,而不必持续提供凭据

接下来,受保护资源可以联系OAuth2服务器以确定令牌的有效性,并检索用户授予它们的角色。角色用于将相关用户分组在一起,并定义用户组可以访问哪些资源。对于本章来说,我们将使用OAuth2和角色来定义用户可以调用哪些服务端点,以及用户可以在端点上调用的HTTP动词。

Web服务安全是一个极其复杂的主题。开发人员必须了解谁将调用自己的服务(公司网络的内部用户还是外部用户),他们将如何调用这些服务(是在内部基于Web客户端、移动设备还是在企业网络之外的Web应用程序),以及他们用代码来完成什么操作。OAuth2允许开发人员使用称为授权(grant)的不同验证方案,在不同的场景中保护基于REST的服务。OAuth2规范具有以下4种类型的授权:

  • 密码(password);
  • 客户端凭据(client credential);
  • 授权码(authorization code);
  • 隐式(implicit)。

本书不会逐一介绍每种授权类型,或者为每种授权类型提供代码示例。究其原因,仅仅是因为需要包含在一章里的内容太多了。取而代之,本章将会完成以下事情:

  • 讨论微服务如何通过一个较简单的OAuth2授权类型(密码授权类型)来使用OAuth2;
  • 使用JSON Web Token来提供一个更健壮的OAuth2解决方案,并在OAuth2令牌中建立一套信息编码的标准;
  • 介绍在构建微服务时需要考虑的其他安全注意事项。

本书在附录B中会提供其他OAuth2授权类型的概述资料。如果读者有兴趣详细了解OAuth2规范以及如何实现所有授权类型,强烈推荐Justin Richer和Antonio Sanso的著作《OAuth2 in Action》,这是对OAuth2的全面解读。

7.2 从小事做起:使用Spring和OAuth2来保护单个端点

为了了解如何建立OAuth2的验证和授权功能,我们将实现OAuth2密码授权类型。要实现这一授权,我们将执行以下操作。

  • 建立一个基于Spring Cloud的OAuth2验证服务。
  • 注册一个伪EagleEye UI应用程序作为一个已授权的应用程序,它可以通过OAuth2服务验证和授权用户身份。
  • 使用OAuth2密码授权来保护EagleEye服务。我们不会为EagleEye构建UI,而是使用POSTMAN模拟登录的用户对EagleEye OAuth2服务进行验证。
  • 保护许可证服务和组织服务,使它们只能被已通过验证的用户调用。

7.2.1 建立EagleEye OAuth2验证服务

就像本书中所有的例子一样,OAuth2验证服务将是另一个Spring Boot服务。验证服务将验证用户凭据并颁发令牌。每当用户尝试访问由验证服务保护的服务时,验证服务将确认OAuth2令牌是否已由其颁发并且尚未过期。这里的验证服务等同于图7-1中的验证服务。

开始时,需要完成以下两件事。

(1)添加引导类所需的适当Maven构建依赖项。

(2)添加一个将作为服务的入口点的引导类。

读者可以在authentication-service目录中找到验证服务的所有代码示例。要建立OAuth2验证服务器,需要在authentication-service/pom.xml文件中添加以下Spring Cloud依赖项:

<dependency>
  <groupId>org.springframework.cloudgroupId>
  <artifactId>spring-cloud-securityartifactId>
dependency>

<dependency>
  <groupId>org.springframework.security.oauthgroupId>
  <artifactId>spring-security-oauth2artifactId>
dependency>

第一个依赖项spring-cloud-security引入了通用Spring和Spring Cloud安全库。第二个依赖项spring-security-oauth2拉取了Spring OAuth2库。

既然已经定义完Maven依赖项,那么就可以在引导类上进行工作。这个引导类可以在authentication-service/src/main/java/com/thoughtmechanix/authentication/Application.java中找到。代码清单7-1展示Application类的代码。

代码清单7-1 authentication-service的引导类

// 为了简洁,省略了import语句
@SpringBootApplication
@RestController
@EnableResourceServer
@EnableAuthorizationServer  ⇽--- 用于告诉Spring Cloud,该服务将作为OAuth2服务
public class Application {

    @RequestMapping(value = { "/user" }, produces = "application/json")  ⇽--- 在本章稍后用于检索有关用户的信息
    public Map<String, Object> user(OAuth2Authentication user) {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("user",
        ➥  user.getUserAuthentication().getPrincipal());
        userInfo.put("authorities",AuthorityUtils.authorityListToSet(
            ➥  user.getUserAuthentication().getAuthorities()));
        return userInfo;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

在代码清单7-1中,要注意的第一样东西是@EnableAuthorizationServer注解。这个注解告诉Spring Cloud,该服务将用作OAuth2服务,并添加几个基于REST的端点,这些端点将在OAuth2验证和授权过程中使用。

在代码清单7-1中,看到的第二件事是添加了一个名为/user(映射到/auth/user)的端点。当试图访问由OAuth2保护的服务时,将会用到这个端点,本章后文会进行介绍。此端点由受保护服务调用,以确认OAuth2访问令牌,并检索访问受保护服务的用户所分配的角色。本章稍后会详细讨论这个端点。

7.2.2 使用OAuth2服务注册客户端应用程序

此时,我们已经有了一个验证服务,但尚未在验证服务器中定义任何应用程序、用户或角色。我们可以从已通过验证服务注册EagleEye应用程序开始。为此,我们将在验证服务中创建一个名为OAuth2Config的类(在authentication-service/src/main/java/com/thoughtmechanix/authentication/ security/OAuth2Config.java中)。

这个类将定义通过OAuth2验证服务注册哪些应用程序。需要注意的是,不能只因为应用程序通过OAuth2服务中注册过,就认为该服务能够访问任何受保护资源。

验证与授权

我经常发现开发人员混淆术语验证(authentication)和授权(authorization)的含义。验证是用户通过提供凭据来证明他们是谁的行为。授权决定是否允许用户做他们想做的事情。例如,Jim可以通过提供用户ID和密码来证明他的身份,但是他可能没有被授权查看敏感数据,如工资单数据。出于我们讨论的目的,必须在授权发生之前对用户进行验证。

OAuth2Config类定义了OAuth2服务知道的应用程序和用户凭据。在代码清单7-2中可以看到OAuth2Config类的代码。

代码清单7-2 OAuth2Config服务定义哪些应用程序可以使用服务

// 为了简洁,省略了import语句
@Configuration  ⇽--- 继承AuthorizationServer- ConfigurerAdapter类,并使用@Configuration注解标注这个类
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override  ⇽--- 覆盖configure()方法。这定义了哪些客户端将注册到服务
    public void configure(ClientDetailsServiceConfigurer clients) throwsException {
        clients.inMemory()
             .withClient("eagleeye")
             .secret("thisissecret")
             .authorizedGrantTypes("refresh_token","password","client_credentials")
             .scopes("webclient","mobileclient");
    }

    @Override  ⇽--- 该方法定义了AuthenticationServerConfigurer中使用的不同组件。这段代码告诉Spring使用Spring提供的默认验证管理器和用户详细信息服务
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)throws Exception {
        endpoints
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService);
      }
}

在代码清单7-2所示的代码中,要注意的第一件事是,这个类扩展了Spring的AuthenticationServerConfigurer类,然后使用@Configuration注解对这个类进行了标记。AuthenticationServerConfigurer类是Spring Security的核心部分,它提供了执行关键验证和授权功能的基本机制。对于OAuth2Config类,我们将要覆盖两个方法。第一个方法是configure(),它用于定义通过验证服务注册了哪些客户端应用程序。configure()方法接受一个名为clientsClientDetailsServiceConfigurer类型的参数。让我们来更详细地了解一下configure()方法中的代码。在这个方法中做的第一件事是注册哪些客户端应用程序允许访问由OAuth2服务保护的服务。这里使用了最广泛的术语“访问”(access),因为我们通过检查调用服务的用户是否有权采取他们正在尝试的操作,控制了客户端应用程序的用户以后可以做什么。

clients.inMemory()
    .withClient("eagleeye")
    .secret("thisissecret")
    .authorizedGrantTypes("password","client_credentials")
    .scopes("webclient","mobileclient");

对于应用程序的信息,ClientDetailsServiceConfigurer类支持两种不同类型的存储:内存存储和JDBC存储。对本例来说,我们将使用clients.inMemory()存储。

withClient()secret()这两个方法提供了注册的应用程序的名称(eagleeye)以及密钥(一个密码,thisissecret),该密钥在EagleEye应用程序调用OAuth2服务器以接收OAuth2访问令牌时提供。

下一个方法是authorizedGrantTypes(),它被传入一个以逗号分隔的授权类型列表,这些授权类型将由OAuth2服务支持。在这个服务中,我们将支持密码授权类型和客户端凭据授权类型。

scopes()方法用于定义调用应用程序在请求OAuth2服务器获取访问令牌时可以操作的范围。例如,ThoughtMechanix可能提供同一应用程序的两个不同版本:基于Web的应用程序和基于手机的应用程序。在这些应用程序中都可以使用相同的客户端名称和密钥来请求对OAuth2服务器保护的资源的访问。然而,当应用程序请求一个密钥时,它们需要定义它们所操作的特定作用域。通过定义作用域,可以编写特定于客户端应用程序所工作的作用域的授权规则。

例如,可能有一个用户使用基于Web的客户端和手机应用程序来访问EagleEye应用程序。EagleEye应用程序的每个版本都:

(1)提供相同的功能;

(2)是一个“受信任的应用程序”,ThoughtMechanix既拥有前端应用程序,也拥有终端用户服务。

因此,我们将使用相同的应用程序名称和密钥来注册EagleEye应用程序,但是Web应用程序只使用“webclient”作用域,而手机版本的应用程序则使用“mobileclient”作用域。通过使用作用域,可以在受保护的服务中定义授权规则,该规则可以根据登录的应用程序限制客户端应用程序可以执行的操作。这与用户拥有的权限无关。例如,我们可能希望根据用户是使用公司网络中的浏览器,还是使用移动设备上的应用程序进行浏览,来限制用户可以看到哪些数据。在处理敏感客户信息(如健康记录或税务信息)时,基于数据访问机制限制数据的做法是很常见的。

到目前为止,我们已经使用OAuth2服务器注册了一个应用程序EagleEye。然而,因为使用的是密码授权,所以需要在开始之前为这些用户创建用户账户和密码。

7.2.3 配置EagleEye用户

我们已经定义并存储了应用程序级的密钥名和密钥。现在要创建个人用户凭据及其所属的角色。用户角色将用于定义一组用户可以对服务执行的操作。

Spring可以从内存数据存储、支持JDBC的关系数据库或LDAP服务器中存储和检索用户信息(个人用户的凭据和分配给用户的角色)。

注意

我希望在定义上谨慎一些。Spring的OAuth2应用程序信息可以存储在内存或关系数据库中。Spring用户凭据和安全角色可以存储在内存数据库、关系数据库或LDAP(活动目录)服务器中。因为我们的主要目的是学习OAuth2,为了保持简单,我们将使用内存数据存储。

对于本章中的代码示例,我们将使用内存数据存储来定义用户角色。我们将定义两个用户账户,即john.carnellwilliam.woodwardjohn.carnell账户将拥有USER角色,而william.woodward账户将拥有ADMIN角色。

要配置OAuth2服务器以验证用户ID,必须创建一个新类WebSecurityConfigurer(在authentication-service/src/main/com/thoughtmechanix/authentication/security/WebSecurityConfigurer.java中)。代码清单7-3展示了这个类的代码。

代码清单7-3 为应用程序定义用户ID、密码和角色

package com.thoughtmechanix.authentication.security;

// 为了简洁,省略了import语句

@Configuration
public class WebSecurityConfigurer
extends WebSecurityConfigurerAdapter {  ⇽--- 扩展核心Spring SecurityWebSecurityConfigurerAdapter

    @Override
    @Bean  ⇽--- AuthenticationManagerBeanSpring Security用来处理验证
    public AuthenticationManager authenticationManagerBean()throws Exception{
        return super.authenticationManagerBean();
    }

    @Override
    @Bean  ⇽--- Spring Security使用UserDetailsService处理返回的用户信息,这些用户信息将由Spring Security返回
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth)throws Exception {
        auth.inMemoryAuthentication()  ⇽--- configure()方法是定义用户、密码和角色的地方
            .withUser("john.carnell")
            .password("password1")
            .roles("USER")
            .and()
            .withUser("william.woodward")
            .password("password2")
            .roles("USER", "ADMIN");
    }
}

像Spring Security框架的其他部分一样,要创建用户(及其角色),要从扩展WebSecurityConfigurerAdapter类并使用@Configuration注解标记它开始。Spring Security的实现方式类似于将乐高积木搭在一起来制造玩具车或模型。因此,我们需要为OAuth2服务器提供一种验证用户的机制,并返回正在验证的用户的用户信息。这通过在Spring WebSecurityConfigurerAdapter实现中定义authenticationManagerBean()userDetailsServiceBean()两个bean来完成。这两个bean通过使用父类WebSecurity``-ConfigurerAdapter中的默认验证authenticationManagerBean()userDetails``-ServiceBean()方法来公开。

从代码清单7-2中可以看出,这些bean被注入到OAuth2Config类中的configure(AuthorizationServerEndpointsConfigurer endpoints)方法中。

public void configure(AuthorizationServerEndpointsConfigurer endpoints)throws Exception {
    endpoints
       .authenticationManager(authenticationManager)
       .userDetailsService(userDetailsService);
}

我们将在稍后的实战中看到,这两个bean用于配置/auth/oauth/token/auth/user端点。

7.2.4 验证用户

此时,我们已经拥有足够多的基本OAuth2服务器功能来执行应用程序,并且能够执行密码授权流程的用户验证。我们现在将通过使用POSTMAN发送POST请求到http://localhost:8901/auth/oauth/token端点并提供应用程序名称、密钥、用户ID和密码来模拟用户获取OAuth2令牌。

首先,需要使用应用程序名称和密钥设置POSTMAN。我们将使用基本验证将这些元素传递到OAuth2服务器端点。图7-2展示了如何设置POSTMAN来执行基本验证调用。Spring微服务实战第7章 保护微服务_第2张图片

图7-2 使用应用程序名称和密钥设置基本验证

但是,我们还没有准备好执行调用来获取令牌。一旦配置了应用程序名称和密钥,就需要在服务中传递以下信息作为HTTP表单参数。

  • grant_type——正在执行的OAuth2授权类型。在本例中,将使用密码(password)授权。
  • scope——应用程序作用域。因为我们在注册应用程序时只定义了两个合法作用域(webclientmobileclient),因此传入的值必须是这两个作用域之一。
  • username——用户登录的名称。
  • password——用户登录的密码。

与本书中的其他REST调用不同,这个列表中的参数不会作为JSON体传递。OAuth2标准期望传递给令牌生成端点的所有参数都是HTTP表单参数。图7-3展示了如何为OAuth2调用配置HTTP表单参数。

Spring微服务实战第7章 保护微服务_第3张图片

图7-3 在请求OAuth2令牌时,用户的凭据作为HTTP表单参数传入/auth/oauth/token端点

图7-4展示了从/auth/oauth/token调用返回的JSON净荷。

Spring微服务实战第7章 保护微服务_第4张图片

图7-4 客户端凭据成功确认后返回的净荷

返回的净荷包含以下5个属性。

  • access_token——OAuth2令牌,它将随用户对受保护资源的每个服务调用一起出示。
  • token_type——令牌的类型。OAuth2规范允许定义多个令牌类型,最常用的令牌类型是不记名令牌(bearer token)。本章不涉及任何其他令牌类型。
  • refresh_token——包含一个可以提交回OAuth2服务器的令牌,以便在访问令牌过期后重新颁发一个访问令牌。
  • expires_in——这是OAuth2访问令牌过期前的秒数。在Spring中,授权令牌过期的默认值是12 h。
  • scope——此OAuth2令牌的有效作用域。

有了有效的OAuth2访问令牌,就可以使用验证服务中创建的/auth/user端点来检索与令牌相关联的用户的信息了。在本章的后面,所有受保护资源都将调用验证服务的/auth/user端点来确认令牌并检索用户信息。

图7-5展示了调用/auth/user端点的结果。如图7-5所示,注意OAuth2访问令牌是如何作为HTTP首部传入的。

Spring微服务实战第7章 保护微服务_第5张图片

图7-5 根据发布的OAuth2令牌查找用户信息

在图7-5中,我们对/auth/user端点发出HTTP GET请求。在任何时候调用OAuth2保护的端点(包括OAuth2的/auth/user端点),都需要传递OAuth2访问令牌。为此,要始终创建一个名为Authorization的HTTP首部,并附有Bearer XXXXX的值。在图7-5所示的调用中,这个HTTP首部的值是Bearer e9decabc-165b-4677-9190-2e0bf8341e0b。传入的访问令牌是在图7-4中调用/auth/oauth/token端点时返回的访问令牌。

如果OAuth2访问令牌有效,/auth/user端点就会返回关于用户的信息,包括分配给他们的角色。例如,从图7-5可以看出,用户john.carnell拥有USER角色。

注意

Spring将前缀ROLE_分配给用户角色,因此ROLE_USER意味着john.carnell拥有USER角色。

7.3 使用OAuth2保护组织服务

一旦通过OAuth2验证服务注册了一个应用程序,并且建立了拥有角色的个人用户账户,就可以开始探索如何使用OAuth2来保护资源了。虽然创建和管理OAuth2访问令牌是OAuth2服务器的职责,但在Spring中,定义哪些用户角色有权执行哪些操作是在单个服务级别上发生的。

要创建受保护资源,需要执行以下操作:

  • 将相应的Spring Security和OAuth2 jar添加到要保护的服务中;
  • 配置服务以指向OAuth2验证服务;
  • 定义谁可以访问服务。

让我们从一个最简单的例子开始,将组织服务创建为受保护资源,并确保它只能由已通过验证的用户来调用。

7.3.1 将Spring Security和OAuth2 jar添加到各个服务

与通常的Spring微服务一样,我们必须要向组织服务的Maven organization-service/pom.xml文件添加几个依赖项。在这里,需要添加两个依赖项:Spring Cloud Security和Spring Security OAuth2。Spring Cloud Security jar是核心的安全jar,它包含框架代码、注解定义和用于在Spring Cloud中实现安全性的接口。Spring Security OAuth2依赖项包含实现OAuth2验证服务所需的所有类。这两个依赖项的Maven条目是:

<dependency>
  <groupId>org.springframework.cloudgroupId>
  <artifactId>spring-cloud-securityartifactId>
dependency>
<dependency>
  <groupId>org.springframework.security.oauthgroupId>
  <artifactId>spring-security-oauth2artifactId>
dependency>

7.3.2 配置服务以指向OAuth2验证服务

记住,一旦将组织服务创建为受保护资源,每次调用服务时,调用者必须将包含OAuth2访问令牌的 Authentication HTTP首部包含到服务中。然后,受保护资源必须调用该OAuth2服务来查看令牌是否有效。

在组织服务的application.yml文件中以security.oauth2.resource.userInfoUri属性定义回调URL。下面是组织服务的application.yml文件中使用的回调配置:

security:
  oauth2:
    resource:
      userInfoUri: http://localhost:8901/auth/user

正如从security.oauth2.resource.userInfoUri属性看到的,回调URL是/auth/ user端点。这个端点在7.2.4节中讨论过。

最后,还需要告知组织服务它是受保护资源。同样,这一点可以通过向组织服务的引导类添加一个Spring Cloud注解来实现。组织服务的引导类代码如代码清单7-4所示,它可以在organization-service/src/main/java/com/thoughtmechanix/organization/Application.java中找到。

代码清单7-4 将引导类配置为受保护资源

package com.thoughtmechanix.organization;

// 为了简洁,省略了import语句
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
@EnableResourceServer  ⇽--- @EnableResourceServer注解用于告诉微服务,它是一个受保护资源
public class Application {
    @Bean
    public Filter userContextFilter() {
        UserContextFilter userContextFilter = new UserContextFilter();
        return userContextFilter;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@EnableResourceServer注解告诉Spring Cloud和Spring Security,该服务是受保护资源。@EnableResourceServer强制执行一个过滤器,该过滤器会拦截对该服务的所有传入调用,检查传入调用的HTTP首部中是否存在OAuth2访问令牌,然后调用security.oauth2.resource.userInfoUri中定义的回调URL来查看令牌是否有效。一旦获悉令牌是有效的,@EnableResourceServer注解也会应用任何访问控制规则,以控制什么人可以访问服务。

7.3.3 定义谁可以访问服务

我们现在已经准备好开始围绕服务定义访问控制规则了。要定义访问控制规则,需要扩展 ResourceServerConfigurerAdapter类并覆盖configure()方法。在组织服务中,ResourceServerConfiguration类位于organization service/src/main/java/com/thoughtmechanix/ organization/security/ResourceServerConfiguration.java。访问规则的范围可以从极其粗粒度(任何已通过验证的用户都可以访问整个服务)到非常细粒度(只有具有此角色的应用程序,才允许通过DELETE方法访问此URL)。

我们不会讨论Spring Security访问控制规则的各种组合,只是看一些更常见的例子。这些例子包括保护资源以便:

  • 只有已通过验证的用户才能访问服务URL;
  • 只有具有特定角色的用户才能访问服务URL。

1.通过验证用户保护服务

接下来要做的第一件事就是保护组织服务,使它只能由已通过验证的用户访问。代码清单7-5展示了如何将此规则构建到ResourceServerConfiguration类中。

代码清单7-5 限制只有已通过验证的用户可以访问

package com.thoughtmechanix.organization.security;

// 为了简洁,省略了import语句
@Configuration  ⇽--- 这个类必须使用@Configuration注解进行标记
public class ResourceServerConfiguration extends
    ResourceServerConfigurerAdapter {  ⇽--- ResourceServiceConfiguration类需要扩展ResourceServerConfigurerAdapter
    @Override  ⇽--- 所有访问规则都是在覆盖的configure()方法中定义的
    public void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests().anyRequest().authenticated();  ⇽--- 所有访问规则都是通过传入方法的HttpSecurity对象配置的
    }
}

所有的访问规则都将在configure()方法中定义。我们将使用由Spring传入的HttpSecurity类来定义规则。在本例中,我们将限制对组织服务中所有URL的访问,仅限已通过身份验证的用户才能访问。

如果在访问组织服务时没有在HTTP首部中提供OAuth2访问令牌,将会收到HTTP响应码401以及一条指示需要对服务进行完整验证的消息。

图7-6展示了在没有OAuth2 HTTP首部的情况下,对组织服务进行调用的输出结果。

Spring微服务实战第7章 保护微服务_第6张图片

图7-6 尝试调用组织服务将导致调用失败

接下来,我们将使用OAuth2访问令牌调用组织服务。要获取访问令牌,需要阅读7.2.4节,了解如何生成OAuth2令牌。我们需要将access_token字段的值从对/auth/oauth/token端点调用所返回的JSON调用结果中剪切出来,并在对组织服务的调用中粘贴使用它。记住,在调用组织服务时,需要添加一个名为Authorization的HTTP首部,其值为Bearer access_token

图7-7展示了对组织服务的调用,但是这次使用了传递给它的OAuth2访问令牌。

Spring微服务实战第7章 保护微服务_第7张图片

图7-7 在对组织服务的调用中传入OAuth2访问令牌

这可能是使用OAuth2保护端点的最简单的用例之一。接下来,我们将在此基础上进行构建,并将对特定端点的访问限制在特定角色。

2.通过特定角色保护服务

在接下来的示例中,我们将锁定组织服务的DELETE调用,仅限那些具有ADMIN访问权限的用户。正如7.2.3节中介绍过的,我们创建了两个可以访问EagleEye服务的用户账户,即john.carnellwilliam.woodwardjohn.carnell账户拥有USER角色,而william.woodward账户拥有USERADMIN角色。

代码清单7-6展示了如何创建configure()方法来限制对DELETE端点的访问,使得只有那些已通过验证并具有ADMIN角色的用户才能访问。

代码清单7-6 限制只有ADMIN角色可以进行删除

package com.thoughtmechanix.organization.security;

// 为了简洁,省略了import语句
@Configuration
public class ResourceServerConfiguration extendsResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception{
        http
            .authorizeRequests()
            .antMatchers(HttpMethod.DELETE, "/v1/organizations/**")  ⇽--- antMatchers()允许开发人员限制对受保护的URL和HTTP DELETE动词的调用
            .hasRole("ADMIN")  ⇽--- hasRole()方法是一个允许访问的角色列表,该列表由逗号分隔
            .anyRequest()
            .authenticated();
    }
}

在代码清单7-6中,我们将服务中以/v1/organizations开头的端点的DELETE调用限制为ADMIN角色:

.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/v1/organizations/**")
.hasRole("ADMIN")

antMatcher()方法可以使用一个以逗号分隔的端点列表。这些端点可以使用通配符风格的符号来定义想要访问的端点。例如,如果要限制DELETE调用,而不管URL名称中的版本如何,那么可以使用*来代替URL定义中的版本号:

.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/*/organizations/**")
.hasRole("ADMIN")

授权规则定义的最后一部分仍然定义了服务中的其他端点都需要由已通过验证的用户来访问:

.anyRequest()
.authenticated();

现在,如果要为用户john.carnell(密码为password1)获取一个OAuth2令牌,并试图调用组织服务的DELETE端点(http://-localhost:8085/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a),那么将会收到HTTP状态码401,以及一条指示访问被拒绝的错误消息。由调用返回的JSON文本将是:

{
   "error": "access_denied",
   "error_description": "Access is denied"
}

如果使用william.woodward用户账户(密码:password2)及其OAuth2令牌尝试完全相同的调用,会看到返回一个成功的调用(HTTP状态码204 —— Not Content),并且该组织将被组织服务删除。

到目前为止,我们已经研究了两个简单示例,它们使用OAuth2调用和保护单个服务(组织服务)。然而,通常在微服务环境中,将会有多个服务调用用来执行一个事务。在这些类型的情况下,需要确保OAuth2访问令牌在服务调用之间传播。

7.3.4 传播OAuth2访问令牌

为了演示在服务之间传播OAuth2令牌,我们现在来看一下如何使用OAuth2保护许可证服务。记住,许可证服务调用组织服务查找信息。问题在于,如何将OAuth2令牌从一个服务传播到另一个服务?

我们将创建一个简单的示例,使用许可证服务调用组织服务。这个示例以第6章中的例子为基础,两个服务都在Zuul网关后面运行。

图7-8展示了一个已通过验证的用户的OAuth2令牌如何流经Zuul网关、许可证服务然后到达组织服务的基本流程。

Spring微服务实战第7章 保护微服务_第8张图片

图7-8 必须在整个调用链中携带OAuth2令牌

在图7-8中发生了以下活动。

(1)用户已经向OAuth2服务器进行了验证,并向EagleEye Web应用程序发出调用。用户的OAuth2访问令牌存储在用户的会话中。EagleEye Web应用程序需要检索一些许可数据,并对许可证服务的REST端点进行调用。作为许可证服务的REST端点的一部分,EagleEye Web应用程序将通过HTTP首部Authorization添加OAuth2访问令牌。许可证服务只能在Zuul服务网关后面访问。

(2)Zuul将查找许可证服务端点,然后将调用转发到其中一个许可证服务的服务器。服务网关需要从传入的调用中复制HTTP首部Authorization,并确保HTTP首部Authorization被转发到新端点。

(3)许可证服务将接收传入的调用。由于许可证服务是受保护资源,它将使用EagleEye的OAuth2服务来确认令牌,然后检查用户的角色是否具有适当的权限。作为其工作的一部分,许可证服务会调用组织服务。在执行这个调用时,许可证服务需要将用户的OAuth2访问令牌传播到组织服务。

(4)当组织服务接收到该调用时,它将再次使用HTTP首部Authorization的令牌,并使用EagleEye OAuth2服务器来确认令牌。

实现这些流程需要做两件事。第一件事是需要修改Zuul服务网关,以将OAuth2令牌传播到许可证服务。在默认情况下,Zuul不会将敏感的HTTP首部(如CookieSet-CookieAuthorization)转发到下游服务。要让Zuul传播HTTP首部Authorization,需要在Zuul服务网关的application.yml或Spring Cloud Config数据存储中设置以下配置:

zuul.sensitiveHeaders: Cookie,Set-Cookie

这一配置是黑名单,它包含Zuul不会传播到下游服务的敏感首部。在上述黑名单中没有Authorization值就意味着Zuul将允许它通过。如果根本没有设置zuul.sensitive-Headers属性,Zuul将自动阻止3个值(CookieSet-CookieAuthorization)被传播。

Zuul的其他OAuth2功能呢?

Zuul可以自动传播下游的OAuth2访问令牌,并通过使用@EnableOAuth2Sso注解来针对OAuth2服务的传入请求进行授权。我特意没有使用这种方法,因为我在本章的目标是,在不增加其他复杂性(或调试)的情况下,展示OAuth2如何工作的基础知识。虽然Zuul服务网关的配置并不复杂,但它会在本已经拥有许多内容的章节中添加更多内容。如果读者有兴趣让Zuul服务网关参与单点登录(Single Sign On,SSO),Spring Cloud Security文档中有一个简短而全面的教程,它涵盖了Spring服务器的建立。

需要做的第二件事就是将许可证服务配置为OAuth2资源服务,并建立所需的服务授权规则。本节不会详细讨论许可证服务的配置,因为在7.3.3节中已经讨论过授权规则。

最后,需要做的就是修改许可证服务中调用组织服务的代码。我们需要确保将HTTP首部Authorization注入应用程序对组织服务的调用中。如果没有Spring Security,那么开发人员必须编写一个servlet过滤器以从传入的许可证服务调用中获取HTTP首部,然后手动将它添加到许可证服务中的每个出站服务调用中。Spring OAuth2提供了一个支持OAuth2调用的新REST模板类OAuth2RestTemplate。要使用OAuth2RestTemplate类,需要先将它公开为一个可以被自动装配到调用另一个受OAuth2保护的服务的服务的bean。我们可以在licensing-service/ src/main/java/com/thoughtmechanix/licenses/Application.java中执行上述操作:

@Bean
public OAuth2RestTemplate oauth2RestTemplate(
➥  OAuth2ClientContext oauth2ClientContext,
➥  OAuth2ProtectedResourceDetails details) {
    return new OAuth2RestTemplate(details, oauth2ClientContext);
}

要实际查看 OAuth2RestTemplate类,可以查看 licensing-service/src/main/java/com/thoughtmechanix/ licenses/clients/OrganizationRestTemplate.java中的OranizationRestTemplateClient类。代码清单7-7展示了OAuth2RestTemplate是如何自动装配到这个类中的。

代码清单7-7 使用OAuth2RestTemplate来传播OAuth2访问令牌

package com.thoughtmechanix.organization.security;

// 为了简洁,省略了import语句

@Component
public class OrganizationRestTemplateClient {
    @Autowired
    OAuth2RestTemplate restTemplate;  ⇽--- OAuth2RestTemplate是标准RestTemplate的增强式替代品,可处理OAuth2访问令牌的传播

    private static final Logger logger =LoggerFactory.getLogger(OrganizationRestTemplateClient.class);

    public Organization getOrganization(String organizationId){
        logger.debug("In Licensing Service.getOrganization: {}",UserContext.getCorrelationId());

        ResponseEntity<Organization> restExchange =  ⇽--- 调用组织服务的方式与标准的RestTemplate完全相同
        ➥  restTemplate.exchange(
            ➥  "http://zuulserver:5555/api/organization
            ➥  /v1/organizations/{organizationId}",HttpMethod.GET,null, Organization.class, organizationId);

        return restExchange.getBody();
    }
}

7.4 JSON Web Token与OAuth2

OAuth2是一个基于令牌的验证框架,但具有讽刺意味的是,它并没有为如何定义其规范中的令牌提供任何标准。为了矫正OAuth2令牌标准的缺陷,一个名为JSON Web Token(JWT)的新标准脱颖而出。JWT是因特网工程任务组(Internet Engineering Task Force,IETF)提出的开放标准(RFC-7519),旨在为OAuth2令牌提供标准结构。JWT令牌具有如下特点。

  • 小巧——JWT令牌编码为Base64,可以通过URL、HTTP首部或HTTP POST参数轻松传递。
  • 密码签名——JWT令牌由颁发它的验证服务器签名。这意味着可以保证令牌没有被篡改。
  • 自包含——由于JWT令牌是密码签名的,接收该服务的微服务可以保证令牌的内容是有效的,因此,不需要调用验证服务来确认令牌的内容,因为令牌的签名可以被接收微服务确认,并且内容(如令牌和用户信息的过期时间)可以被接收微服务检查。
  • 可扩展——当验证服务生成一个令牌时,它可以在令牌被密封之前在令牌中放置额外的信息。接收服务可以解密令牌净荷,并从它里面检索额外的上下文。

Spring Cloud Security为JWT提供了开箱即用的支持。但是,要使用和消费JWT令牌,OAuth2验证服务和受验证服务保护的服务必须以不同的方式配置。这个配置并不困难,接下来让我们来看一下不一样的地方。

注意

我选择将JWT配置保存在本章的GitHub存储库的一个单独分支中(名为JWT_Example)。这是因为标准的Spring Cloud Security OAuth2配置和基于JWT的OAuth2配置需要不同的配置类。

7.4.1 修改验证服务以颁发JWT令牌

对于要受OAuth2保护的验证服务和两个微服务(许可证服务和组织服务),需要在它们的Maven pom.xml文件中添加一个新的Spring Security依赖项,以包含JWT OAuth2库。这个新的依赖项是:


  org.springframework.security
  spring-security-jwt

添加完Maven依赖项之后,需要先告诉验证服务如何生成和翻译JWT令牌。为此,将要在验证服务中创建一个名为JWTTokenStoreConfig的新配置类(在authentication-service/src/java/com/thoughtmechanix/authentication/security/JWTTokenStoreConfig.java中)。代码清单7-8展示了这个类的代码。

代码清单7-8 创建JWT令牌存储

@Configuration
public class JWTTokenStoreConfig {

    @Autowired
    private ServiceConfig serviceConfig;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    @Primary  ⇽--- @Primary注解用于告诉Spring,如果有多个特定类型的bean(在本例中是DefaultTokenService),那么就使用被@Primary标注的bean类型进行自动注入

    public DefaultTokenServices tokenServices() {  ⇽--- 用于从出示给服务的令牌中读取数据
        DefaultTokenServices defaultTokenServices
        ➥  = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {  ⇽--- 在JWT和OAuth2服务器之间充当翻译
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(serviceConfig.getJwtSigningKey());  ⇽--- 定义将用于签署令牌的签名密钥
        return converter;
    }

    @Bean
    public TokenEnhancer jwtTokenEnhancer() {
        return new JWTTokenEnhancer();
    }
}

JWTTokenStoreConfig类用于定义Spring将如何管理JWT令牌的创建、签名和翻译。因为tokenServices()将使用Spring Security的默认令牌服务实现,所以这里的工作是固定的。我们要关注的是 jwtAccessTokenConverter()方法,它定义了令牌将如何被翻译。关于这个方法,需要注意的最重要的一点是,我们正在设置将要用于签署令牌的签名密钥。

对于本例,我们将使用一个对称密钥,这意味着验证服务和受验证服务保护的服务必须要在所有服务之间共享相同的密钥。该密钥只不过是存储在验证服务Spring Cloud Config条目(https://github.com/carnellj/config-repo/blob/master/authenticationservice/authenticationservice.yml)中的随机字符串值。这个签名密钥的实际值是

signing.key: "345345fsdgsf5345"

注意

Spring Cloud Security支持对称密钥加密和使用公钥/私钥的不对称加密。本书不打算使用公钥/私钥创建JWT。遗憾的是,关于JWT、Spring Security和公私钥的文档很少。如果读者对实现上面讨论的内容感兴趣,我强烈建议读者查看Baeldung.com,它非常好地解释了JWT和公钥/私钥如何创建。

在代码清单7-8的JWTTokenStoreConfig中,我们定义了如何创建和签名JWT令牌。现在,我们需要将它挂钩到整个OAuth2服务中。在代码清单7-2中,我们使用OAuth2Config类来定义OAuth2服务的配置,我们创建了用于服务的验证管理器,以及应用程序名称和密钥。接下来,我们将使用一个名为JWTOAuth2Config的新类(在authentication-service/src/main/java/ com/thoughtmechanix/authentication/security/JWTOAuth2Config.java中)替换OAuth2Config类。

代码清单7-9展示了JWTOAuth2Config类的代码。

代码清单7-9 通过JWTOAuth2Config类将JWT挂钩到验证服务中

package com.thoughtmechanix.authentication.security;

// 为了简洁,省略了import语句
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  private UserDetailsService userDetailsService;

  @Autowired
  private TokenStore tokenStore;

  @Autowired
  private DefaultTokenServices tokenServices;

  @Autowired
  private JwtAccessTokenConverter jwtAccessTokenConverter;

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints)throws Exception {

      TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
      tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer,
      ➥  jwtAccessTokenConverter));

      endpoints
          .tokenStore(tokenStore)  ⇽--- 代码清单7-8中定义的令牌存储将在这里注入
          .accessTokenConverter(jwtAccessTokenConverter)  ⇽--- 这是钩子,用于告诉Spring Security OAuth2代码使用JWT
          .authenticationManager(authenticationManager)
          .userDetailsService(userDetailsService);
  }

// 为了简洁,省略了类的其余部分
}

现在,如果重新构建验证服务并重新启动它,应该会返回一个基于JWT的令牌。图7-9展示了调用验证服务的结果,现在它使用JWT。

Spring微服务实战第7章 保护微服务_第9张图片

图7-9 来自验证调用的访问和刷新令牌现在是JWT令牌

实际的令牌本身并不是直接作为JSON返回的。相反,JSON体使用Base64进行了编码。如果读者对JWT令牌的内容感兴趣,可以使用在线工具来解码令牌。我喜欢使用一个叫Stormpath的公司的在线工具,这个工具是一个在线的JWT解码器。图7-10展示了解码令牌的输出结果。

Spring微服务实战第7章 保护微服务_第10张图片

图7-10 使用http://jswebtoken.io可以解码内容

注意

了解JWT令牌已签名但未加密非常重要。任何在线JWT工具都可以解码JWT令牌并公开其内容。我之所以提到这一点,是因为JWT规范允许开发人员扩展令牌,并向令牌添加额外的信息。不要在JWT令牌中暴露敏感信息或个人身份信息(Personally Identifiable Information,PII)。

7.4.2 在微服务中使用JWT

到目前为止,我们已经拥有了创建JWT令牌的OAuth2验证服务。下一步就是配置许可证服务和组织服务以使用JWT。这很简单,只需要做两件事。

(1)将spring-security-jwt依赖项添加到许可证服务和组织服务的pom.xml文件(参见7.4.1节,以获取需要添加的确切的Maven依赖项)。

(2)在许可证服务和组织服务中创建JWTTokenStoreConfig类。这个类几乎与验证服务使用的类相同(参见代码清单7-8)。本书不会重复讲解相同的东西,读者可以在licensing-service/ src/main/com/thoughtmechanix/licensing-service/security/JWTTokenStoreConfig.java和organization- service/src/main/com/thoughtmechanix/organization-service/security/JWTTokenStoreConfig.java中看到JWTTokenStoreConfig类的例子。

我们需要做最后一项工作。因为许可证服务调用组织服务,所以需要确保OAuth2令牌被传播。这项工作通常是通过OAuth2RestTemplate类完成的,但是OAuth2RestTemplate类并不传播基于JWT的令牌。为了确保许可证服务能够做到这一点,需要添加一个自定义的RestTemplate bean来完成这个注入。这个自定义的RestTemplate可以在licensingservice/src/ main/java/com/thoughtmechanix/licenses/Application.java中找到。代码清单7-10展示了这个自定义bean的定义。

代码清单7-10 创建自定义的 RestTemplate类以注入JWT令牌

public class Application {
    // 为了简洁,省略了其他代码
    @Primary
    @Bean
    public RestTemplate getCustomRestTemplate() {
        RestTemplate template = new RestTemplate();
        List interceptors = template.getInterceptors();
        if (interceptors == null) {
            template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));  ⇽--- UserContextInterceptor会将Authorization首部注入每个REST调用
        } else {
            interceptors.add(new UserContextInterceptor());  ⇽--- UserContextInterceptor会将Authorization首部注入每个REST调用
            template.setInterceptors(interceptors);
        }

        return template;
    }
}

在前面的代码中,我们定义了一个使用ClientHttpRequestInterceptor的自定义RestTemplate bean。回想一下第6章,ClientHttpRequestInterceptor是一个Spring类,它允许在基于REST的调用之前挂钩要执行的功能。这个拦截器类是第6章中定义的UserContextInterceptor类的变体。这个类在licensing-service/src/main/java/com/thoughtmechanix/ licenses/utils/UserContextInterceptor.java中。代码清单7-11展示了这个类。

代码清单7-11 UserContextInterceptor将注入JWT令牌到REST调用

public class UserContextInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,ClientHttpRequestExecution execution)throws IOException {

        headers.add(UserContext.CORRELATION_ID,UserContextHolder.getContext().getCorrelationId());
        ➥  headers.add(UserContext.AUTH_TOKEN,UserContextHolder.getContext().getAuthToken());  ⇽--- 将授权令牌添加到HTTP首部
        return execution.execute(request, body);
    }
}

UserContextInterceptor使用了第6章中的几个实用工具类。记住,每个服务都使用一个自定义servlet过滤器(名为UserContextFilter)来从HTTP首部解析出验证令牌和关联ID。在代码清单7-11中,我们使用已解析的UserContext.AUTH_TOKEN值来填入传出的HTTP调用。

就是这样。有了这些功能部件,现在就可以调用许可证服务(或组织服务),并将Base64编码的JWT添加到HTTP Authorizationt首部中,其值为Bearer<>,服务将正确地读取和确认JWT令牌。

7.4.3 扩展JWT令牌

如果读者仔细观察图7-10中的JWT令牌,那么就会注意到EagleEye的organizationId字段(图7-11展示了图7-10中展示的JWT令牌的放大图)。这不是标准的JWT令牌字段,而是额外的字段,是在创建JWT令牌时通过注入新字段添加的。

Spring微服务实战第7章 保护微服务_第11张图片

图7-11 使用organizationId扩展JWT令牌的示例

通过向验证服务添加一个Spring OAuth2令牌增强器类,可以很容易地扩展JWT令牌。这个类是JWTTokenEnhancer,其源代码可以在authentication-service/src/main/java/com/thoughtmechanix/ authentication/security/JWTTokenEnhancer.java中找到。代码清单7-12展示了这段代码。

代码清单7-12 使用JWT令牌增强器类添加自定义字段

package com.thoughtmechanix.authentication.security;

// 为了简洁,省略了其他import语句
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

public class JWTTokenEnhancer implements TokenEnhancer {  ⇽--- 需要扩展TokenEnhancer@Autowired
    private OrgUserRepository orgUserRepo;

    private String getOrgId(String userName){  ⇽--- getOrgId()方法基于用户名查找用户的组织ID
        UserOrganization orgUser =
        ➥  orgUserRepo.findByUserName( userName );
        return orgUser.getOrganizationId();
    }

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,  ⇽--- 要进行这种增强,需要覆盖enhance()方法
    ➥  OAuth2Authentication authentication) {
        Map<String, Object> additionalInfo = new HashMap<>();
        String orgId = getOrgId(authentication.getName());

        additionalInfo.put("organizationId", orgId);

        ((DefaultOAuth2AccessToken) accessToken)
            .setAdditionalInformation(additionalInfo);  ⇽--- 所有附加的属性都放在HashMap中,并设置在传入该方法的accessToken变量上
        return accessToken;
    }
}

需要做的最后一件事是告诉OAuth2服务使用JWTTokenEnhancer类。首先,需要为JWTTokenEnhancer类公开一个Spring bean。通过在代码清单7-8中定义的JWTTokenStoreConfig类中添加一个bean定义来实现这一点:

package com.thoughtmechanix.authentication.security;

@Configuration
public class JWTTokenStoreConfig {
    // 为了简洁,省略了类的其余部分
    @Bean
    public TokenEnhancer jwtTokenEnhancer() {
        return new JWTTokenEnhancer();
    }
}

一旦将JWTTokenEnhancer作为bean公开,那么就可以将它挂钩到代码清单7-9所示的JWTOAuth2Config类中。这一点在JWTOAuth2Config类的configure()方法中完成。代码清单7-13展示了对JWTOAuth2Config类的configure()方法的修改。

代码清单7-13 挂钩TokenEnhancer

package com.thoughtmechanix.authentication.security;
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {
    // 为了简洁,省略了其余代码
    @Autowired
    private TokenEnhancer jwtTokenEnhancer;  ⇽--- 自动装配在TokenEnhancer类中

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)throws Exception {
        TokenEnhancerChain tokenEnhancerChain =new TokenEnhancerChain();  ⇽--- Spring OAuth允许开发人员挂钩多个令牌增强器,因此将令牌增强器添加到TokenEnhancerChain类中
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

        endpoints.tokenStore(tokenStore)
            .accessTokenConverter(jwtAccessTokenConverter)
            .tokenEnhancer(tokenEnhancerChain)  ⇽--- 将令牌增强器挂钩到传入configure()方法的endpoints参数
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService);
    }
}

到目前为止,我们已将自定义字段添加到JWT令牌中。接下来的问题是,如何从JWT令牌中解析自定义字段?

7.4.4 从JWT令牌中解析自定义字段

本节将转到Zuul网关,以说明如何解析JWT令牌中的自定义字段。具体来说,我们将修改第6章中介绍的TrackingFilter类,以从流经网关的JWT令牌中解码 organizationId字段。

要完成这一点,我们将要引入一个JWT解析器库,并添加到Zuul服务器的pom.xml文件中。有多个令牌解析器可供使用,这里选择JJWT库来进行解析。这个库的Maven依赖项是


  io.jsonwebtoken
  jjwt
  0.7.0

添加完JJWT库后,可以向TrackingFiler类(在zuulsvr/src/main/java/com/thoughtmechanix/ zuulsvr/filters/TrackingFilter.java 中)添加一个名为getOrganizationId()的新方法。代码清单7-14展示了这个新方法。

代码清单7-14 从JWT令牌中解析出organizationId

private String getOrganizationId(){
    String result="";
    if (filterUtils.getAuthToken()!=null){
        String authToken = filterUtils  ⇽--- 从HTTP首部Authorization解析出令牌
                         .getAuthToken()
                         .replace("Bearer ","");
        try {
            Claims claims =  ⇽--- 传入用于签署令牌的签名密钥,使用JWTS类解析令牌
            ➥  Jwts.parser()
                    .setSigningKey(serviceConfig
                    ➥  .getJwtSigningKey().getBytes("UTF-8"))
                   .parseClaimsJws(authToken)
                   .getBody();
            result = (String) claims.get("organizationId");  ⇽--- 从令牌中提取出organizationId
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }
    return result;
}

实现了getOrganizationId()方法之后,我们就将System.out.println添加到TrackingFilterrun()方法中,以打印从流经Zuul网关的JWT令牌中解析出来的organizationId。接下来,我们就来调用任何启用网关的REST端点。我使用GET方法调用http://localhost:5555/api/licensing/v1/organizations/e254f8c-c442-4ebe-a82a-
e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a。记住,在进行这个调用时,仍然需要创建所有HTTP表单参数和HTTP授权首部,来包含Authorization首部和JWT令牌。

图7-12展示了已解析的organizationId在命令行控制台的输出。

Spring微服务实战第7章 保护微服务_第12张图片

图7-12 Zuul服务从流经的JWT令牌中解析出组织ID

7.5 关于微服务安全的总结

虽然本章介绍了OAuth2规范,以及如何使用Spring Cloud Security实现OAuth2验证服务,但OAuth2只是微服务安全难题的一部分。在构建用于生产级别的微服务时,应该围绕以下实践构建微服务安全。

(1)对所有服务通信使用HTTPS/安全套接字层(Secure Sockets Layer,SSL)。

(2)所有服务调用都应通过API网关。

(3)将服务划分到公共API和私有API。

(4)通过封锁不需要的网络端口来限制微服务的攻击面。

图7-13展示了这些不同的实践如何配合起来工作。上述列表中的每个编号项都与图7-13中的数字对应。

Spring微服务实战第7章 保护微服务_第13张图片

图7-13 微服务安全架构不只是实现OAuth2

让我们更详细地审查前面列表和图7-13中列出的每个主题领域。

1.为所有业务通信使用HTTPS/安全套接字层(SSL)

在本书的所有代码示例中,我们一直使用HTTP,这是因为HTTP是一个简单的协议,并且不需要在每个服务上进行安装就能开始使用该服务。

在生产环境中,微服务应该只通过HTTPS和SSL提供的加密通道进行通信。HTTPS的配置和安装可以通过DevOps脚本自动完成。

注意

如果应用程序需要满足信用卡支付的支付卡行业(Payment Card Industry,PCI)的合规性要求,那么就需要为所有的服务通信实现HTTPS。在构建服务时,要尽早就使用HTTPS,这要比将应用程序和微服务部署到生产环境之后再进行项目迁移容易得多。

2.使用服务网关访问微服务

客户端永远不应该直接访问运行服务的各个服务器、服务端点和端口。相反,应该使用服务网关作为服务调用的入口点和守门人。在微服务运行的操作系统或容器上配置网络层,以便仅接受来自服务网关的流量。

记住,服务网关可以作为一个针对所有服务执行的策略执行点(PEP)。通过像Zuul这样的服务网关来进行服务调用,让开发人员可以在保护和审计服务方面保持一致。服务网关还允许开发人员锁定要向外界公开的端口和端点。

3.将服务划分到公共API和私有API

一般来说,安全是关于构建访问和执行最小权限概念的层。最小权限是用户应该拥有最少的网络访问权限和特权来完成他们的日常工作。为此,开发人员应该通过将服务分离到两个不同的区域(即公共区域和私有区域)来实现最小权限。

公共区域包含由客户端使用的公共API(EagleEye应用程序)。公共API微服务应该执行面向工作流的小任务。公共API微服务通常是服务聚合器,在多个服务中提取数据并执行任务。

公共微服务应该位于它们自己的服务网关后面,并拥有自己的验证服务来执行OAuth2验证。客户端应用程序应该通过受服务网关保护的单一路由访问公共服务。此外,公共区域应该有自己的验证服务。

私有区域充当保护核心应用程序功能和数据的壁垒,它应该只通过一个众所周知的端口访问,并且应该被封锁,只接受来自运行私有服务的网络子网的网络流量。除此之外,私有区域应该拥有自己的服务网关和验证服务。公共API服务应该对私有区域验证服务进行验证。所有的应用程序数据至少应该在私有区域的网络子网中,并且只能通过驻留在私有区域的微服务访问。

私有API网络区域应该要被封锁到什么程度

许多组织采取的方法是,他们的安全模型应该有一个坚硬的外在中心,但拥有一个更柔软的内表面。这意味着,一旦流量进入私有API区域,私有区域中的服务之间的通信就可以不加密(不需要HTTPS),也不需要验证机制。大多数时候,这都是为了方便和加快开发。拥有的安全性越高,调试问题的难度就越大,从而增加管理应用程序的整体复杂性。

我倾向于对这个世界抱有一种偏执的看法。(我在金融服务行业工作了8年,因此自然而然地变得多疑。)我宁愿牺牲额外的复杂性(可以通过DevOps脚本来减轻这种复杂性),强制在私有API区域中运行的所有服务都使用SSL,并通过私有区域中运行的验证服务进行验证。读者需要问自己的问题是,是否愿意看到自己的组织因为遭受网络入侵而登上当地报纸的头版?

4.通过封锁不需要的网络端口来限制微服务的攻击面

许多开发人员并没有重视为了使服务正常运行而需要打开的端口的最少数量。请配置运行服务的操作系统,只允许打开入站和出站访问服务所需的端口,或者服务所需的一部分基础设施(监视、日志聚合)。

不要只关注入站访问端口。许多开发人员忘记了封锁他们的出站端口。封锁出站端口可以防止数据在服务本身被攻击者破坏的情况下从服务中泄露。另外,要确保查看公共API区域和私有API区域中的网络端口访问。

7.6 小结

  • OAuth2是一个基于令牌的验证框架,用于对用户进行验证。
  • OAuth2确保每个执行用户请求的微服务不需要在每次调用时都出示用户凭据。
  • OAuth2为保护Web服务调用提供了不同的机制,这些机制称为授权(grant)。
  • 要在Spring中使用OAuth2,需要建立一个基于OAuth2的验证服务。
  • 想要调用服务的每个应用程序都需要通过OAuth2验证服务注册。
  • 每个应用程序都有自己的应用程序名称和密钥。
  • 用户凭据和角色存储在内存或数据存储中,并通过Spring Security访问。
  • 每个服务必须定义角色可以采取的动作。
  • Spring Cloud Security支持JSON Web Token(JWT)规范。
  • JWT定义了一个签名的JSON标准,用于生成OAuth2令牌。
  • 使用JWT可以将自定义字段注入规范中。
  • 保护微服务涉及的不仅仅是使用OAuth2,还应该使用HTTPS加密服务之间的所有调用。
  • 使用服务网关来缩小可以到达服务的访问点的数量。
  • 通过限制运行服务的操作系统上的入站端口和出站端口数来限制服务的攻击面。

源码地址

https://github.com/WaterMoonMirror/spmia-chapter7

就越大,从而增加管理应用程序的整体复杂性。

我倾向于对这个世界抱有一种偏执的看法。(我在金融服务行业工作了8年,因此自然而然地变得多疑。)我宁愿牺牲额外的复杂性(可以通过DevOps脚本来减轻这种复杂性),强制在私有API区域中运行的所有服务都使用SSL,并通过私有区域中运行的验证服务进行验证。读者需要问自己的问题是,是否愿意看到自己的组织因为遭受网络入侵而登上当地报纸的头版?

4.通过封锁不需要的网络端口来限制微服务的攻击面

许多开发人员并没有重视为了使服务正常运行而需要打开的端口的最少数量。请配置运行服务的操作系统,只允许打开入站和出站访问服务所需的端口,或者服务所需的一部分基础设施(监视、日志聚合)。

不要只关注入站访问端口。许多开发人员忘记了封锁他们的出站端口。封锁出站端口可以防止数据在服务本身被攻击者破坏的情况下从服务中泄露。另外,要确保查看公共API区域和私有API区域中的网络端口访问。

7.6 小结

  • OAuth2是一个基于令牌的验证框架,用于对用户进行验证。
  • OAuth2确保每个执行用户请求的微服务不需要在每次调用时都出示用户凭据。
  • OAuth2为保护Web服务调用提供了不同的机制,这些机制称为授权(grant)。
  • 要在Spring中使用OAuth2,需要建立一个基于OAuth2的验证服务。
  • 想要调用服务的每个应用程序都需要通过OAuth2验证服务注册。
  • 每个应用程序都有自己的应用程序名称和密钥。
  • 用户凭据和角色存储在内存或数据存储中,并通过Spring Security访问。
  • 每个服务必须定义角色可以采取的动作。
  • Spring Cloud Security支持JSON Web Token(JWT)规范。
  • JWT定义了一个签名的JSON标准,用于生成OAuth2令牌。
  • 使用JWT可以将自定义字段注入规范中。
  • 保护微服务涉及的不仅仅是使用OAuth2,还应该使用HTTPS加密服务之间的所有调用。
  • 使用服务网关来缩小可以到达服务的访问点的数量。
  • 通过限制运行服务的操作系统上的入站端口和出站端口数来限制服务的攻击面。

源码地址

https://github.com/WaterMoonMirror/spmia-chapter7

你可能感兴趣的:(微服务,spring,restful,java)