分享免费的编程资源和教程

网站首页 > 技术教程 正文

Spring Security 配置授权(1):限制访问-定义权限和角色

goqiw 2024-10-02 21:57:17 技术教程 26 ℃ 0 评论

本文导读

  • 定义权限和角色
  • 在端点上应用授权规则

几年前,目睹了这个有趣的场景。大约有十到十五个人排队等着进小屋去滑雪坡顶。一位著名的流行艺术家在两名保镖的陪同下出现了。他自信地大步走了过来,以为自己很有名就可以不用排队了。当他走到队伍的前头时,他大吃一惊。 管理登机的人说: “请出示票!",然后他不得不解释,“嗯,你首先需要一张票,其次,这次登机没有优先排队,抱歉。” 队伍到这里就结束了。”他指了指队伍的末尾。在生活中的大多数情况下,你是谁并不重要。对于软件应用程序,我们也可以这样说。当您试图访问特定的功能或数据时,您是谁并不重要!

到目前为止,我们只讨论了身份认证,正如您所了解的,身份认证是应用程序标识资源调用者的过程。在前面文章的示例中,我们没有实现任何决定是否批准请求的规则。我们只关心系统是否认识用户。在大多数应用程序中,并不是所有被系统识别的用户都可以访问系统中的每个资源。在本文中,我们将讨论授权。Authorization ( 授权 ) 是系统决定已识别的客户端是否具有访问请求资源的权限的过程 ( 图 1 )。

授权是应用程序决定是否允许经过身份认证的实体访问资源的过程。授权总是在身份认证之后进行。

在 Spring Security 中,应用程序结束身份验证流程后,便将请求委派给授权过滤器。 过滤器根据配置的授权规则允许或拒绝请求( 图 2 )。

为了包含授权的所有基本细节,在本文中,我们将按照以下步骤操作:

  1. 了解权限是什么,并根据用户的权限在所有端点上应用访问规则。
  2. 了解如何在角色中对权限进行分组,以及如何基于用户的角色应用授权规则。



当客户端发出请求时,认证过滤器对用户进行认证。身份认证成功后,身份认证过滤器将用户详细信息存储在安全上下文中,并将请求转发给授权过滤器。授权过滤器决定是否允许调用。要决定是否授权请求,授权过滤器将使用来自安全上下文的详细信息。

在后面一篇文章中,我们将继续将讨论授权规则应用到的端点。现在,让我们看看权限和角色,以及它们如何限制对应用程序的访问。

基于权限和角色限制访问

在本节中,您将了解授权和角色的概念。您可以使用这些来保护应用程序的所有端点。您需要理解这些概念,然后才能在实际场景中应用它们,在实际场景中,不同的用户拥有不同的权限。根据用户拥有的特权,他们只能执行特定的操作。应用程序以权限和角色的形式提供特权。

在前面的文章中,您实现了 GrantedAuthority 接口。在讨论另一个基本组件: UserDetails 接口时,我介绍了这个接口。我们当时没有使用 GrantedAuthority ,因为正如您将在本文学到的,这个接口主要与授权流程相关。现在我们可以回到 “GrantedAuthority ” 来研究它的目的。图 3 展示了 UserDetails 接口和 GrantedAuthority 接口之间的关系。一旦我们完成了对接口的讨论,您将学习如何单独或针对特定请求使用这些规则。


用户具有一个或多个权限(用户可以执行的操作)。 在身份认证过程中,UserDetailsService 获取有关用户的所有详细信息,包括权限。 在成功认证用户身份之后,该应用程序将使用GrantedAuthority 接口所表示的权限进行授权。

清单 1 显示了 GrantedAuthority 接口的定义。权限是用户可以使用系统资源执行的操作。一个权限有一个名称,对象的 getAuthority() 行为将其作为 String 返回。我们在定义自定义授权规则时使用权限的名称。授权规则通常是这样的: “Jane 被允许删除产品记录”,或者 “Tom被允许读取文档记录”。在这些情况下,delete 和 read 是授予的权限。该应用程序允许用户 Jane 和 Tom 执行这些操作,这些操作的名称通常是 read、write 或 delete。

清单 1 GrantedAuthority 接口

public interface GrantedAuthority extends Serializable {
  String getAuthority();
}

