第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服务器就会提供一个令牌,每当用户的应用程序使用的服务试图访问受保护的资源(微服务)时,就可以提交这个令牌。
 
 
图7-1 OAuth2允许用户进行验证,而不必持续提供凭据
接下来,受保护资源可以联系OAuth2服务器以确定令牌的有效性,并检索用户授予它们的角色。角色用于将相关用户分组在一起,并定义用户组可以访问哪些资源。对于本章来说,我们将使用OAuth2和角色来定义用户可以调用哪些服务端点,以及用户可以在端点上调用的HTTP动词。
    Web服务安全是一个极其复杂的主题。开发人员必须了解谁将调用自己的服务(公司网络的内部用户还是外部用户),他们将如何调用这些服务(是在内部基于Web客户端、移动设备还是在企业网络之外的Web应用程序),以及他们用代码来完成什么操作。OAuth2允许开发人员使用称为授权(grant)的不同验证方案,在不同的场景中保护基于REST的服务。OAuth2规范具有以下4种类型的授权(表示客户端应用程序获取用户授权的4种方式,比如下面的授权码在微信api和一个客户端应用程序需要从QQ平台获取用户信息):
    密码(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.cloud</groupId>   
<artifactId>
spring-cloud-security
</artifactId> 
</dependency> 
<dependency>   
<groupId>org.springframework.security.oauth</groupId>   
<artifactId>spring-security-oauth2</artifactId> 
</dependency>
    
第一个依赖项spring-cloud-security引入了通用Spring和Spring Cloud安全库。第二个依赖项spring-security-oauth2拉取了SpringOAuth2库。
    既然已经定义完Maven依赖项,那么就可以在引导类上进行工作。这个引导类可以在authentication-service/src/main/java/com/thoughtmechanix
/authentication/Application.java中找到。代码清单7-1展示Application类的代码。
代码清单7-1 authentication-service的引导类
// 为了简洁,省略了import语句 
@SpringBootApplication
@RestController
@EnableResourceServer
⇽--- 用于告诉Spring Cloud,该服务将作为OAuth2服务
@EnableAuthorizationServer
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的端点(自动添加的/oauth端点和类里手动添加的/user端点),这些端点将在OAuth2验证和授权过程中使用。
    在代码清单7-1中,看到的第二件事是添加了一个名为/user(映射到/auth/user)的端点。当试图访问由OAuth2保护的服务时,将会用到这个端点,本章后文会进行介绍。此端点由受保护服务调用,以确认OAuth2访问令牌,并检索访问受保护服务的用户所分配的角色(确认访问令牌的有效性和获取访问用户的角色名)。本章稍后会详细讨论这个端点。
    7.2.2 使用OAuth2服务注册客户端应用程序
    此时,我们已经有了一个验证服务,但尚未在验证服务器中定义任何应用程序、用户或角色。我们可以从已通过验证服务注册EagleEye应用程序开始(这句话的意思是从已经在验证服务上注册的EagleEye应用程序开始。为此,我们将在验证服务中创建一个名为OAuth2Config的类(在authentication-service/src/main/java/com/thoughtmechanix/authentication/ security/OAuth2Config.java中)。
    这个类将定义通过OAuth2验证服务注册哪些应用程序(在OAuth2验证服务里注册哪些应用程序)。需要注意的是,不能只因为应用程序通过OAuth2服务中注册过,就认为该服务能够访问任何受保护资源。
    
验证与授权
    我经常发现开发人员混淆术语验证(authentication)和授权(authorization)的含义。验证是用户通过提供凭据来证明他们是谁的行为。授权决定是否允许用户做他们想做的事情。例如,Jim可以通过提供用户ID和密码来证明他的身份,但是他可能没有被授权查看敏感数据,如工资单数据。出于我们讨论的目的,必须在授权发生之前对用户进行验证。
    OAuth2Config类定义了OAuth2服务知道的应用程序和用户凭据。在代码清单7-2中可以看到OAuth2Config类的代码。
 
代码清单7-2 OAuth2Config服务定义哪些应用程序可以使用服务(具体客户端应用程序能够使用哪些服务是由用户授权的,而即使用户授权了,如果用户本身对该服务没有权限,则依然客户端应用程序操作失败)。
    // 为了简洁,省略了import语句
⇽--- 继承AuthorizationServerConfigurerAdapter类,并使用@Configuration注解标注这个类  
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Autowired
    private UserDetailsService userDetailsService;
    ⇽--- 覆盖configure()方法。这定义了哪些客户端将注册到验证服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("eagleeye")
                .secret("thisissecret")
                .authorizedGrantTypes("refresh_token", "password", "client_credentials")
                .scopes("webclient", "mobileclient");
    }
    ⇽--- 该方法定义了AuthenticationServerConfigurer中使用的不同组件。
          这段代码告诉Spring使用Spring提供的默认验证管理器和用户详细信息服务    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
      endpoints
        .authenticationManager(authenticationManager)
        .userDetailsService(userDetailsService);
    }
}
 
