编辑此页面

Web 端点的授权

Quarkus 包含一个可插拔的 Web 安全层。当安全功能激活时,系统会对所有 HTTP 请求执行权限检查,以确定它们是否应该继续执行。

如果路径受到 quarkus.http.auth. 配置的限制,即使使用 @PermitAll 也不会开放该路径。要确保可以访问特定路径,必须在 Quarkus 安全设置中进行适当的配置。

如果您使用 Jakarta RESTful Web Services,请考虑使用 quarkus.security.jaxrs.deny-unannotated-endpointsquarkus.security.jaxrs.default-roles-allowed 来设置默认安全要求,而不是 HTTP 路径级别的匹配,因为注解可以覆盖各个端点上的这些属性。

授权基于安全提供程序提供的用户角色。要自定义这些角色,可以创建一个 SecurityIdentityAugmentor,请参阅安全身份自定义

使用配置进行授权

权限在 Quarkus 配置中由权限集定义,每个权限集指定一个访问控制策略。

表 1. Quarkus 策略摘要
内置策略 描述

deny(拒绝)

此策略拒绝所有用户。

permit(允许)

此策略允许所有用户。

authenticated(已认证)

此策略仅允许已认证用户。

您可以定义基于角色的策略,允许具有特定角色的用户访问资源。

基于角色的策略示例
quarkus.http.auth.policy.role-policy1.roles-allowed=user,admin                  (1)
1 这定义了一个基于角色的策略,允许具有 useradmin 角色的用户。

您可以通过配置在 application.properties 文件中定义的内置权限集来引用自定义策略,如下面的配置示例所示

策略配置示例
quarkus.http.auth.permission.permit1.paths=/public/*                            (1)
quarkus.http.auth.permission.permit1.policy=permit
quarkus.http.auth.permission.permit1.methods=GET

quarkus.http.auth.permission.deny1.paths=/forbidden                             (2)
quarkus.http.auth.permission.deny1.policy=deny

quarkus.http.auth.permission.roles1.paths=/roles-secured/*,/other/*,/api/*      (3)
quarkus.http.auth.permission.roles1.policy=role-policy1
1 此权限引用默认的内置 permit 策略,以允许对 /publicGET 方法。在这种情况下,所演示的设置不会影响此示例,因为无论如何都允许此请求。
2 此权限引用内置的 deny 策略,用于 /forbidden/forbidden/ 路径。这是一个精确的路径匹配,因为它不以 * 结尾。
3 此权限集引用先前定义的策略。roles1 是一个示例名称;您可以随意命名权限集。

上面的示例中的精确路径模式 /forbidden 也保护了 /forbidden/ 路径。这样,下面示例中的 forbidden 端点就受到 deny1 权限的保护。

package org.acme.crud;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/forbidden")
public class ForbiddenResource {
    @GET
    public String forbidden() { (1)
        return "No!";
    }
}
1 为了保护 forbidden 端点,需要保护 /forbidden/forbidden/ 路径。

如果您需要允许访问 /forbidden/ 路径,请添加具有更具体精确路径的新权限,如下面的示例所示

quarkus.http.auth.permission.permit1.paths=/forbidden/ (1)
quarkus.http.auth.permission.permit1.policy=permit
1 /forbidden/ 路径未受到保护。

自定义 HttpSecurityPolicy

有时注册您自己的命名策略可能很有用。您可以通过创建一个实现了 io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy 接口的应用程序作用域 CDI bean 来完成它,如下面的示例所示

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomNamedHttpSecPolicy implements HttpSecurityPolicy {
    @Override
    public Uni<CheckResult> checkPermission(RoutingContext event, Uni<SecurityIdentity> identity,
            AuthorizationRequestContext requestContext) {
        if (customRequestAuthorization(event)) {
            return CheckResult.permit();
        }
        return CheckResult.deny();
    }

    @Override
    public String name() {
        return "custom"; (1)
    }

    private static boolean customRequestAuthorization(RoutingContext event) {
        // here comes your own security check
        return !event.request().path().endsWith("denied");
    }
}
1 命名的 HTTP 安全策略将仅应用于由 application.properties 路径匹配规则匹配的请求。
从配置文件引用的自定义命名 HttpSecurityPolicy 示例
quarkus.http.auth.permission.custom1.paths=/custom/*
quarkus.http.auth.permission.custom1.policy=custom                              (1)
1 自定义策略名称必须与 io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name 方法返回的值匹配。

或者,您可以使用 @AuthorizationPolicy 安全注解将自定义命名的 HttpSecurityPolicy 绑定到 Jakarta REST 端点。

绑定到 Jakarta REST 端点的自定义命名 HttpSecurityPolicy 示例
import io.quarkus.vertx.http.security.AuthorizationPolicy;
import jakarta.annotation.security.DenyAll;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@DenyAll (1)
@Path("hello")
public class HelloResource {

    @AuthorizationPolicy(name = "custom") (2)
    @GET
    public String hello() {
        return "hello";
    }

}
1 @AuthorizationPolicy 注解可以与其他标准安全注解一起使用。与往常一样,方法级别的注解优先于类级别的注解。
2 将自定义命名的 HttpSecurityPolicy 应用于 Jakarta REST hello 端点。

您还可以创建在每个请求上调用的全局 HttpSecurityPolicy。只需不实现 io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name 方法并保留策略的无名称状态。

@RequestScoped bean 注入 HttpSecurityPolicy

只有当 CDI 请求上下文处于活动状态时,才能注入 @RequestScoped bean。用户可以激活上下文,例如使用 @ActivateRequestContext,但是授权发生在 Quarkus 准备一些 @RequestScoped bean 之前。我们建议让 Quarkus 为您激活和准备 CDI 请求上下文。例如,考虑一种情况,您想从 Jakarta REST 上下文中注入一个 bean,例如 jakarta.ws.rs.core.UriInfo bean。在这种情况下,您必须将 HttpSecurityPolicy 应用于 Jakarta REST 端点。这可以通过以下方式之一实现:* 使用 @AuthorizationPolicy 安全注解。* 设置 quarkus.http.auth.permission.custom1.applies-to=jaxrs 配置属性。

匹配路径和方法

权限集还可以将路径和方法指定为逗号分隔的列表。如果路径以 * 通配符结尾,则它生成的查询匹配所有子路径。否则,它查询精确匹配,并且仅匹配该特定路径

quarkus.http.auth.permission.permit1.paths=/public*,/css/*,/js/*,/robots.txt    (1)
quarkus.http.auth.permission.permit1.policy=permit
quarkus.http.auth.permission.permit1.methods=GET,HEAD
1 路径末尾的 * 通配符匹配零个或多个路径段,但绝不匹配从 /public 路径开始的任何单词。因此,诸如 /public-info 之类的路径与此模式不匹配。

匹配路径但不匹配方法

如果请求基于路径匹配一个或多个权限集,但不匹配任何所需的方法,则该请求将被拒绝。

给定前面的权限集,GET /public/foo 将匹配路径和方法,因此将被允许。相反,POST /public/foo 将匹配路径但不匹配方法,因此将被拒绝。

匹配多个路径:最长路径获胜

匹配始终以“最长路径获胜”为基础完成。如果已匹配到更具体的权限集,则不会考虑不太具体的权限集

quarkus.http.auth.permission.permit1.paths=/public/*
quarkus.http.auth.permission.permit1.policy=permit
quarkus.http.auth.permission.permit1.methods=GET,HEAD

quarkus.http.auth.permission.deny1.paths=/public/forbidden-folder/*
quarkus.http.auth.permission.deny1.policy=deny

给定前面的权限集,GET /public/forbidden-folder/foo 将匹配两个权限集的路径。但是,由于较长的路径与 deny1 权限集的路径匹配,因此选择 deny1,并且请求被拒绝。

子路径权限优先于根路径权限,正如之前 deny1permit1 权限示例所说明的那样。

此规则通过一个子路径权限允许访问公共资源,而根路径权限需要授权的场景进一步说明。

quarkus.http.auth.policy.user-policy.roles-allowed=user
quarkus.http.auth.permission.roles.paths=/api/*
quarkus.http.auth.permission.roles.policy=user-policy

quarkus.http.auth.permission.public.paths=/api/noauth/*
quarkus.http.auth.permission.public.policy=permit

匹配多个子路径:通配符 * 的最长路径获胜

之前的示例演示了当路径以 * 通配符结尾时匹配所有子路径。

此通配符也适用于路径的中间,表示单个路径段。它不能与其他路径段字符混合使用;因此,路径分隔符始终包含 * 通配符,如 /public/*/about-us 路径所示。

