Quarkus中基於角色的許可權訪問控制教程

banq發表於2024-05-03

在本教程中,我們將討論基於角色的訪問控制 (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 = <font>"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<i>
}

使用者表儲存登入憑據以及使用者和角色之間的關係:

@Entity
@Table(name = <font>"roles")
public class Role {
    @Id
    private String name;
    @Roles
    @Convert(converter = PermissionConverter.class)
    private Set<Permission> permissions = new HashSet<>();
   
// Getters and Setters<i>
}

同樣,為了簡單起見,許可權使用逗號分隔值儲存在列中,為此,我們使用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,其中包含與框架相容的特定版本。對於這個例子,我們需要:
  • quarkus-smallrye-jwt-build
  • quarkus-smallrye-jwt
  • quarkus-test-security
  • quarkus-test-security-jwt

接下來,為了實現令牌簽名,我們需要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(<font>"/secured")
public class SecureResourceController {
   
// other methods...<i>
    @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(<font>"/secured")
public class SecureResourceController {
    private final UserService userService;
    private final SecurityIdentity securityIdentity;
   
// constructor<i>
    @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()+"!");
    }
   
//...<i>
}

檢視程式碼,我們可以看到向端點新增許可權控制是多麼簡單。在我們的例子中,/secured/resource/user現在需要VIEW_USER_DETAILS許可權,並且/secured/resource需要VIEW_ADMIN_DETAILS。我們還可以觀察到可以分配一系列許可權而不是僅分配一個許可權。在這種情況下,Quarkus 將至少需要 @RolesAllowed 中列出的許可權之一。 

另一個重要的說明是,令牌包含當前登入使用者(安全身份中的主體)的許可權和資訊。

測試
Quarkus 提供了許多工具,使我們的應用程式測試變得簡單且易於實施。使用這些工具,我們可以配置 JWT 的建立和設定及其上下文,使測試意圖清晰且易於理解。下面的測試表明瞭這一點:

@QuarkusTest
class SecureResourceControllerTest {
    @Test
    @TestSecurity(user = <font>"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!"));
    }
   
//...<i>
}

@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<font>/*
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 安全模組提供了許多其他功能。

 

相關文章