在代码清单7-2所示的代码中,要注意的第一件事是,这个类扩展了Spring的AuthenticationServerConfigurer类,然后使用@Configuration注解对这个类进行了标记。AuthenticationServerConfigurer类是Spring Security的核心部分,它提供了执行关键验证和授权功能的基本机制。对于OAuth2Config类,我们将要覆盖两个方法。第一个方法是configure(),它用于定义通过验证服务注册了哪些客户端应用程序。configure()方法接受一个名为clients的ClientDetailsServiceConfigurer类型的参数。让我们来更详细地了解一下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服务器获取访问令牌时可以操作的范围(也就是定义该应用程序能够在哪些场景里操作,比如手机端还是pc端)。例如,ThoughtMechanix可能提供同一应用程序的两个不同版本:基于Web的应用程序和基于手机的应用程序。在这些应用程序中都可以使用相同的客户端名称和密钥来请求对OAuth2服务器保护的资源的访问。然而,当应用程序请求一个密钥时,它们需要定义它们所操作的特定作用域。通过定义作用域,可以编写特定于客户端应用程序所工作的作用域的授权规则。
    例如,可能有一个用户使用基于Web的客户端和手机应用程序来访问EagleEye应用程序。EagleEye应用程序的每个版本都:
    (1)提供相同的功能;
    (2)是一个“受信任的应用程序”,ThoughtMechanix既拥有前端应用程序,也拥有终端用户服务。
    
因此,我们将使用相同的应用程序名称和密钥来注册EagleEye应用程序,但是Web应用程序只使用“webclient”作用域,而手机版本的应用程序则使用“mobileclient”作用域。通过使用作用域,可以在受保护的服务中定义授权规则,该规则可以根据登录的应用程序限制客户端应用程序可以执行的操作。这与用户拥有的权限无关。例如,我们可能希望根据用户是使用公司网络中的浏览器,还是使用移动设备上的应用程序进行浏览,来限制用户可以看到哪些数据。在处理敏感客户信息(如健康记录或税务信息)时,基于数据访问机制限制数据的做法是很常见的。
    到目前为止,我们已经使用OAuth2服务器注册了一个应用程序EagleEye。然而,因为使用的是密码授权,所以需要在开始之前为这些用户创建用户账户和密码。
 
7.2.3 配置EagleEye用户
    我们已经定义并存储了应用程序级的密钥名和密钥。现在要创建个人用户凭据及其所属的角色。用户角色将用于定义一组用户可以对服务执行的操作(角色是由平台定义的,比如QQ,谷歌,oauth验证服务和被保护的资源和服务都是引用平台定义的角色)。
    Spring可以从内存数据存储、支持JDBC的关系数据库或LDAP服务器中存储和检索用户信息(个人用户的凭据和分配给用户的角色)。
 
注意
    
    我希望在定义上谨慎一些。Spring的OAuth2应用程序信息可以存存储在内存或关系数据库中。Spring用户凭据和安全角色可以存储在内存数据库、关系数据库或LDAP(活动目录)服务器中。因为我们的主要目的是学习OAuth2,为了保持简单,我们将使用内存数据存储。
    
对于本章中的代码示例,我们将使用内存数据存储来定义用户角色。我们将定义两个用户账户,即john.carnell和william.woodward。john.carnell账户将拥有USER角色,而william.woodward账户将拥有ADMIN角色。
 
要配置OAuth2服务器以验证用户ID,必须创建一个新类WebSecurityConfigurer(在authentication-service/src/main/com/thoughtmechanix/authentication/security/WebSecurityConfigurer.java中)。
代码清单7-3展示了这个类的代码。
    