UserDetails 是 Spring Security 中描述用户的接口,它拥有 GrantedAuthority 实例集合,如图 3 所示。可以允许一个用户拥有一个或多个权限。getAuthorities() 方法返回 GrantedAuthority 实例的集合。在清单 2 中,您可以在 UserDetails 接口中查看这个方法。我们实现这个方法,以便它返回授予用户的所有权限。身份认证结束后,权限是关于登录用户的详细信息的一部分,应用程序可以使用它来授予权限。

清单 2 来自 UserDetails 契约的 getAuthorities() 方法

public interface UserDetails extends Serializable {
  Collection<? extends GrantedAuthority> getAuthorities();

  // Omitted code
}


根据用户权限限制所有端点的访问

在本节中,我们将讨论限制指定用户对端点的访问。到目前为止,在我们的示例中,任何经过身份认证的用户都可以调用应用程序的任何端点。从现在开始,您将学习如何定制这种访问。在您生产环境中找到的应用程序中,即使您没有经过身份认证,也可以调用应用程序的某些端点,而对于其他应用程序,则需要特殊权限 ( 图 4 )。我们将编写几个示例,以便您了解在 Spring Security 中应用这些限制的各种方法。


权限是用户可以在应用程序中执行的操作。基于这些操作,可以实现授权规则。只有具有特定权限的用户才能向端点发出特定请求。例如,Jane 只能读取和写入端点,而 Tom 可以读取、写入、删除和更新端点。

现在您已经记住了 UserDetails GrantedAuthority 接口以及它们之间的关系,现在可以编写一个应用授权规则的小应用程序了。通过这个示例,您将了解一些基于用户权限配置对端点访问的替代方法。我们开始一个新项目。我将向您展示三种配置访问的方法,如前所述,使用以下方法:

  • hasAuthority() -- 仅接收应用程序为其配置限制的一个授权作为参数。只有具有该权限的用户才能调用端点。
  • hasAnyAuthority() -- 可以接收应用程序为其配置限制的多个权限。 我记得这种方法是“具有任何给定的权限”。 用户必须至少具有指定的权限之一才能发出请求。为了简单起见,我建议使用这个方法或 hasAuthority() 方法,具体取决于您为用户分配的特权的数量。这些配置很容易阅读,并使您的代码更容易理解。
  • access() -- 由于应用程序基于 Spring Expression Language(SpEL)构建授权规则,因此为您提供了配置访问的无限可能。 但是,这会使代码更难以阅读和调试。 因此,仅当您无法应用 hasAnyAuthority() hasAuthority() 方法时,才建议将其作为次要解决方案。

pom.xml 文件中仅仅需要的依赖项是 spring-boot-starter-webspring-boot-starter-security 。 这些依赖关系足以解决前面列举的所有三个解决方案。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

我们还在应用程序中添加了一个端点来测试我们的授权配置:

@RestController
public class HelloController {
    
  @GetMapping("/hello")
  public String hello() {
    return "Hello!";
  }
}

在配置类中,我们将 InMemoryUserDetailsManager 声明为 UserDetailsService ,并添加两个要由该实例管理的用户 Tom 和 Jane。 每个用户都有不同的权限。 您可以在下面的清单中查看如何执行此操作。

清单 3 声明 UserDetailsService 并分配用户

@Configuration
public class ProjectConfig {

  // 返回的 UserDetailsService 被添加到 SpringContext 中。
  @Bean
  public UserDetailsService userDetailsService() {
    // 声明一个存储两个用户的 InMemoryUserDetailsManager
    var manager = new InMemoryUserDetailsManager();

    // 第一个用户 tom 具有 READ 权限
    var user1 = User.withUsername("tom")
                    .password("12345")
                    .authorities("READ")
                    .build();
    // 第一个用户 jane 具有 WRITE 权限
    var user2 = User.withUsername("jane")
                    .password("12345")
                    .authorities("WRITE")
                    .build();

    // 用户由 UserDetailsService 添加和管理。
    manager.createUser(user1);
    manager.createUser(user2);

    return manager;
  }

  // 别忘了还需要一个 PasswordEncoder。
  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance(); 
  }
}

