Quarkus中基于角色的权限访问控制教程

在本教程中,我们将讨论基于角色的访问控制 (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 安全模块提供了许多其他功能。