编程

Quarkus 中基于角色的访问控制(RBAC)

63 2024-05-09 04:41:00

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 安全模块之间的差异,以及它们如何在两种情况下帮助测试这些功能。