接下来要做的是添加授权配置。 在前面的文章中,当我们研究第一个示例时,您了解了如何使所有人都能访问所有端点。 为此,您扩展了 WebSecurityConfigurerAdapter 类并覆盖了 configure() 方法,与在下一个清单中看到的类似。

清单 4 使所有端点都可以在无需身份认证的情况下访问

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  // Omitted code

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();
        
    http.authorizeRequests()
          .anyRequest().permitAll(); // 允许访问所有请求
    }
}

authorizeRequests() 方法允许我们继续在端点上指定授权规则。anyRequest() 方法表示该规则适用于所有请求,而不考虑使用的 URL 或 HTTP 方法。permitAll() 方法允许访问所有请求,无论是否经过身份认证。

假设我们希望确保只有具有 WRITE 权限的用户才能访问所有端点。在我们的例子中,这意味着只有 Jane。我们可以实现我们的目标,并根据用户的权限限制这次的访问。看一下下面清单中的代码。

清单 5 限制仅对具有 WRITE 权限的用户进行访问

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  // Omitted code

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();

    http.authorizeRequests()
         .anyRequest()
          .hasAuthority("WRITE"); // 指定用户访问端点的条件
  }
}

您可以看到,我用hasAuthority() 方法替换了 permitAll() 方法。您将允许用户使用的权限名称作为 hasAuthority() 方法的参数。应用程序首先需要对请求进行身份认证,然后根据用户的权限决定是否允许调用。

现在,我们可以通过调用两个用户中的每个端点来测试应用程序。当我们调用用户 Jane 的端点时,HTTP 响应状态是 200 OK,并且我们看到响应体 “Hello!” 当我们用用户 Tome 调用它时,HTTP 响应状态是 403 Forbidden,并且返回一个空响应体。例如,用用户 Jane 调用这个端点,

curl -u jane:12345 http://localhost:8080/hello

我们得到这个响应:

Hello!

使用用户 Tom 调用端点,

curl -u tom:12345 http://localhost:8080/hello

我们得到这个响应:

{
  "status":403,
  "error":"Forbidden",
  "message":"Forbidden",
  "path":"/hello"
}

以类似的方式,您可以使用 hasAnyAuthority() 方法。这个方法有参数 varargs;通过这种方式,它可以接收多个权限名称。如果用户至少有其中一个权限作为参数提供给该方法,则应用程序允许该请求。您可以用 hasAnyAuthority("WRITE") 替换前面清单中的 hasAuthority() ,在这种情况下,应用程序以完全相同的方式工作。但是,如果您将 hasAuthority() 替换为 hasAnyAuthority ("WRITE", "READ") ,那么来自具有这两种权限的用户的请求都会被接受。对于我们的示例,应用程序允许来自 Tom 和 Jane 的请求。在下面的清单中,您可以看到如何应用 hasAnyAuthority() 方法。

清单 6 应用 hasAnyAuthority() 方法

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  // Omitted code

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();

    http.authorizeRequests()
          .anyRequest()
            .hasAnyAuthority("WRITE", "READ");
  }
}

您现在可以通过我们的两个用户中的任何一个成功地调用端点。这是 Tome 的调用请求:

curl -u tom:12345 http://localhost:8080/hello

响应体:

Hello!

Jane 的调用请求:

curl -u jane:12345 http://localhost:8080/hello

响应体:

Hello!

要根据用户权限指定访问权限,您在实践中找到的第三种方法是 access() 方法。然而,access() 方法更为通用。它接收指定授权条件的 Spring 表达式 ( SpEL ) 作为参数。这个方法很强大,而且它不仅涉及权限。然而,这种方法也使代码更难阅读和理解。出于这个原因,我建议将其作为最后一个选项,并且只有在您不能应用本节前面介绍的 hasAuthority()hasAnyAuthority() 方法之一的情况下。

为了使此方法更容易理解,我首先将其作为使用 hasAuthority() hasAnyAuthority() 方法指定权限的替代方法。正如您在本例中所了解的,您必须提供一个 Spring 表达式作为方法的参数。我们定义的授权规则变得更加难以阅读,这就是为什么我不建议对简单规则使用这种方法的原因。但是,access() 方法的优点是允许您通过作为参数提供的表达式自定义规则。这真的很强大!与 SpEL 表达式一样,您基本上可以定义任何条件。

注意

