Quarkus 中基于角色的访问控制(RBAC)
1. 概述
本教程中,我们将讨论基于角色的访问控制(RBAC)以及如何使用 Quarkus 实现此功能。
RBAC 是实现复杂安全系统的一种众所周知的机制。Quarkus 是一个现代云原生全栈 Java 框架,支持开箱即用的 RBAC。
在我们开始之前,重要的是要注意角色可以通过多种方式应用。在企业中,角色通常只是权限的集合,用于标识用户可以执行的特定操作组。在雅加达,角色是允许执行资源操作(相当于权限)的标记。实现 RBAC 系统有不同的方法。
在本教程中,我们将使用分配给资源的权限来控制访问,角色将对权限列表进行分组。
2. RBAC
基于角色的访问控制是一种基于预定义权限,授予应用程序用户访问权限的安全模型。系统管理员可以在尝试访问时将这些权限分配并验证给特定资源。为了帮助管理权限,他们创建角色对其进行分组:
为了演示使用 Quarkus 实现 RBAC 系统,我们需要一些其他工具,如 JSON Web Token(JWT)、JPA 和 Quarkus 安全模块。JWT 帮助我们实现一种简单而独立的方式来验证身份和授权,因此为了简单起见,我们将其作为示例。同样,JPA 将帮助我们处理域逻辑和数据库之间的通信,而 Quarkus 将是所有这些组件的粘合剂。
3. JWT
JSON Web Tokens (JWT) 是一种在用户和服务器之间,以紧凑的、URL 安全的 JSON 对象传输信息的安全方式。此令牌通过数字签名以进行验证,通常用于基于 web 的应用中的身份验证和安全数据交换。在身份验证过程中,服务器会发出包含用户身份和声明的 JWT,客户端将在随后的访问受保护资源的请求中使用该 JWT:
客户端通过提供一些凭证来请求令牌,然后授权服务器提供签名的令牌;稍后,当尝试访问资源时,客户端提供 JWT 令牌,资源服务器根据所需的权限对其进行验证。考虑到这些基本概念,让我们探索如何在 Quarkus 应用程序中集成 RBAC 和 JWT。
4. 数据设计
为保持简单,我们将创建一个基本的 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
。
5. 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
或提供的绝对路径。该框架使用密钥对声明进行签名并验证令牌。
6. 凭证
现在,要创建 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
header 即可对调用进行身份验证。
7. 权限
如前所述,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()+"!");
}
//...
}
查看这些代码,我们可以看到向节点添加权限控制是多么简单。在我们的示例中,/security/resource/user
现在需要 VIEW_USER_DETAILS 权限,而 /securite/resource
需要 VIEW_ADMIN_DETAILS 权限。我们还可以观察到,可以分配一个权限列表,而不是仅分配一个。在这种情况下,Quarkus 将需要 @RolesAllowed
中列出的至少一个权限。
另一个重要的特征是,令牌包含当前登录用户(安全标识中的主体)的权限和信息。
8. 测试
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 允许定义 Token 的声明。使用这两种工具,我们可以测试大量的场景和用例。
到目前为止,我们看到的工具已经足以使用 Quarkus 实现强大的 RBAC 系统。然而,它还有更多的选项。
9. 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
。此外,权限还允许不同的行为,例如inclusive 标志,权限匹配机制的行为从 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 安全模块提供了许多其他特性,不过他们不会再本文中涵盖。
10. 结论
在这篇文章中,我们讨论了 RBAC 系统,以及我们如何利用 Quarkus 框架来实现它。我们还看到了如何使用角色或权限的一些细微差别,以及它们在实现中的概念差异。最后,我们观察了 Jakarta 实现和 Quarkus 安全模块之间的差异,以及它们如何在两种情况下帮助测试这些功能。