如何利用OAuth2协议实现分布式授权?

今天,我将和你一起分析在微服务系统中,如何利用OAuth2协议实现服务访问授权。让我们先来考虑这样一个场景。在微服务系统中,存在一个服务A。我们知道,在微服务系统中,每个服务都具有自己的业务边界和数据。所以,每个服务中的功能并不一定是全部对外开放,也就是说,并不是每一个服务都可以无条件的访问服务A。那么,如果我们站在服务A的角度看,它怎么判断一个HTTP请求具备访问自己的权限呢?这是我们需要解决的第一个问题。


更进一步,就算一个请求具有访问服务A的权限,但并不意味着它能够访问服务A中的所有功能。对于某些核心功能,需要具备较高的权限才能访问,而有些则不需要。这就是我们需要解决的第二个问题,也就是说,如何对服务访问的权限进行精细化管理?


现在,我们已经明确了本课程需要解决的问题,这个问题本质上就是一个授权问题。既然是授权问题,我们就不难想象在服务A之前应该存在一个授权层。这种授权层的构建也有很多方法,而在微服务架构中,目前最主流的技术体系就是使用OAuth2协议。我们先来对OAuth2协议做一个简单的介绍。

OAuth是Open Authorization的简称,面向分布式场景下开放和消费第三方接口,该协议解决的是授权问题而不是认证问题。为了理解OAuth 2协议,我们有必要先解释一下其中的几个专用名词。

首先,OAuth2协议中把需要访问的接口或服务统称为资源(Resource),而每个资源都有一个拥有者(Resource Owner)。这个资源拥有者所拥有的资源统一存放在资源服务器(Resource Server)中。同时,协议规定需要有一台授权服务器(Authorization Server),即专门用来处理对访问请求进行授权的服务器。

OAuth2协议的作用就是让客户端程序安全可控地获取用户的授权信息,并与资源服务器进行交互。OAuth2协议在客户端程序和资源服务器之间设置了一个授权层,所以客户端程序不能直接访问资源服务器,而是只能先登录授权层。资源拥有者会首先同意给客户端授权,客户端使用获得的授权,向认证服务器申请一个Token,Token中就包含了权限范围和有效期。然后,客户端使用Token,向资源服务器申请获取资源,资源服务器就根据Token的权限范围和有效期向客户端开放拥有者的资源。OAuth 2.0的基本运行流程如下图所示。


把这一流程应用到微服务架构的场景中,显然作为服务提供者的服务A,其所充当的角色就是资源服务器,而发出HTTP请求的服务就是客户端。事实上,各个服务本身都可以是客户端,也可以作为资源服务器,或者两者兼之。当客户端拿到Token之后,该Token就能在各个服务之间进行传递。


介绍完OAuth2协议的基本流程之后,我们需要明确它所提供的几种授权模式。OAuth2提供了4中授权模式,包括授权码模式(Authorization Code)、简化模式(Implicit)、密码模式(Resource Owner Password Credentials)和客户端模式(Client Credentials)。

这里,我们以比较简单也比较容易理解的密码模式为例进行展开。在密码模式下,用户向客户端提供用户名和密码,并将用户名和密码发给授权服务器并请求Token,然后授权服务器确认无误后,向客户端发放Token。

OAuth2协议的流程看起来虽然并不是很复杂,在实现上难度很大,需要综合考虑加密、授权、认证、敏感信息存储等一系列技术要点。幸好在Spring家族提供了Spring Security和Spring Cloud Security框架来简化在业务系统中集成OAuth2协议的实现过程。接下来,我们就使用这两个框架来完成OAuth2服务授权流程的实现。整个开发流程需要涉及两个维度,一方面我们需要构建一个独立的授权服务并完成用户和客户端信息的初始化,另一方面,每个微服务也需要完成与授权服务之间的集成。

我们先来看第一个维度,即构建独立的授权服务。授权服务的构建并不简单,需要有四个步骤,分别是构建授权服务器、初始化用户、初始化客户端和生成Token。


第一步是授权服务器的构建,需要我们创建一个独立的Spring Boot工程,并在启动类上添加@EnableAuthorizationServer注解

@SpringBootApplication 

@RestController

@EnableAuthorizationServer

public class AuthorizationApplication {

    public static void main(String[] args) {

        SpringApplication.run(AuthorizationApplication.class, args);

    }

}

这个@EnableAuthorizationServer注解的作用在于为微服务运行环境提供一个基于OAuth2协议的授权服务,该服务通过暴露一系列以/oauth为根节点的HTTP端点供OAuth2授权流程进行使用,例如用于生成Token的/oauth/token端点,用于授权的/oauth/authorize端点。