代码清单7-3 为应用程序定义用户ID、密码和角色
// 为了简洁,省略了import语句
⇽--- 扩展核心Spring Security的WebSecurityConfigurerAdapter
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    ←Spring Security使用AuthenticationManagerBean来处理身份验证。
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    ⇽--- Spring Security使用UserDetailsService处理返回的用户信息,这些用户信息将由Spring Security返回
   @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }
 
    ⇽--- configure()方法是定义用户、密码和角色的地方
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .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通过使用父类WebSecurityConfigurerAdapter中的默认验证方法authenticationManagerBean()和userDetailsServiceBean()方法来公开。
   
 从代码清单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来执行基本验证调用。
 
图7-2 使用应用程序名称和密钥设置基本验证
    
但是,我们还没有准备好执行调用来获取令牌。一旦配置了应用程序名称和密钥,就需要在服务中传递以下信息作为HTTP表单参数。
    grant_type——正在执行的OAuth2授权类型。在本例中,将使用密码(password)授权。
    scope——应用程序作用域。因为我们在注册应用程序时只定义了两个合法作用域(webclient和mobileclient),因此传入的值必须是这两个作用域之一。
    username——用户登录的名称。
    password——用户登录的密码。
    与本书中的其他REST调用不同,这个列表中的参数不会作为JSON体传递。OAuth2标准期望传递给令牌生成端点的所有参数都是HTTP表单参数。
 
图7-3展示了如何为OAuth2调用配置HTTP表单参数。
 
 
在本地测试的时候,使用postman发送请求的界面如下:
 
 
应用程序客户端的信息如下:
clients.inMemory()
.withClient("eagleeye")
.secret("thisissecret")
.authorizedGrantTypes("password", "client_credentials")
.scopes("webclient","mobileclient");
用户的信息如下:
auth.inMemoryAuthentication()
.withUser("john.carnell")
.password("password1")
.roles("USER")
.and()
.withUser("william.woodward")
.password("password2")
.roles("USER", "ADMIN");
 
从上面的截图可以看出,Basic Auth一栏中的 Username和 Password字段是客户端应用程序的信息,
这两个字段是作为header传送的。
注意使用postman的时候,要选择 x-www-form-urlencoded  ,而不能选择 form-data ,如下图,否则服务端会报错
  2019-01-29 20:45:56.504  INFO 2436 --- [nio-8901-exec-8] o.s.s.o.provider.endpoint.TokenEndpoint  : Handling error: InvalidRequestException, Missing grant type        
 
在本地测试的时候,使用postman第一次发送请求会弹出登录框,如下图:
用户名和密码要填写客户端应用程序的用户名和密码,即:
eagleeye和thisissecret
 
后面又仔细看了下,在发送请求之前,需要点击Basic Auth栏中的 Refresh headers    按钮,点了之后,页面如下所示:
 
header里多了一项“Authorization”,它的值是base64编码的。这样就不会弹出登录框了。
 
图7-3 在请求OAuth2令牌时,用户的凭据作为HTTP表单参数传入/auth/oauth/token端点
    
图7-4展示了从/auth/oauth/token调用返回的JSON净荷。
 
图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端点来检索与令牌相关联的用户的信息了(可以使用该端点验证OAuth2令牌是否有效和通过OAuth2令牌查找用户的信息)。在本章的后面,所有受保护资源都将调用验证服务的/auth/user端点来确认令牌并检
索用户信息。
    
图7-5展示了调用/auth/user端点的结果。如图7-5所示,注意OAuth2访问令牌是如何作为HTTP首部传入的。
 
 
图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.cloud</groupId>   
    <artifactId>spring-cloud-security</artifactId> 
    </dependency> 
    <dependency>   
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    </dependency>
 
7.3.2 配置服务以指向OAuth2验证服务
    记住,一旦将组织服务创建为受保护资源,每次调用服务时,调用者必须将包含OAuth2访问令牌的 Authentication HTTP首部包含到服务中(调用者必须将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 将引导类配置为受保护资源
// 为了简洁,省略了import语句
import org.springframework.security.oauth2.
@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 限制只有已通过验证的用户可以访问
// 为了简洁,省略了import语句
⇽--- 这个类ResourceServiceConfiguration类需要扩展ResourceServerConfigurerAdapter
@Configuration
public class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
⇽--- 所有访问规则都是在覆盖的configure()方法中定义的   
@Override
public void configure(HttpSecurity http) throws Exception{
⇽--- 所有访问规则都是通过传入方法的HttpSecurity对象配置的
http.authorizeRequests().anyRequest().authenticated();
}
}
所有的访问规则都将在configure()方法中定义。我们将使用由Spring传入的HttpSecurity类来定义规则。在本例中,我们将限制对组织服务中所有URL的访问,仅限已通过身份验证的用户才能访问。
如果在访问组织服务时没有在HTTP首部中提供OAuth2访问令牌,将会收到HTTP响应码401以及一条指示需要对服务进行完整验证的消息。
    
