网站首页 > 技术教程 正文
本文导读
- 定义权限和角色
- 在端点上应用授权规则
几年前,目睹了这个有趣的场景。大约有十到十五个人排队等着进小屋去滑雪坡顶。一位著名的流行艺术家在两名保镖的陪同下出现了。他自信地大步走了过来,以为自己很有名就可以不用排队了。当他走到队伍的前头时,他大吃一惊。 管理登机的人说: “请出示票!",然后他不得不解释,“嗯,你首先需要一张票,其次,这次登机没有优先排队,抱歉。” 队伍到这里就结束了。”他指了指队伍的末尾。在生活中的大多数情况下,你是谁并不重要。对于软件应用程序,我们也可以这样说。当您试图访问特定的功能或数据时,您是谁并不重要!
到目前为止,我们只讨论了身份认证,正如您所了解的,身份认证是应用程序标识资源调用者的过程。在前面文章的示例中,我们没有实现任何决定是否批准请求的规则。我们只关心系统是否认识用户。在大多数应用程序中,并不是所有被系统识别的用户都可以访问系统中的每个资源。在本文中,我们将讨论授权。Authorization ( 授权 ) 是系统决定已识别的客户端是否具有访问请求资源的权限的过程 ( 图 1 )。
授权是应用程序决定是否允许经过身份认证的实体访问资源的过程。授权总是在身份认证之后进行。
在 Spring Security 中,应用程序结束身份验证流程后,便将请求委派给授权过滤器。 过滤器根据配置的授权规则允许或拒绝请求( 图 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-web 和 spring-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 方法保护哪些请求。相反,我们已经为所有请求应用了规则,而不管应用程序公开的端点是什么。完成用户角色的相同配置后,我们将讨论如何选择将授权配置应用到的端点。
下一篇我们来讨论在端点上应用授权规则。
猜你喜欢
- 2024-10-02 网络请求返回HTTP状态码(404,400,500)
- 2024-10-02 那些代表性的HTTP状态码,你还只知道404吗?快来看看吧
- 2024-10-02 重温HTTP,你到底做了什么? 重温张柏芝引用2008
- 2024-10-02 最近发现室友看电脑鬼鬼祟祟,利用python几行代码窥探室友电脑
- 2024-10-02 Kong Gateway 身份验证 gateway登录验证
- 2024-10-02 如何正确认识 HTTP 正确认识自己心理健康教案
- 2024-10-02 HTTP请求状态码 请求失败状态码为412
- 2024-10-02 一文读懂HTTP常见状态码 http常见状态码
- 2024-10-02 http 请求方法以及返回状态码的类型和含义
- 2024-10-02 HTTP状态码常见的网站错误代码大全
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- sd分区 (65)
- raid5数据恢复 (81)
- 地址转换 (73)
- 手机存储卡根目录 (55)
- tcp端口 (74)
- project server (59)
- 双击ctrl (55)
- 鼠标 单击变双击 (67)
- debugview (59)
- 字符动画 (65)
- flushdns (57)
- ps复制快捷键 (57)
- 清除系统垃圾代码 (58)
- web服务器的架设 (67)
- 16进制转换 (69)
- xclient (55)
- ps源文件 (67)
- filezilla server (59)
- 句柄无效 (56)
- word页眉页脚设置 (59)
- ansys实例 (56)
- 6 1 3固件 (59)
- sqlserver2000挂起 (59)
- vm虚拟主机 (55)
- config (61)
本文暂时没有评论,来添加一个吧(●'◡'●)