在本教程中,我们将讨论基于角色的访问控制 (RBAC) 以及如何使用Quarkus实现此功能。
RBAC 是一种众所周知的用于实现复杂安全系统的机制。 Quarkus 是一个现代云原生全栈 Java 框架,支持开箱即用的 RBAC。
在开始之前,请务必注意角色可以通过多种方式应用。在企业中,角色通常只是用于标识用户可以执行的特定操作组的权限的聚合。在Jakarta中,角色是允许执行资源操作(相当于权限)的标签。实现 RBAC 系统有不同的方法。
在本教程中,我们将使用分配给资源的权限来控制访问,并且角色将对权限列表进行分组。
角色控制RBAC
基于角色的访问控制是一种安全模型,它根据预定义的权限授予应用程序用户访问权限。系统管理员可以在尝试访问时向特定资源分配和验证这些权限。
为了演示使用 Quarkus 实现 RBAC 系统,我们需要一些其他工具,例如 JSON Web Tokens (JWT)、JPA 和 Quarkus 安全模块。 JWT 帮助我们实现一种简单且独立的方式来验证身份和授权,因此为了简单起见,我们在示例中使用它。同样,JPA 将帮助我们处理领域逻辑和数据库之间的通信,而 Quarkus 将成为所有这些组件的粘合剂。
什么是JWT?
JSON Web 令牌 (JWT)是一种以紧凑、URL 安全的 JSON 对象的形式在用户和服务器之间传输信息的安全方法。该令牌经过数字签名以进行验证,通常用于基于 Web 的应用程序中的身份验证和安全数据交换。在身份验证过程中,服务器会发出包含用户身份和声明的 JWT,客户端将在后续请求中使用该 JWT 来访问受保护的资源
客户端通过提供一些凭据来请求令牌,然后授权服务器提供签名的令牌;随后,当尝试访问资源时,客户端会提供 JWT 令牌,资源服务器会根据所需的权限来验证该令牌。
考虑到这些基本概念,让我们探讨如何在 Quarkus 应用程序中集成 RBAC 和 JWT。
数据设计
为了简单起见,我们将创建一个基本的 RBAC 系统以在本示例中使用。
这使我们能够表示用户、他们的角色以及构成每个角色的权限。JPA数据库表将代表我们的域对象:
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(unique = true) private String username; @Column private String password; @Column(unique = true) private String email; @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_name")) private Set<Role> roles = new HashSet<>(); // Getter and Setters }
|
用户表保存登录凭据以及用户和角色之间的关系:
@Entity @Table(name = "roles") public class Role { @Id private String name; @Roles @Convert(converter = PermissionConverter.class) private Set<Permission> permissions = new HashSet<>(); // Getters and Setters }
|
同样,为了简单起见,权限使用逗号分隔值存储在列中,为此,我们使用PermissionConverter。
JSON Web Token 和 Quarkus
在凭证方面,要使用 JWT 令牌并启用登录,我们需要以下依赖项:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> <version>3.9.4</version> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> <version>3.9.4</version> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security</artifactId> <scope>test</scope> <version>3.9.4</version> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security-jwt</artifactId> <scope>test</scope> <version>3.9.4</version> </dependency>
|
这些模块为我们提供了实现令牌生成、权限验证和测试我们的实现的工具。现在,为了定义依赖项和 Quarkus 版本,我们将使用BOM Parent,其中包含与框架兼容的特定版本。对于这个例子,我们需要:
接下来,为了实现令牌签名,我们需要RSA公钥和私钥。 Quarkus 有一种简单的配置方法。生成后,我们必须配置以下属性:mp.jwt.verify.publickey.location=publicKey.pem mp.jwt.verify.issuer=my-issuer smallrye.jwt.sign.key.location=privateKey.pem
|
默认情况下,Quarkus 会查看/resources或提供的绝对路径。该框架使用密钥来签署声明并验证令牌。
凭证
现在,要创建 JWT 令牌并设置其权限,我们需要验证用户的凭据。下面的代码是我们如何做到这一点的示例:
@Path("/secured") public class SecureResourceController { // other methods... @POST @Path("/login") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @PermitAll public Response login(@Valid final LoginDto loginDto) { if (userService.checkUserCredentials(loginDto.username(), loginDto.password())) { User user = userService.findByUsername(loginDto.username()); String token = userService.generateJwtToken(user); return Response.ok().entity(new TokenResponse("Bearer " + token,"3600")).build(); } else { return Response.status(Response.Status.UNAUTHORIZED).entity(new Message("Invalid credentials")).build(); } } }
|
登录端点验证用户凭据并在成功时发出令牌作为响应。另一个需要注意的重要事项是@PermitAll,它确保此端点是公共的并且不需要任何身份验证。不过,我们很快就会更详细地了解许可。
在这里,我们要特别注意的另一段重要代码是generateJwtToken方法,它创建并签署一个令牌。
public String generateJwtToken(final User user) { Set<String> permissions = user.getRoles() .stream() .flatMap(role -> role.getPermissions().stream()) .map(Permission::name) .collect(Collectors.toSet()); return Jwt.issuer(issuer) .upn(user.getUsername()) .groups(permissions) .expiresIn(3600) .claim(Claims.email_verified.name(), user.getEmail()) .sign(); }
|
在此方法中,我们检索每个角色提供的权限列表并将其注入到令牌中。发行者还定义了令牌、重要声明和生存时间,然后最后我们对令牌进行签名。一旦用户收到它,它将用于验证所有后续呼叫。该令牌包含服务器验证和授权相应用户所需的所有内容。用户只需将不记名令牌发送到Authentication标头即可对调用进行身份验证。权限
如前所述,Jakarta使用@RolesAllowed为资源分配权限。尽管它称它们为角色,但它们的工作方式类似于权限(考虑到我们之前定义的概念),这意味着我们只需要用它注释我们的端点即可保护它们,例如:
@Path("/secured") public class SecureResourceController { private final UserService userService; private final SecurityIdentity securityIdentity; // constructor @GET @Path("/resource") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({"VIEW_ADMIN_DETAILS"}) public String get() { return "Hello world, here are some details about the admin!"; } @GET @Path("/resource/user") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({"VIEW_USER_DETAILS"}) public Message getUser() { return new Message("Hello "+securityIdentity.getPrincipal().getName()+"!"); } //... }
|
查看代码,我们可以看到向端点添加权限控制是多么简单。在我们的例子中,/secured/resource/user现在需要VIEW_USER_DETAILS权限,并且/secured/resource需要VIEW_ADMIN_DETAILS。我们还可以观察到可以分配一系列权限而不是仅分配一个权限。在这种情况下,Quarkus 将至少需要 @RolesAllowed 中列出的权限之一。 另一个重要的说明是,令牌包含当前登录用户(安全身份中的主体)的权限和信息。
测试
Quarkus 提供了许多工具,使我们的应用程序测试变得简单且易于实施。使用这些工具,我们可以配置 JWT 的创建和设置及其上下文,使测试意图清晰且易于理解。下面的测试表明了这一点:
@QuarkusTest class SecureResourceControllerTest { @Test @TestSecurity(user = "user", roles = "VIEW_USER_DETAILS") @JwtSecurity(claims = { @Claim(key = "email", value = "user@test.io") }) void givenSecureAdminApi_whenUserTriesToAccessAdminApi_thenShouldNotAllowRequest() { given() .contentType(ContentType.JSON) .get("/secured/resource") .then() .statusCode(403); } @Test @TestSecurity(user = "admin", roles = "VIEW_ADMIN_DETAILS") @JwtSecurity(claims = { @Claim(key = "email", value = "admin@test.io") }) void givenSecureAdminApi_whenAdminTriesAccessAdminApi_thenShouldAllowRequest() { given() .contentType(ContentType.JSON) .get("/secured/resource") .then() .statusCode(200) .body(equalTo("Hello world, here are some details about the admin!")); } //... }
|
@TestSecurity注释允许定义安全属性,而@JwtSecurity允许定义令牌的声明。使用这两种工具,我们可以测试多种场景和用例。到目前为止,我们看到的工具已经足以使用 Quarkus 实现强大的 RBAC 系统。然而,它有更多的选择。
Quarkus安全
Quarkus 还提供了强大的安全系统,可以与我们的 RBAC 解决方案集成。让我们检查一下如何将此类功能与 RBAC 实现结合起来。首先,我们需要了解概念,因为 Quarkus 权限系统不适用于角色。但是,可以在角色权限之间创建映射。让我们看看如何:
quarkus.http.auth.policy.role-policy1.permissions.VIEW_ADMIN_DETAILS=VIEW_ADMIN_DETAILS quarkus.http.auth.policy.role-policy1.permissions.VIEW_USER_DETAILS=VIEW_USER_DETAILS quarkus.http.auth.policy.role-policy1.permissions.SEND_MESSAGE=SEND_MESSAGE quarkus.http.auth.policy.role-policy1.permissions.CREATE_USER=CREATE_USER quarkus.http.auth.policy.role-policy1.permissions.OPERATOR=OPERATOR quarkus.http.auth.permission.roles1.paths=/permission-based/* quarkus.http.auth.permission.roles1.policy=role-policy1
|
使用应用程序属性文件,我们定义一个角色策略,它将角色映射到权限。映射的工作方式类似于quarkus.http.auth.policy.{policyName}.permissions.{roleName}={listOfPermissions}。在有关角色和权限的示例中,它们具有相同的名称并一一映射。但是,这可能不是强制性的,也可以将角色映射到权限列表。然后,映射完成后,我们使用配置的最后两行定义应用此策略的路径。
资源权限设置也会有所不同,例如:
@Path("/permission-based") public class PermissionBasedController { private final SecurityIdentity securityIdentity; public PermissionBasedController(SecurityIdentity securityIdentity) { this.securityIdentity = securityIdentity; } @GET @Path("/resource/version") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @PermissionsAllowed("VIEW_ADMIN_DETAILS") public String get() { return "2.0.0"; } @GET @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Path("/resource/message") @PermissionsAllowed(value = {"SEND_MESSAGE", "OPERATOR"}, inclusive = true) public Message message() { return new Message("Hello "+securityIdentity.getPrincipal().getName()+"!"); } }
|
设置类似,在我们的例子中,唯一的变化是@PermissionsAllowed注释而不是@RolesAllowed 。此外,权限还允许不同的行为,例如包含标志,权限匹配机制的行为从 OR 更改为 AND。我们使用与之前相同的设置来测试行为:@QuarkusTest class PermissionBasedControllerTest { @Test @TestSecurity(user = "admin", roles = "VIEW_ADMIN_DETAILS") @JwtSecurity(claims = { @Claim(key = "email", value = "admin@test.io") }) void givenSecureVersionApi_whenUserIsAuthenticated_thenShouldReturnVersion() { given() .contentType(ContentType.JSON) .get("/permission-based/resource/version") .then() .statusCode(200) .body(equalTo("2.0.0")); } @Test @TestSecurity(user = "user", roles = "SEND_MESSAGE") @JwtSecurity(claims = { @Claim(key = "email", value = "user@test.io") }) void givenSecureMessageApi_whenUserOnlyHasOnePermission_thenShouldNotAllowRequest() { given() .contentType(ContentType.JSON) .get("/permission-based/resource/message") .then() .statusCode(403); } @Test @TestSecurity(user = "new-operator", roles = {"SEND_MESSAGE", "OPERATOR"}) @JwtSecurity(claims = { @Claim(key = "email", value = "operator@test.io") }) void givenSecureMessageApi_whenUserOnlyHasBothPermissions_thenShouldAllowRequest() { given() .contentType(ContentType.JSON) .get("/permission-based/resource/message") .then() .statusCode(200) .body("message", equalTo("Hello new-operator!")); } }
|
Quarkus 安全模块提供了许多其他功能。