在大多数情况下,可以使用 hasAuthority() 和 hasAnyAuthority() 方法实现所需的限制,我建议您使用这些方法。 仅当其他两个选项都不适合并且您要实现更多通用授权规则时,才使用 access() 方法。

我从一个简单的示例开始,以匹配与先前案例相同的要求。 如果仅需要测试用户是否具有特定权限,则需要与 access() 方法一起使用的表达式可以是以下之一:

  • hasAuthority('WRITE') -- 规定用户需要 WRITE 权限才能调用端点。
  • hasAnyAuthority('READ', 'WRITE') -- 指定用户需要 READ WRITE 权限之一。使用此表达式,您可以枚举希望允许访问的所有权限。

请注意,这些表达式与本节前面介绍的方法具有相同的名称。 以下清单演示了如何使用 access() 方法。

清单 7 使用 access() 方法配置对端点的访问

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  // Omitted code

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();

    http.authorizeRequests()
          .anyRequest()
            .access("hasAuthority('WRITE')");
  }
}

清单 7 中的示例证明了如果您将 access() 方法用于简单的需求,它将如何使语法变得复杂。在这种情况下,您应该直接使用 hasAuthority()hasAnyAuthority() 方法。但是 access() 方法也不全是坏事。如前所述,它为您提供了灵活性。在实际场景中,您将发现可以使用它编写更复杂的表达式,应用程序将根据这些表达式授予访问权。如果没有 access() 方法,您将无法实现这些场景。

在清单 8 中,您会发现 access() 方法应用了一个表达式,否则很难编写该表达式。确切地说,清单 8 中的配置定义了两个用户,清单 Tom 和 Jane,他们拥有不同的权限。用户 Tome 只有读权限,而Jane有读、写和删除权限。具有读取权限的用户应该可以访问端点,而具有删除权限的用户则不能访问端点。

注意

在 Spring 应用程序中,您可以找到用于权限命名的各种样式和约定。 一些开发人员全部使用大写字母,其他开发人员全部使用小写字母。 我认为,所有这些选择都是可以的,只要您在应用程序中保持一致即可。 在本书中,我在示例中使用了不同的样式,因此您可以观察到在现实世界中可能遇到的更多方法。

当然,这只是一个假设的示例,但是它足够简单,容易理解,也足够复杂,可以证明为什么access() 方法更强大。用 access() 方法实现这一点,可以使用反映需求的表达式。例如:

"hasAuthority('read') and !hasAuthority('delete')"

下一个清单说明了如何使用更复杂的表达式应用 access() 方法。

清单 8 将 access() 方法应用于更复杂的表达式

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Bean
  public UserDetailsService userDetailsService() {
    var manager = new InMemoryUserDetailsManager();

    var user1 = User.withUsername("tom")
            .password("12345")
            .authorities("read")
            .build();

    var user2 = User.withUsername("jane")
            .password("12345")
            .authorities("read", "write", "delete")
            .build();

    manager.createUser(user1);
    manager.createUser(user2);

    return manager;
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }

  @Override
  protected void configure(HttpSecurity http) 
    throws Exception {

    http.httpBasic();

      // 指出用户必须具有读取权限但没有删除权限
    String expression = "hasAuthority('read') and !hasAuthority('delete')";

    http.authorizeRequests()
         .anyRequest()
         .access(expression);
  }
}

现在让我们通过调用用户 清单Tom 的 /hello 端点来测试我们的应用程序:

curl -u tom:12345 http://localhost:8080/hello

响应体:

Hello!

并使用用户 Jane 调用端点:

curl -u jane:12345 http://localhost:8080/hello

响应体:

{
    "status":403,
    "error":"Forbidden",
    "message":"Forbidden",
    "path":"/hello"
}

用户 Tom 只有读取权限,可以成功调用端点。但是Jane也有删除权限,没有权限调用端点。Jane的 HTTP 状态为 403 禁止。

用户需要访问一些指定的端点。当然,我们还没有讨论如何选择基于路径或 HTTP 方法保护哪些请求。相反,我们已经为所有请求应用了规则,而不管应用程序公开的端点是什么。完成用户角色的相同配置后,我们将讨论如何选择将授权配置应用到的端点。

下一篇我们来讨论在端点上应用授权规则。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表