1. 概述

本教程将探讨基于角色的访问控制(RBAC)机制,以及如何使用Quarkus框架实现该功能。

RBAC是构建复杂安全系统的经典方案。Quarkus作为现代化的云原生全栈Java框架,原生支持RBAC功能。需要说明的是,角色在实际应用中有多种实现方式:

  • 在企业系统中,角色通常是权限的聚合,用于标识用户可执行的操作组
  • Jakarta规范中,角色本质是允许执行资源操作的标签(等同于权限)

本教程将采用资源权限分配的方式控制访问,角色则作为权限的集合容器。

2. RBAC核心原理

基于角色的访问控制是一种安全模型,通过预定义的权限授予用户应用访问权。系统管理员在访问尝试时:

  1. ✅ 为特定资源分配权限
  2. ✅ 验证用户权限
  3. ✅ 通过角色分组管理权限

RBAC架构图

为演示Quarkus中的RBAC实现,我们将结合以下技术:

  • JWT:实现身份验证和授权的自包含方案
  • JPA:处理领域逻辑与数据库交互
  • Quarkus Security:整合所有安全组件

3. JWT机制解析

JSON Web Tokens (JWT)是用户与服务器间安全传输信息的紧凑型URL安全JSON对象。该令牌具有数字签名,常用于Web应用的身份验证和安全数据交换:

JWT工作流程

核心流程:

  1. 客户端提供凭证请求令牌
  2. 授权服务器返回签名令牌
  3. 客户端携带JWT访问资源
  4. 资源服务器验证令牌及所需权限

接下来我们将探讨如何在Quarkus中整合RBAC与JWT。

4. 数据模型设计

为简化示例,我们设计基础RBAC数据模型,包含以下表结构:

Quarkus 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. JWT与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-smallrye-jwt-build:令牌生成
  • quarkus-smallrye-jwt:权限验证
  • quarkus-test-security(-jwt):测试支持

配置RSA密钥实现令牌签名:

mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=my-issuer
smallrye.jwt.sign.key.location=privateKey.pem

Quarkus默认在/resources或指定绝对路径查找密钥文件,用于:

  • 签名claims
  • 验证令牌有效性

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:声明公开接口(无需认证)
  • 验证成功返回Bearer令牌
  • 验证失败返回401状态码

核心方法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();
}

执行流程:

  1. 收集用户所有角色的权限
  2. 设置令牌核心属性:
    • 签发方(issuer)
    • 用户标识(upn)
    • 权限组(groups)
    • 过期时间(3600秒)
    • 自定义声明(email_verified)
  3. 签名生成令牌

客户端后续请求只需在Authorization头携带Bearer令牌即可完成认证。

7. 权限控制

Jakarta规范使用@RolesAllowed注解分配权限(虽命名为roles但实际作为权限使用):

@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:需要VIEW_ADMIN_DETAILS权限
  • /secured/resource/user:需要VIEW_USER_DETAILS权限
  • ✅ 支持多权限列表(满足任一即可)
  • SecurityIdentity提供当前用户信息

8. 测试方案

Quarkus提供强大测试工具,简化JWT测试场景:

@QuarkusTest
class SecureResourceControllerTest {
    @Test
    @TestSecurity(user = "user", roles = "VIEW_USER_DETAILS")
    @JwtSecurity(claims = {
        @Claim(key = "email", value = "user@example.com")
    })
    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@example.com")
    })
    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:配置令牌声明(claims)
  • ✅ 支持多场景测试(权限不足/权限充足)

9. Quarkus安全扩展

Quarkus安全模块可与RBAC深度集成。虽然其原生权限系统不直接使用角色概念,但可通过配置建立映射关系:

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

配置解析:

  • role-policy:定义角色-权限映射
  • 格式:quarkus.http.auth.policy.{策略名}.permissions.{角色名}={权限列表}
  • 后两行:指定策略应用路径

接口实现需改用@PermissionsAllowed注解:

@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()+"!");
    }
}

关键特性:

  • inclusive=true:要求同时满足所有权限(AND逻辑)
  • 默认inclusive=false:满足任一权限即可(OR逻辑)

测试用例验证复合权限场景:

@QuarkusTest
class PermissionBasedControllerTest {
    @Test
    @TestSecurity(user = "admin", roles = "VIEW_ADMIN_DETAILS")
    @JwtSecurity(claims = {
        @Claim(key = "email", value = "admin@example.com")
    })
    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@example.com")
    })
    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@example.com")
    })
    void givenSecureMessageApi_whenUserOnlyHasBothPermissions_thenShouldAllowRequest() {
        given()
          .contentType(ContentType.JSON)
          .get("/permission-based/resource/message")
          .then()
          .statusCode(200)
          .body("message", equalTo("Hello new-operator!"));
    }
}

测试覆盖:

  • 单一权限访问(成功)
  • 部分权限访问(失败)
  • 完整权限访问(成功)

10. 总结

本文深入探讨了RBAC系统在Quarkus中的实现方案,重点对比了:

  • 角色与权限的概念差异
  • Jakarta规范与Quarkus安全模块的实现区别
  • 不同场景下的测试策略

核心要点:

  1. ✅ JWT提供轻量级认证方案
  2. ✅ JPA简化权限数据管理
  3. ✅ 注解式权限控制提升开发效率
  4. ✅ 配置化权限映射增强灵活性

完整代码示例请参考GitHub仓库


原始标题:Role-Based Access Control in Quarkus