图7-6展示了在没有OAuth2 HTTP首部的情况下,对组织服务进行调用的输出结果。
图7-6 尝试调用组织服务将导致调用失败
    接下来,我们将使用OAuth2访问令牌调用组织服务。要获取访问令牌,需要阅读7.2.4节,了解如何生成OAuth2令牌。我们需要将access_token字段的值从对/auth/oauth/token端点调用所返回的JSON调用结果中剪切出来,并在对组织服务的调用中粘贴使用它。记住,在调用组织服务时,需要添加一个名为Authorization的HTTP首部,其值为Bearer access_token。
    图7-7展示了对组织服务的调用,但是这次使用了传递给它的OAuth2访问令牌。
 
图7-7 在对组织服务的调用中传入OAuth2访问令牌
    这可能是使用OAuth2保护端点的最简单的用例之一。接下来,我们将在此基础上进行构建,并将对特定端点的访问限制在特定角色。
    2.通过特定角色保护服务
    在接下来的示例中,我们将锁定组织服务的DELETE调用,仅限那些具有ADMIN访问权限的用户。正如7.2.3节中介绍过的,我们创建了两个可以访问EagleEye服务的用户账户,即john.carnell和william.woodward。john.carnell账户拥有USER角色,而william.woodward账户拥有USER和ADMIN角色。
 
代码清单7-6展示了如何创建configure()方法来限制对DELETE端点的访问,使得只有那些已通过验证并具有ADMIN角色的用户才能访问。
 
代码清单7-6 限制只有ADMIN角色可以进行删除
// 为了简洁,省略了import语句
@Configuration
public class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception{
http
⇽--- authorizeRequests()表示验证的请求
.authorizeRequests()
⇽--- antMatchers()允许开发人员限制对受保护的URL和HTTP DELETE动词的调用
.antMatchers(HttpMethod.DELETE, "/v1/organizations/**")
⇽---  hasRole()方法是一个逗号分隔的可访问角色列表。
.hasRole("ADMIN")
⇽--- 下面表示任何请求都需要被验证
.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网关、许可证服务然后到达组织服务的基本流程。
 
图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首部(如Cookie、Set-Cookie和Authorization)转发到下游服务。要让Zuul传播HTTP首部Authorization,需要在Zuul服务网关的application.yml或Spring Cloud Config数据存储中设置以下配置:
    
zuul.sensitiveHeaders: Cookie,Set-Cookie
    
这一配置是黑名单,它包含Zuul不会传播到下游服务的敏感首部。在上述黑名单中没有Authorization值就意味着Zuul将允许它通过。如果根本没有设置zuul.sensitive-Headers属性,Zuul将自动阻止3个值(Cookie、Set-Cookie和Authorization)被传播。
 
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/OrganizationRestTemplateClient.java类。
 
代码清单7-7展示了OAuth2RestTemplate是如何自动装配到这个类中的。   
 
代码清单7-7 使用OAuth2RestTemplate来传播OAuth2访问令牌
// 为了简洁,省略了import语句
@Component
public class OrganizationRestTemplateClient {
     ⇽--- OAuth2RestTemplate是标准RestTemplate的增强式替代品,可处理OAuth2访问令牌的传播
    @Autowired
    OAuth2RestTemplate restTemplate;
 
    private static final Logger logger = LoggerFactory.getLogger(OrganizationRestTemplateClient.class);
 
    public Organization getOrganization(String organizationId){
        logger.debug("In Licensing Service.getOrganization: {}", UserContext.getCorrelationId());
        ⇽--- 调用组织服务的方式与标准的RestTemplate完全相同     
        ResponseEntity<Organization> restExchange =
                restTemplate.exchange(
                        "http://zuulserver:5555/api/organization/v1/organizations/{organizationId}",
                        HttpMethod.GET,
                        null, Organization.class, organizationId);
 
        return restExchange.getBody();
    }
}
 
 
posted @ 2019-12-03 10:53  mongotea  阅读(377)  评论(0编辑  收藏  举报