当多个路径模式对应于同一请求路径时,系统会选择通向 * 通配符的最长子路径。在这种情况下,每个路径段字符都比 * 通配符更具体。

这是一个简单的例子

quarkus.http.auth.permission.secured.paths=/api/*/detail                    (1)
quarkus.http.auth.permission.secured.policy=authenticated
quarkus.http.auth.permission.public.paths=/api/public-product/detail        (2)
quarkus.http.auth.permission.public.policy=permit
1 诸如 /api/product/detail 之类的请求路径只能由已认证用户访问。
2 路径 /api/public-product/detail 更具体,因此任何人都可以访问。

应该测试使用配置授权保护的所有路径。使用多个通配符编写路径模式可能很麻烦。请确保路径已按您的预期授权。

在以下示例中,路径从最具体到最不具体排序

请求路径 /one/two/three/four/five 从最具体到最不具体的路径排序匹配
/one/two/three/four/five
/one/two/three/four/*
/one/two/three/*/five
/one/two/three/*/*
/one/two/*/four/five
/one/*/three/four/five
/*/two/three/four/five
/*/two/three/*/five
/*

路径末尾的 * 通配符匹配零个或多个路径段。放置在任何其他位置的 * 通配符恰好匹配一个路径段。

匹配多个路径:最具体的方法获胜

当使用多个权限集注册路径时,显式指定与请求匹配的 HTTP 方法的权限集优先。在这种情况下,如果没有方法的权限集仅在请求方法与具有方法规范的权限集不匹配时才生效。

quarkus.http.auth.permission.permit1.paths=/public/*
quarkus.http.auth.permission.permit1.policy=permit
quarkus.http.auth.permission.permit1.methods=GET,HEAD

quarkus.http.auth.permission.deny1.paths=/public/*
quarkus.http.auth.permission.deny1.policy=deny

前面的权限集表明 GET /public/foo 匹配两个权限集的路径。但是,它专门与 permit1 权限集的显式方法对齐。因此,选择 permit1,并且请求被接受。

相反,PUT /public/foopermit1 的方法权限不匹配。结果,deny1 被激活,导致请求被拒绝。

匹配多个路径和方法:两者都获胜

有时,先前描述的规则允许多个权限集同时获胜。在这种情况下,为了继续执行请求,所有权限都必须允许访问。为此,两者都必须指定该方法,或者没有方法。方法特定的匹配优先。

quarkus.http.auth.policy.user-policy1.roles-allowed=user
quarkus.http.auth.policy.admin-policy1.roles-allowed=admin

quarkus.http.auth.permission.roles1.paths=/api/*,/restricted/*
quarkus.http.auth.permission.roles1.policy=user-policy1

quarkus.http.auth.permission.roles2.paths=/api/*,/admin/*
quarkus.http.auth.permission.roles2.policy=admin-policy1
给定前面的权限集,GET /api/foo 将匹配两个权限集的路径,需要 useradmin 角色。

拒绝访问的配置属性

以下配置设置更改基于角色的访问控制 (RBAC) 拒绝行为

quarkus.security.jaxrs.deny-unannotated-endpoints=true|false

如果设置为 true,则默认情况下拒绝所有 Jakarta REST 端点的访问。如果 Jakarta REST 端点没有安全注解,则默认为 @DenyAll 行为。这有助于您避免意外暴露应该受到保护的端点。默认为 false

quarkus.security.jaxrs.default-roles-allowed=role1,role2

定义未注解端点的默认角色要求。** 角色是一个特殊角色,表示任何已认证用户。这不能与 deny-unannotated-endpoints 结合使用,因为 deny 会生效。

quarkus.security.deny-unannotated-members=true|false

如果设置为 true,则拒绝访问所有没有安全注解但在包含具有安全注解的方法的类中定义的 CDI 方法和 Jakarta REST 端点。默认为 false

禁用权限

可以使用每个声明权限的 enabled 属性在构建时禁用权限,例如

quarkus.http.auth.permission.permit1.enabled=false
quarkus.http.auth.permission.permit1.paths=/public/*,/css/*,/js/*,/robots.txt
quarkus.http.auth.permission.permit1.policy=permit
quarkus.http.auth.permission.permit1.methods=GET,HEAD

可以使用系统属性或环境变量在运行时重新启用权限,例如:-Dquarkus.http.auth.permission.permit1.enabled=true

权限路径和 HTTP 根路径

quarkus.http.root-path 配置属性更改 http 端点上下文路径

默认情况下,quarkus.http.root-path 会自动附加到配置的权限路径,然后不使用正斜杠,例如

quarkus.http.auth.permission.permit1.paths=public/*,css/*,js/*,robots.txt

此配置等效于以下内容

quarkus.http.auth.permission.permit1.paths=${quarkus.http.root-path}/public/*,${quarkus.http.root-path}/css/*,${quarkus.http.root-path}/js/*,${quarkus.http.root-path}/robots.txt

前导斜杠会更改配置的权限路径的解释方式。配置的 URL 按原样使用,如果 quarkus.http.root-path 的值发生更改,则不会调整路径。

示例
quarkus.http.auth.permission.permit1.paths=/public/*,css/*,js/*,robots.txt

此配置仅影响从固定或静态 URL 提供的资源,/public,如果 quarkus.http.root-path 已设置为 / 以外的值,则可能与您的应用程序资源不匹配。

有关更多信息,请参阅 Quarkus 中的路径解析

映射 SecurityIdentity 角色

获胜的基于角色的策略可以将 SecurityIdentity 角色映射到部署特定的角色。然后,这些角色通过使用 @RolesAllowed 注解适用于端点授权。

quarkus.http.auth.policy.admin-policy1.roles.admin=Admin1 (1)
quarkus.http.auth.permission.roles1.paths=/* (2)
quarkus.http.auth.permission.roles1.policy=admin-policy1
1 admin 角色映射到 Admin1 角色。SecurityIdentity 将同时具有 adminAdmin1 角色。
2 /* 路径受到保护,仅授予经过身份验证的 HTTP 请求访问权限。

如果您需要做的只是将 SecurityIdentity 角色映射到部署特定的角色,而不管路径如何,您也可以这样做

quarkus.http.auth.roles-mapping.admin=Admin1 (1) (2)
1 admin 角色映射到 Admin1 角色。SecurityIdentity 将同时具有 adminAdmin1 角色。
2 /* 路径未受到保护。您必须使用标准安全注解来保护您的端点,或者除了此配置属性之外,还定义 HTTP 权限。

共享权限检查

非共享权限检查的一条重要规则是,仅应用一个路径匹配,即最具体的路径匹配。当然,您可以指定任意数量具有相同获胜路径的权限,它们都将被应用。但是,可能有一些权限检查您想应用于许多路径,而无需一次又一次地重复它们。这就是共享权限检查的用武之地,当权限路径匹配时,它们始终会被应用。

应用于每个 HTTP 请求的自定义命名 HttpSecurityPolicy 示例
quarkus.http.auth.permission.custom1.paths=/*
quarkus.http.auth.permission.custom1.shared=true    (1)
quarkus.http.auth.permission.custom1.policy=custom

quarkus.http.auth.policy.admin-policy1.roles-allowed=admin
quarkus.http.auth.permission.roles1.paths=/admin/*
quarkus.http.auth.permission.roles1.policy=admin-policy1
1 自定义 HttpSecurityPolicy 也将与 admin-policy1 策略一起应用于 /admin/1 路径。
配置许多共享权限检查不如配置非共享权限检查有效。使用共享权限来补充非共享权限检查,如下面的示例所示。
使用共享权限映射 SecurityIdentity 角色
quarkus.http.auth.policy.role-policy1.roles.root=admin,user (1)
quarkus.http.auth.permission.roles1.paths=/secured/*        (2)
quarkus.http.auth.permission.roles1.policy=role-policy1
quarkus.http.auth.permission.roles1.shared=true

quarkus.http.auth.policy.role-policy2.roles-allowed=user    (3)
quarkus.http.auth.permission.roles2.paths=/secured/user/*
quarkus.http.auth.permission.roles2.policy=role-policy2

quarkus.http.auth.policy.role-policy3.roles-allowed=admin
quarkus.http.auth.permission.roles3.paths=/secured/admin/*
quarkus.http.auth.permission.roles3.policy=role-policy3
1 角色 root 将能够访问 /secured/user/*/secured/admin/* 路径。
2 /secured/* 路径只能由经过身份验证的用户访问。这样,您就保护了 /secured/all 路径等等。
3 共享权限始终在非共享权限之前应用,因此具有 root 角色的 SecurityIdentity 也将具有 user 角色。

使用注解进行授权

Quarkus 包含内置安全性,允许基于常见安全注解 @RolesAllowed@DenyAll@PermitAll 在 REST 端点和 CDI bean 上进行 基于角色的访问控制 (RBAC)

表 2. Quarkus 注解类型摘要
注解类型 描述

@DenyAll

指定不允许任何安全角色调用指定的方法。

@PermitAll

指定允许所有安全角色调用指定的方法。

@PermitAll 允许所有人进入,即使没有身份验证。

@RolesAllowed

指定允许访问应用程序中方法的安全角色列表。

@Authenticated

Quarkus 提供了 io.quarkus.security.Authenticated 注解,该注解允许任何经过身份验证的用户访问资源。它等效于 @RolesAllowed("**")

@PermissionsAllowed

指定允许调用指定方法的权限列表。

@AuthorizationPolicy

指定命名的 io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy,该策略应授权访问指定的 Jakarta REST 端点。命名的 HttpSecurityPolicy 可用于一般授权检查,如 绑定到 Jakarta REST 端点的自定义命名 HttpSecurityPolicy 示例所示。

以下 SubjectExposingResource 示例演示了一个端点,该端点使用 Jakarta REST 和 Common Security 注解来描述和保护其端点。

SubjectExposingResource 示例
import java.security.Principal;

import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

@Path("subject")
public class SubjectExposingResource {

    @GET
    @Path("secured")
    @RolesAllowed("Tester") (1)
    public String getSubjectSecured(@Context SecurityContext sec) {
        Principal user = sec.getUserPrincipal(); (2)
        String name = user != null ? user.getName() : "anonymous";
        return name;
    }

    @GET
    @Path("authenticated")
    @Authenticated (3)
    public String getSubjectAuthenticated(@Context SecurityContext sec) {
        Principal user = sec.getUserPrincipal();
        String name = user != null ? user.getName() : "anonymous";
        return name;
    }

    @GET
    @Path("unsecured")
    @PermitAll (4)
    public String getSubjectUnsecured(@Context SecurityContext sec) {
        Principal user = sec.getUserPrincipal(); (5)
        String name = user != null ? user.getName() : "anonymous";
        return name;
    }

    @GET
    @Path("denied")
    @DenyAll (6)
    public String getSubjectDenied(@Context SecurityContext sec) {
        Principal user = sec.getUserPrincipal();
        String name = user != null ? user.getName() : "anonymous";
        return name;
    }
}
1 /subject/secured 端点需要经过身份验证的用户,该用户通过使用 @RolesAllowed("Tester") 注解被授予 "Tester" 角色。
2 该端点从 Jakarta REST SecurityContext 获取用户主体。对于安全端点,这将返回 non-null
3 /subject/authenticated 端点通过指定 @Authenticated 注解来允许任何经过身份验证的用户。
4 /subject/unsecured 端点通过指定 @PermitAll 注解来允许未经身份验证的访问。
5 如果调用者未经身份验证,则获取用户主体的调用将返回 null;如果调用者经过身份验证,则返回 non-null
6 /subject/denied 端点声明了 @DenyAll 注解,禁止所有直接访问它作为 REST 方法,而不管调用它的用户是谁。该方法仍然可以被该类中的其他方法在内部调用。
如果您计划在 IO 线程上使用标准安全注解,请查看 主动身份验证 中的信息。

@RolesAllowed 注解值支持 属性表达式,包括默认值和嵌套属性表达式。与注解一起使用的配置属性在运行时解析。

表 3. 注解值示例
注解 值说明

@RolesAllowed("${admin-role}")

该端点允许具有 admin-role 属性值表示的角色的用户。

@RolesAllowed("${tester.group}-${tester.role}")

一个示例,说明该值可以包含多个变量。

@RolesAllowed("${customer:User}")

默认值演示。所需的角色由 customer 属性的值表示。但是,如果未指定该属性,则默认需要一个名为 User 的角色。

@RolesAllowed 注解中属性表达式的用法示例
admin=Administrator
tester.group=Software
tester.role=Tester
%prod.secured=User
%dev.secured=**
all-roles=Administrator,Software,Tester,User
主体访问控制示例
import java.security.Principal;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

@Path("subject")
public class SubjectExposingResource {

    @GET
    @Path("admin")
    @RolesAllowed("${admin}") (1)
    public String getSubjectSecuredAdmin(@Context SecurityContext sec) {
        return getUsername(sec);
    }

    @GET
    @Path("software-tester")
    @RolesAllowed("${tester.group}-${tester.role}") (2)
    public String getSubjectSoftwareTester(@Context SecurityContext sec) {
        return getUsername(sec);
    }

    @GET
    @Path("user")
    @RolesAllowed("${customer:User}") (3)
    public String getSubjectUser(@Context SecurityContext sec) {
        return getUsername(sec);
    }

    @GET
    @Path("secured")
    @RolesAllowed("${secured}") (4)
    public String getSubjectSecured(@Context SecurityContext sec) {
        return getUsername(sec);
    }

    @GET
    @Path("list")
    @RolesAllowed("${all-roles}") (5)
    public String getSubjectList(@Context SecurityContext sec) {
        return getUsername(sec);
    }

    private String getUsername(SecurityContext sec) {
        Principal user = sec.getUserPrincipal();
        String name = user != null ? user.getName() : "anonymous";
        return name;
    }
}
1 @RolesAllowed 注解值设置为 Administrator 的值。
2 /subject/software-tester 端点需要一个已被授予 "Software-Tester" 角色的经过身份验证的用户。可以在角色定义中使用多个表达式。
3 /subject/user 端点需要一个已被授予角色 "User" 的经过身份验证的用户,通过使用 @RolesAllowed("${customer:User}") 注解,因为我们没有设置配置属性 customer
4 在生产环境中,此 /subject/secured 端点需要具有 User 角色的经过身份验证的用户。在开发模式下,它允许任何经过身份验证的用户。
5 属性表达式 all-roles 将被视为集合类型 List,因此,端点将可用于角色 AdministratorSoftwareTesterUser

端点安全注解和 Jakarta REST 继承

Quarkus 支持放置在端点实现或其类上的安全注解,如下面的示例所示

@Path("hello")
public interface HelloInterface {

    @GET
    String hello();

}

@DenyAll (1)
public class HelloInterfaceImpl implements HelloInterface {

    @RolesAllowed("admin") (2)
    @Override
    public String hello() {
        return "Hello";
    }
}
1 类级别的安全注解必须放置在声明端点实现的类上。
2 方法级别的安全注解必须放置在端点实现上。

声明为默认接口方法的 RESTEasy 子资源定位器不能通过标准安全注解来保护。安全的子资源定位器必须在接口实现器上实现和保护,如下面的示例所示

@Path("hello")
public interface HelloInterface {

    @RolesAllowed("admin")
    @Path("sub")
    default HelloSubResource wrongWay() {
        // not supported
    }

    @Path("sub")
    HelloSubResource rightWay();

}

public class HelloInterfaceImpl implements HelloInterface {

    @RolesAllowed("admin")
    @Override
    public HelloSubResource rightWay() {
        return new HelloSubResource();
    }
}

权限注解

Quarkus 还提供了 io.quarkus.security.PermissionsAllowed 注解,该注解授权任何具有给定权限的经过身份验证的用户访问资源。此注解是常见安全注解的扩展,并检查授予 SecurityIdentity 实例的权限。

使用 @PermissionsAllowed 注解保护的端点示例
package org.acme.crud;

import io.quarkus.arc.Arc;
import io.vertx.ext.web.RoutingContext;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

import io.quarkus.security.PermissionsAllowed;

import java.security.BasicPermission;
import java.security.Permission;
import java.util.Collection;
import java.util.Collections;

@Path("/crud")
public class CRUDResource {

    @PermissionsAllowed("create") (1)
    @PermissionsAllowed("update")
    @POST
    @Path("/modify/repeated")
    public String createOrUpdate() {
        return "modified";
    }

    @PermissionsAllowed(value = {"create", "update"}, inclusive=true) (2)
    @POST
    @Path("/modify/inclusive")
    public String createOrUpdate(Long id) {
        return id + " modified";
    }

    @PermissionsAllowed({"see:detail", "see:all", "read"}) (3)
    @GET
    @Path("/id/{id}")
    public String getItem(String id) {
        return "item-detail-" + id;
    }

    @PermissionsAllowed(value = "list", permission = CustomPermission.class) (4)
    @Path("/list")
    @GET
    public Collection<String> list(@QueryParam("query-options") String queryOptions) {
        // your business logic comes here
        return Collections.emptySet();
    }

    public static class CustomPermission extends BasicPermission {

        public CustomPermission(String name) {
            super(name);
        }

        @Override
        public boolean implies(Permission permission) {
            var event = Arc.container().instance(RoutingContext.class).get(); (5)
            var publicContent = "public-content".equals(event.request().params().get("query-options"));
            var hasPermission = getName().equals(permission.getName());
            return hasPermission && publicContent;
        }
    }
}
1 资源方法 createOrUpdate 仅适用于同时具有 createupdate 权限的用户。
2 默认情况下,至少需要通过一个注解实例指定的权限之一。您可以通过设置 inclusive=true 来要求所有权限。两个资源方法 createOrUpdate 具有相等的授权要求。
3 如果 SecurityIdentity 具有 read 权限或 see 权限以及 alldetail 操作之一,则授予对 getItem 的访问权限。
4 您可以使用您首选的 java.security.Permission 实现。默认情况下,基于字符串的权限由 io.quarkus.security.StringPermission 执行。
5 权限不是 bean,因此获取 bean 实例的唯一方法是通过编程方式使用 Arc.container()
如果您计划在 IO 线程上使用 @PermissionsAllowed,请查看 主动身份验证 中的信息。
由于 Quarkus 拦截器的限制,@PermissionsAllowed 在类级别上是不可重复的。有关更多信息,请参阅 Quarkus "CDI 参考" 指南的 可重复拦截器绑定 部分。

将权限添加到启用角色的 SecurityIdentity 实例的最简单方法是将角色映射到权限。使用 使用配置进行授权 向经过身份验证的请求授予 CRUDResource 端点所需的 SecurityIdentity 权限,如下面的示例所示

quarkus.http.auth.policy.role-policy1.permissions.user=see:all                                      (1)
quarkus.http.auth.policy.role-policy1.permissions.admin=create,update,read                          (2)
quarkus.http.auth.permission.roles1.paths=/crud/modify/*,/crud/id/*                                 (3)
quarkus.http.auth.permission.roles1.policy=role-policy1

quarkus.http.auth.policy.role-policy2.permissions.user=list
quarkus.http.auth.policy.role-policy2.permission-class=org.acme.crud.CRUDResource$CustomPermission  (4)
quarkus.http.auth.permission.roles2.paths=/crud/list
quarkus.http.auth.permission.roles2.policy=role-policy2
1 将权限 see 和操作 all 添加到 user 角色的 SecurityIdentity 实例。类似地,对于 @PermissionsAllowed 注解,默认使用 io.quarkus.security.StringPermission
2 权限 createupdateread 映射到角色 admin
3 角色策略 role-policy1 仅允许经过身份验证的请求访问 /crud/modify/crud/id 子路径。有关路径匹配算法的更多信息,请参阅本指南后面的 匹配多个路径:最长路径获胜
4 您可以指定 java.security.Permission 类的自定义实现。您的自定义类必须定义恰好一个构造函数,该构造函数接受权限名称,并且可以选择一些操作,例如 String 数组。在这种情况下,权限 list 作为 new CustomPermission("list") 添加到 SecurityIdentity 实例。

您还可以创建具有其他构造函数参数的自定义 java.security.Permission 类。这些附加参数名称与使用 @PermissionsAllowed 注解的方法的参数名称匹配。稍后,Quarkus 将使用实际参数实例化您的自定义权限,使用这些参数调用使用 @PermissionsAllowed 注解的方法。

接受其他参数的自定义 java.security.Permission 类示例
package org.acme.library;

import java.security.Permission;
import java.util.Arrays;
import java.util.Set;

public class LibraryPermission extends Permission {

    private final Set<String> actions;
    private final Library library;

    public LibraryPermission(String libraryName, String[] actions, Library library) { (1)
        super(libraryName);
        this.actions = Set.copyOf(Arrays.asList(actions));
        this.library = library;
    }

    @Override
    public boolean implies(Permission requiredPermission) {
        if (requiredPermission instanceof LibraryPermission) {
            LibraryPermission that = (LibraryPermission) requiredPermission;
            boolean librariesMatch = getName().equals(that.getName());
            boolean requiredLibraryIsSublibrary = library.isParentLibraryOf(that.library);
            boolean hasOneOfRequiredActions = that.actions.stream().anyMatch(actions::contains);
            return (librariesMatch || requiredLibraryIsSublibrary) && hasOneOfRequiredActions;
        }
        return false;
    }

    // here comes your own implementation of the `java.security.Permission` class methods

    public static abstract class Library {

        protected String description;

        abstract boolean isParentLibraryOf(Library library);

    }

    public static class MediaLibrary extends Library {

        @Override
        boolean isParentLibraryOf(Library library) {
            return library instanceof MediaLibrary;
        }
    }

    public static class TvLibrary extends MediaLibrary {
        // TvLibrary specific implementation of the 'isParentLibraryOf' method
    }
}
1 必须恰好有一个自定义 Permission 类的构造函数。第一个参数始终被认为是权限名称,并且必须是 String 类型。Quarkus 可以选择性地将权限操作传递给构造函数。为此,请将第二个参数声明为 String[]

如果允许 SecurityIdentity 执行所需的操作之一,例如 readwritelist,则 LibraryPermission 类允许访问当前库或父库。

以下示例显示了如何使用 LibraryPermission

package org.acme.library;

import io.quarkus.security.PermissionsAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import org.acme.library.LibraryPermission.Library;

@ApplicationScoped
public class LibraryService {

    @PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class) (1)
    public Library updateLibrary(String newDesc, Library library) {
        library.description = newDesc;
        return library;
    }

    @PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class) (2)
    @PermissionsAllowed(value = {"tv:read", "tv:list"}, permission = LibraryPermission.class)
    public Library migrateLibrary(Library migrate, Library library) {
        // migrate libraries
        return library;
    }

}
1 形式参数 library 被标识为匹配同名 LibraryPermission 构造函数参数的参数。因此,Quarkus 会将 library 参数传递给 LibraryPermission 类构造函数。但是,必须每次调用 updateLibrary 方法时都实例化 LibraryPermission
2 在这里,第二个 Library 参数匹配名称 library,而 migrate 参数在 LibraryPermission 权限实例化期间被忽略。
使用 LibraryPermission 保护的资源示例
package org.acme.library;

import io.quarkus.security.PermissionsAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.acme.library.LibraryPermission.Library;

@Path("/library")
public class LibraryResource {

    @Inject
    LibraryService libraryService;

    @PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class)
    @PUT
    @Path("/id/{id}")
    public Library updateLibrary(@PathParam("id") Integer id, Library library) {
        ...
    }

    @PUT
    @Path("/service-way/id/{id}")
    public Library updateLibrarySvc(@PathParam("id") Integer id, Library library) {
        String newDescription = "new description " + id;
        return libraryService.updateLibrary(newDescription, library);
    }

}

CRUDResource 示例类似,以下示例显示了如何向具有 admin 角色的用户授予更新 MediaLibrary 的权限

package org.acme.library;

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection (1)
public class MediaLibraryPermission extends LibraryPermission {

    public MediaLibraryPermission(String libraryName, String[] actions) {
        super(libraryName, actions, new MediaLibrary());    (2)
    }

}
1 构建本机可执行文件时,除非权限类也用于至少一个 io.quarkus.security.PermissionsAllowed#name 参数中,否则必须注册该权限类以进行反射。有关 @RegisterForReflection 注解的更多详细信息,可以在 本机应用程序提示 页面上找到。
2 我们想将 MediaLibrary 实例传递给 LibraryPermission 构造函数。
quarkus.http.auth.policy.role-policy3.permissions.admin=media-library:list,media-library:read,media-library:write   (1)
quarkus.http.auth.policy.role-policy3.permission-class=org.acme.library.MediaLibraryPermission
quarkus.http.auth.permission.roles3.paths=/library/*
quarkus.http.auth.permission.roles3.policy=role-policy3
1 授予权限 media-library,该权限允许 readwritelist 操作。由于 MediaLibraryTvLibrary 类父类,因此具有 admin 角色的用户也被允许修改 TvLibrary
可以从 Keycloak 提供程序 Dev UI 页面测试 /library/* 路径,因为用户 aliceKeycloak 的 Dev Services 自动创建,具有 admin 角色。

到目前为止提供的示例演示了角色到权限的映射。也可以以编程方式将权限添加到 SecurityIdentity 实例。在以下示例中,SecurityIdentity 被自定义 以添加先前使用基于 HTTP 角色的策略授予的相同权限。

以编程方式将 LibraryPermission 添加到 SecurityIdentity 的示例
import java.security.Permission;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class PermissionsIdentityAugmentor implements SecurityIdentityAugmentor {

    @Override
    public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
        if (isNotAdmin(identity)) {
            return Uni.createFrom().item(identity);
        }
        return Uni.createFrom().item(build(identity));
    }

    private boolean isNotAdmin(SecurityIdentity identity) {
        return identity.isAnonymous() || !"admin".equals(identity.getPrincipal().getName());
    }

    SecurityIdentity build(SecurityIdentity identity) {
        return QuarkusSecurityIdentity.builder(identity)
                .addPermission(new MediaLibraryPermission("media-library", new String[] { "read", "write", "list"}); (1)
                .build();
    }

}
1 添加一个已创建的 media-library 权限可以执行 readwritelist 操作。由于 MediaLibraryTvLibrary 类父类,因此具有 admin 角色的用户也被允许修改 TvLibrary
基于注解的权限不适用于自定义 Jakarta REST SecurityContexts,因为 jakarta.ws.rs.core.SecurityContext 中没有权限。

创建权限检查器

默认情况下,必须使用权限配置 SecurityIdentity,该权限可用于检查此身份是否通过 @PermissionAllowed 授权限制。或者,您可以使用 @PermissionChecker 注解将任何 CDI bean 方法标记为权限检查器。@PermissionChecker 注解值应与 @PermissionsAllowed 注解值声明的所需权限匹配。例如,可以像这样创建权限检查器

package org.acme.security.rest.resource;

import io.quarkus.security.PermissionChecker;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;

@Path("/project/{projectName}")
public class ProjectResource {

    @PermissionsAllowed("rename-project") (1)
    @POST
    public void renameProject(@RestPath String projectName, @RestForm String newName) {
        Project project = Project.findByName(projectName);
        project.name = newName;
    }

    @PermissionChecker("rename-project") (2)
    boolean canRenameProject(SecurityIdentity identity, String projectName) { (3) (4)
        var principalName = identity.getPrincipal().getName();
        var user = User.getUserByName(principalName);
        return userOwnsProject(projectName, user);
    }
}
1 访问 ProjectResource#renameProject 所需的权限是 rename-project 权限。
2 ProjectResource#canRenameProject 方法授权访问 ProjectResource#renameProject 端点。
3 SecurityIdentity 实例可以注入到任何权限检查器方法中。
4 在此示例中,rename-project 权限检查器在同一资源上声明。但是,对可以在何处声明此权限检查器没有限制,如以下示例所示。
权限检查器方法可以在常规作用域的 CDI bean 或 @Singleton bean 上声明。当前不支持 @Dependent CDI bean 作用域。

上面的权限检查器需要 SecurityIdentity 实例来授权 renameProject 端点。您可以直接在资源上声明 rename-project 权限检查器,而不是像此示例中那样在任何 CDI bean 上声明它

package org.acme.security.rest.resource;

import io.quarkus.security.PermissionChecker;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped (1)
public class ProjectPermissionChecker {

    @PermissionChecker("rename-project")
    boolean canRenameProject(String projectName, SecurityIdentity identity) {   (2)
        var principalName = identity.getPrincipal().getName();
        var user = User.getUserByName(principalName);
        return userOwnsProject(projectName, user);
    }

}
1 具有权限检查器的 CDI bean 必须是常规作用域的 bean 或 @Singleton bean。
2 权限检查器方法必须返回 booleanUni<Boolean>。不支持私有检查器方法。
默认情况下,权限检查在事件循环上运行。如果您想在工作线程上运行检查,请使用 io.smallrye.common.annotation.Blocking 注解对权限检查器方法进行注解。

@PermissionsAllowed 值和 @PermissionChecker 值之间的匹配基于字符串相等性,如下面的示例所示

package org.acme.security;

import io.quarkus.security.PermissionChecker;
import io.quarkus.security.PermissionsAllowed;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class FileService {

    @PermissionsAllowed({ "delete:all", "delete:dir" }) (1)
    void deleteDirectory(Path directoryPath) {
        // delete directory
    }

    @PermissionsAllowed(value = { "delete:service", "delete:file" }, inclusive = true) (2)
    void deleteServiceFile(Path serviceFilePath) {
        // delete service file
    }

    @PermissionChecker("delete:all")
    boolean canDeleteAllDirectories(SecurityIdentity identity) {
        String filePermissions = identity.getAttribute("user-group-file-permissions");
        return filePermissions != null && filePermissions.contains("w");
    }

    @PermissionChecker("delete:service")
    boolean canDeleteService(SecurityIdentity identity) {
        return identity.hasRole("admin");
    }

    @PermissionChecker("delete:file")
    boolean canDeleteFile(Path serviceFilePath) {
        return serviceFilePath != null && !serviceFilePath.endsWith("critical");
    }
}
1 权限检查器方法 canDeleteAllDirectories 授予对 deleteDirectory 的访问权限,因为 delete:all 值相等。
2 必须恰好有两个权限检查器方法,一个用于 delete:service 权限,另一个用于 delete:file 权限。

创建权限元注解

@PermissionsAllowed 也可以在元注解中使用。例如,可以像这样创建一个新的 @CanWrite 安全注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import io.quarkus.security.PermissionsAllowed;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@PermissionsAllowed(value = "write", permission = CustomPermission.class) (1)
public @interface CanWrite {

}
1 使用 @CanWrite 注解的任何方法或类都使用此 @PermissionsAllowed 注解实例进行保护。

@BeanParam 参数传递到自定义权限

Quarkus 可以将安全方法的参数字段映射到自定义权限构造函数参数。您可以使用此功能将 jakarta.ws.rs.BeanParam 参数传递到您的自定义权限。让我们考虑以下 Jakarta REST 资源

package org.acme.security.rest.resource;

import io.quarkus.security.PermissionsAllowed;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/hello")
public class HelloResource {

    @PermissionsAllowed(value = "say-hello", params = "beanParam.securityContext.userPrincipal.name") (1)
    @GET
    public String sayHello(@BeanParam SimpleBeanParam beanParam) {
        return "Hello from " + beanParam.uriInfo.getPath();
    }

}
1 params 注解属性指定应将用户主体名称传递给 BeanParamPermissionChecker#canSayHello 方法。其他 BeanParamPermissionChecker#canSayHello 方法参数(如 customAuthorizationHeaderquery)会自动匹配。Quarkus 在 beanParam 字段及其公共访问器中标识 BeanParamPermissionChecker#canSayHello 方法参数。为了避免歧义解析,自动检测仅适用于 beanParam 字段。因此,我们必须显式指定用户主体名称的路径。

其中 SimpleBeanParam 类的声明如下面的示例所示

package org.acme.security.rest.dto;

import java.util.List;

import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.core.UriInfo;

public class SimpleBeanParam {

    @HeaderParam("CustomAuthorization")
    private String customAuthorizationHeader;

    @Context
    SecurityContext securityContext;

    @Context
    public UriInfo uriInfo;

    @QueryParam("query")
    public String query;    (1)

    public SecurityContext getSecurityContext() { (2)
        return securityContext;
    }

    public String customAuthorizationHeader() { (3)
        return customAuthorizationHeader;
    }
}
1 Quarkus Security 只能将公共字段传递给自定义权限构造函数。
2 如果公共 getter 方法可用,Quarkus Security 会自动使用它们。
3 customAuthorizationHeader 字段不是公共字段,因此 Quarkus 使用 customAuthorizationHeader 访问器访问此字段。这对于 Java 记录特别有用,其中生成的访问器不以 get 作为前缀。

以下是一个 @PermissionChecker 方法示例,该方法根据用户主体、自定义标头和查询参数检查 say-hello 权限

package org.acme.security.permission;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.security.PermissionChecker;

@ApplicationScoped
public class BeanParamPermissionChecker {

    @PermissionChecker("say-hello")
    boolean canSayHello(String customAuthorizationHeader, String name, String query) {
        boolean queryParamAllowedForPermissionName = checkQueryParams(query);
        boolean usernameWhitelisted = isUserNameWhitelisted(name);
        boolean customAuthorizationMatches = checkCustomAuthorization(customAuthorizationHeader);
        return queryParamAllowedForPermissionName && usernameWhitelisted && customAuthorizationMatches;
    }

    ...
}
您可以将 @BeanParam 直接传递到 @PermissionChecker 方法并以编程方式访问其字段。当您有多个结构不同的 @BeanParam 类时,使用 @PermissionsAllowed#params 属性引用 @BeanParam 字段的功能非常有用。

相关内容