在构建了授权服务器之后,接下来就需要初始化密码模式下的用户和客户端信息。基于Spring Security框架,初始化用户操作所依赖的配置类是WebSecurityConfigurer类。但一般我们不需要直接操作这个配置类,而是使用框架所提供的适配器类WebSecurityConfigurerAdapter来简化该配置类的使用方式。我们可以继承WebSecurityConfigurerAdapter类并且覆写其中的configure()的方法来完成配置工作。

@Configuration

public class MyWebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Override

    protected void configure(AuthenticationManagerBuilder builder) throws Exception {

        builder.inMemoryAuthentication()

                .withUser("user1").password("password1")

.roles("USER")

                .and()

                .withUser("user2").password("password2")

.roles("ADMIN");

 }

}

这里,我们构建了"user1"和"user2"这两个用户,其密码分别是"password1"和"password2",在角色上也分别代表着普通用户USER以及管理员ADMIN。

初始化客户端也是依赖于一个配置类AuthorizationServerConfigurer。同样,我们在日常开发中,通常也是通过覆写框架提供的AuthorizationServerConfigurerAdapter类的configure()方法来简化开发过程

@Configuration

public class MyAuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

    @Override

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.inMemory()

                .withClient("myclient")

                .secret("mysecret")

                .authorizedGrantTypes("password", "refresh_token").scopes("webclient")

;

    }

}

注意到这里需要指定客户端名称、密码以及授权类型,其中的授权类型可以指定OAuth2协议所提供的四种类型中的任意一种,显然这里指定的是密码授权模式。而"refresh_token"则用于刷新操作,以便获取该授权模式下所产生的新的令牌。至于最后的"scope"配置项则用来限制客户端的访问范围,这里指定为Web端访问。

当授权服务器启动完毕,我们就可以使用http://localhost:8080/oauth/token端点来获取Token。为了演示生成Token的过程和结果,本课程使用Postman来模拟HTTP请求。在Postman中,我们首先在请求头"Authorization"中指定认证类型为“Basic Auth”,然后设置客户端名称和密码分别为“myclient”和“mysecret”。


接下来,我们在请求的"Body"部分中指定针对密码授权模式的专用配置信息,包括授权类型grant_type、授权范围scope、用户名username和密码password,这里依次填入已初始化的用户信息即可。


我们通过Postman发起这个请求,会得到http://localhost:8080/oauth/token端点的返回结果,除了作为请求参数的scope之外,我们看到在消息体中包含了access_token、token_type、refresh_token和expires_in属性。

{

    "access_token": "b7c2c7e0-0223-40e2-911d-eff82d125b80",

    "token_type": "bearer",

    "refresh_token": "40ee99d5-90f6-43ce-920f-383a619fc806",

    "expires_in": 43199,

    "scope": "webclient"

}

其中access_token 就是OAuth2 token,当访问每个受保护的服务时,用户都需要携带该Token以便进行验证;针对token_type,在OAuth2协议中存在很多种Token类型可供选择,包括bearer类型、mac类型等,这里返回的是最常见的一种类型,即bearer类型;refresh_token的作用在于当access_token 过期之后,用于重新下发一个新的access_token;expires_in属性用于指定access_token的有效时间,当超过这个有效时间时,access_token将会自动失效。至此,我们已经成功获取了可用于访问各个服务的Token信息。接下来,我们将演示如何使用该Token。

我们知道在OAuth2协议中,诸如服务A这样的单个微服务的定位就是资源服务器,Spring Security框架为此提供了专门的@EnableResourceServer注解。通过在Spring Boot的Bootstrap类上添加@EnableResourceServer注解,相当于就是声明了该服务中的所有内容都是受保护的资源:

@SpringBootApplication

@EnableResourceServer

public class TestApplication {

    public static void main(String[] args) {

        SpringApplication.run(TestApplication.class, args);

    }

}

在添加了@EnableResourceServer注解之后,微服务会对所有的HTTP请求进行验证以确定消息头中是否包含Token信息,如果没有Token信息,则会直接限制访问;如果有Token信息,就会访问授权服务器并进行Token的验证。

同时,作为资源服务器,每个微服务对于自身资源的保护粒度并不是固定的,而是可以根据需求进行控制。在日常开发过程中,我们可以使用三种不同的控制粒度,分别是只要用户层级、用户+角色层级以及用户+角色+请求方法层级。

为了实现各个层级的控制,我们的做法同样是继承一个ResourceServerConfigurerAdapter类并覆写它的configure方法,注意到这个方法的入参是一个HttpSecurity对象:

@Configuration

public class MyResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override

    public void configure(HttpSecurity httpSecurity) throws Exception{

        httpSecurity.authorizeRequests()

             .anyRequest()

             .authenticated();

    }

}

现在,如果我们访问该服务的任何HTTP端点,系统都会抛出一段错误信息“Full authentication is required to access this resource”。解决办法就是在HTTP请求中设置“Authorization”请求头并传入Token信息。注意,因为我们使用的是bearer类型的Token,所以需要在access_token的具体值之前加上“bearer”前缀。


当然,如果我们随意输入一个无效的Token,系统会抛出“invalid_token”错误。

对于某些安全性要求比较高的资源,我们不应该开放资源访问入口给所有的认证用户,而是需要限定访问资源的角色,比方说限定只有角色为“ADMIN”的管理员才能访问该服务中的资源。要想达到这种效果,实现方式也比较简单,就是在HttpSecurity 中通过antMatchers()和hasRole()方法指定想要限制的资源和角色。

@Configuration

public class MyResourceServerConfiguration extends 

ResourceServerConfigurerAdapter{

    @Override

public void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity.authorizeRequests()

                .antMatchers("/myresource/**")

                .hasRole("ADMIN")

                .anyRequest()

                .authenticated();

    }

}

在这个示例中,我们规定"/myresource/"端点下的所有资源只有ADMIN角色才能访问。联想到前面我们初始化用户时的示例,如果这时候使用角色为“USER”的“user1”用户来访问这个端点,那么将得到“access_denied”错误。只有基于角色为“ADMIN”的“user2”用户所生成的Token才能访问这一端点中的资源。

更进一步,我们还可以针对某个端点的某个具体HTTP方法进行控制。例如,如果"/myresource/"端点下的资源进行更新的风险很高,那么就可以在HttpSecurity的antMatchers()中添加HttpMethod.UPDATE限定。

@Configuration

public class MyResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override

    public void configure(HttpSecurity httpSecurity) throws Exception{

        httpSecurity.authorizeRequests()

                .antMatchers(HttpMethod.UPDATE, "/myresource/**")

                .hasRole("ADMIN")

                .anyRequest()

                .authenticated();

    }

}

至此,关于使用OAuth2协议来对服务访问进行授权管理的实现过程就介绍完毕了。最后,作为总结,我们来梳理一下与OAuth2协议和服务访问授权相关的各个知识点。我们首先介绍了微服务系统中服务访问授权的需求和必要性。然后,我们引出了OAuth2协议这一在处理微服务访问授权是非常常用的一款安全性协议,介绍了OAuth2协议本身的工作流程、所具备的授权模式以及与微服务架构的集成方式。然后,我们基于Spring框架来完成独立授权服务的构建以及与微服务的集成,我们对这其中的实现步骤都做了详细展开。

在使用Spring Cloud等框架开发微服务系统时,我们可以使用OAuth2协议对服务访问进行授权控制。总体而言,服务访问授权是一个比较复杂的过程,实现上也有很多可以扩展的点。例如,在初始化用户和客户端的场景中,课程中演示的都是基于内存存储的数据持久化方案,我们在开发过程中可以根据需要采用关系型数据库等更为灵活和扩展的实现方案。

相关推荐

  1. Spring 实现 OAuth2 授权之解决方案

    2024-05-16 13:08:09       37 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-16 13:08:09       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-16 13:08:09       106 阅读
  3. 在Django里面运行非项目文件

    2024-05-16 13:08:09       87 阅读
  4. Python语言-面向对象

    2024-05-16 13:08:09       96 阅读

热门阅读

  1. 一步步教您轻松搭建YOLO训练环境(视频教程)

    2024-05-16 13:08:09       27 阅读
  2. git命令使用

    2024-05-16 13:08:09       34 阅读
  3. iOS 数据库升级

    2024-05-16 13:08:09       30 阅读
  4. LeetCode 22. 括号生成

    2024-05-16 13:08:09       53 阅读
  5. 循环、使用dict和set

    2024-05-16 13:08:09       34 阅读
  6. 使用 Gradle 自定义任务生成初始化 SQL 文件

    2024-05-16 13:08:09       38 阅读
  7. 数学建模(科普)

    2024-05-16 13:08:09       38 阅读
  8. IT行业的现状与未来:技术驱动下的新世界

    2024-05-16 13:08:09       39 阅读
  9. js 数组filter使用

    2024-05-16 13:08:09       34 阅读
  10. RIP、OSPF、BGP等协议及华为路由器配置总结

    2024-05-16 13:08:09       37 阅读