Authorization of web endpoints

Quarkus 整合了一个可插拔 Web 安全层。当安全处于活动状态时,系统会对所有 HTTP 请求执行权限检查以确定请求是否应继续。

Quarkus incorporates a pluggable web security layer. When security is active, the system performs a permission check on all HTTP requests to determine if they should proceed.

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

Using @PermitAll will not open a path if the path is restricted by the quarkus.http.auth. configuration. To ensure specific paths are accessible, appropriate configurations must be made within the Quarkus security settings.

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

If you use Jakarta RESTful Web Services, consider using quarkus.security.jaxrs.deny-unannotated-endpoints or quarkus.security.jaxrs.default-roles-allowed to set default security requirements instead of HTTP path-level matching because annotations can override these properties on an individual endpoint.

授权基于安全提供程序提供的用户角色。要自定义这些角色,可创建 SecurityIdentityAugmentor,请参阅 Security Identity Customization

Authorization is based on user roles that the security provider provides. To customize these roles, a SecurityIdentityAugmentor can be created, see Security Identity Customization.

Authorization using configuration

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

Permissions are defined in the Quarkus configuration by permission sets, each specifying a policy for access control.

Table 1. {project-name} policies summary
Built-in policy Description

deny

This policy denies all users.

permit

This policy permits all users.

authenticated

This policy permits only authenticated users.

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

You can define role-based policies that allow users with specific roles to access the resources.

Example of a role-based policy
quarkus.http.auth.policy.role-policy1.roles-allowed=user,admin                  1
1 This defines a role-based policy that allows users with the user and admin roles.

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

You can reference a custom policy by configuring the built-in permission sets that are defined in the application.properties file, as outlined in the following configuration example:

Example of policy configuration
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 This permission references the default built-in permit policy to allow GET methods to /public. In this case, the demonstrated setting would not affect this example because this request is allowed anyway.
2 This permission references the built-in deny policy for both /forbidden and /forbidden/ paths. It is an exact path match because it does not end with *.
3 This permission set references the previously defined policy. roles1 is an example name; you can call the permission sets whatever you want.

上例中的确切路径模式 /forbidden 也保护 /forbidden/ 路径。这样,下例中的 forbidden 端点由 deny1 权限保护。

The exact path pattern /forbidden in the example above also secures the /forbidden/ path. This way, the forbidden endpoint in the example below is secured by the deny1 permission.

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 Both /forbidden and /forbidden/ paths need to be secured in order to secure the forbidden endpoint.

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

If you need to permit access to the /forbidden/ path, please add new permission with more specific exact path like in the example below:

quarkus.http.auth.permission.permit1.paths=/forbidden/ 1
quarkus.http.auth.permission.permit1.policy=permit
1 The /forbidden/ path is not secured.

Custom HttpSecurityPolicy

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

Sometimes it might be useful to register your own named policy. You can get it done by creating application scoped CDI bean that implements the io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy interface like in the example below:

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 Uni.createFrom().item(CheckResult.PERMIT);
        }
        return Uni.createFrom().item(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 Named HTTP Security policy will only be applied to requests matched by the application.properties path matching rules.
Example of custom named HttpSecurityPolicy referenced from configuration file
quarkus.http.auth.permission.custom1.paths=/custom/*
quarkus.http.auth.permission.custom1.policy=custom                              1
1 Custom policy name must match the value returned by the io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name method.

您还可以在每个请求上创建一个全局的 HttpSecurityPolicy。只需不要实现 io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name 方法并将策略保持无名。

You can also create global HttpSecurityPolicy invoked on every request. Just do not implement the io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.name method and leave the policy nameless.

Matching on paths and methods

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

Permission sets can also specify paths and methods as a comma-separated list. If a path ends with the * wildcard, the query it generates matches all sub-paths. Otherwise, it queries for an exact match and only matches that specific path:

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 The * wildcard at the end of the path matches zero or more path segments, but never any word starting from the /public path. For that reason, a path like /public-info is not matched by this pattern.

Matching a path but not a method

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

The request is rejected if it matches one or more permission sets based on the path but none of the required methods.

鉴于前面的权限集, GET /public/foo 将同时匹配路径和方法,因此被允许。相比之下, POST /public/foo 将匹配路径但不匹配方法,因此被拒绝。

Given the preceding permission set, GET /public/foo would match both the path and method and therefore be allowed. In contrast, POST /public/foo would match the path but not the method, and, therefore, be rejected.

Matching multiple paths: longest path wins

匹配始终在“最长路径获胜”的基础上进行。如果已经匹配更具体的权限集,则不会考虑不太具体的权限集:

Matching is always done on the "longest path wins" basis. Less specific permission sets are not considered if a more specific one has been matched:

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,请求被拒绝。

Given the preceding permission set, GET /public/forbidden-folder/foo would match both permission sets' paths. However, because the longer path matches the path of the deny1 permission set, deny1 is chosen, and the request is rejected.

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

Subpath permissions precede root path permissions, as the deny1 versus permit1 permission example previously illustrated.

当子路径权限允许访问公共资源而根路径权限需要授权时,此规则通过场景得到了进一步举例说明。

This rule is further exemplified by a scenario where subpath permission allows access to a public resource while the root path permission necessitates authorization.

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

Matching multiple sub-paths: longest path to the * wildcard wins

前面的示例演示了在路径以 * 通配符结尾时如何匹配所有子路径。

Previous examples demonstrated matching all sub-paths when a path concludes with the * wildcard.

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

This wildcard also applies in the middle of a path, representing a single path segment. It cannot be mixed with other path segment characters; thus, path separators always enclose the wildcard, as seen in the /public//about-us path.

当多个路径模式对应于同一请求路径时,系统会选择到 wildcard. In this context, every path segment character is more specific than the 通配符的最长子路径。

When several path patterns correspond to the same request path, the system selects the longest sub-path leading to the wildcard. In this context, every path segment character is more specific than the wildcard.

这是一个简单的示例:

Here is a simple example:

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 Request paths like /api/product/detail can only be accessed by authenticated users.
2 The path /api/public-product/detail is more specific, therefore accessible by anyone.

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

All paths secured with the authorization using configuration should be tested. Writing path patterns with multiple wildcards can be cumbersome. Please make sure paths are authorized as you intended.

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

In the following example, paths are ordered from the most specific to the least specific one:

Request path /one/two/three/four/five matches ordered from the most specific to the least specific path
/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
/*

放在其他任何位置的 wildcard at the end of the path matches zero or more path segments. The 通配符完全匹配一个路径段。

The wildcard at the end of the path matches zero or more path segments. The wildcard placed anywhere else matches exactly one path segment.

Matching multiple paths: most specific method wins

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

When a path is registered with multiple permission sets, the permission sets explicitly specifying an HTTP method that matches the request take precedence. In this instance, the permission sets without methods only come into effect if the request method does not match permission sets with the method specification.

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 并且请求被接受。

The preceding permission set shows that GET /public/foo matches the paths of both permission sets.However, it specifically aligns with the explicit method of the permit1 permission set.Therefore, permit1 is selected, and the request is accepted.

相比之下,PUT /public/foo 则不匹配 permit1 的方法权限。因此,deny1 被激活,导致请求被拒绝。

In contrast, PUT /public/foo does not match the method permissions of permit1. As a result, deny1 is activated, leading to the rejection of the request.

Matching multiple paths and methods: both win

有时,前面描述的规则允许多个权限集同时获胜。在这种情况下,为继续进行请求,所有权限都必须允许访问。要实现此目的,两者都必须指定该方法或不包含任何方法。方法特定匹配优先。

Sometimes, the previously described rules allow multiple permission sets to win simultaneously. In that case, for the request to proceed, all the permissions must allow access. For this to happen, both must either have specified the method or have no method. Method-specific matches take precedence.

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 角色。

Given the preceding permission set, GET /api/foo would match both permission sets' paths, requiring both the user and admin roles.

Configuration properties to deny access

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

The following configuration settings alter the role-based access control (RBAC) denying behavior:

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

If set to true, access is denied for all Jakarta REST endpoints by default. If a Jakarta REST endpoint has no security annotations, it defaults to the @DenyAll behavior. This helps you to avoid accidentally exposing an endpoint that is supposed to be secured. Defaults to false.

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

Defines the default role requirements for unannotated endpoints. The ** role is a special role that means any authenticated user. This cannot be combined with deny-unannotated-endpoints because deny takes effect instead.

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

If set to true, the access is denied to all CDI methods and Jakarta REST endpoints that do not have security annotations but are defined in classes that contain methods with security annotations. Defaults to false.

Disabling permissions

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

Permissions can be disabled at build time with an enabled property for each declared permission, such as:

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

Permissions can be reenabled at runtime with a system property or environment variable, such as: -Dquarkus.http.auth.permission.permit1.enabled=true.

Permission paths and HTTP root path

quarkus.http.root-path 配置属性会更改 http endpoint context path

The quarkus.http.root-path configuration property changes the http endpoint context path.

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

By default, quarkus.http.root-path is prepended automatically to configured permission paths then do not use a forward slash, for example:

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

此配置等效于以下内容:

This configuration is equivalent to the following:

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 的值改变时不会调整路径。

A leading slash changes how the configured permission path is interpreted. The configured URL is used as-is, and paths are not adjusted if the value of quarkus.http.root-path changes.

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

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

This configuration only impacts resources served from the fixed or static URL, /public, which might not match your application resources if quarkus.http.root-path has been set to something other than /.

有关更多信息,请参阅 Path Resolution in Quarkus

For more information, see Path Resolution in Quarkus.

Map SecurityIdentity roles

获胜角色型策略可以将 SecurityIdentity 角色映射到特定于部署的角色。随后,可使用 @RolesAllowed 注释应用这些角色进行端点授权。

Winning role-based policy can map the SecurityIdentity roles to the deployment-specific roles. These roles are then applicable for endpoint authorization by using the @RolesAllowed annotation.

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 Map the admin role to Admin1 role. The SecurityIdentity will have both admin and Admin1 roles.
2 The /* path is secured, only authenticated HTTP requests are granted access.

如果你需要将 SecurityIdentity 角色映射到特定于部署的角色,而不考虑路径,你还可以执行以下操作:

If all that you need is to map the SecurityIdentity roles to the deployment-specific roles regardless of a path, you can also do this:

quarkus.http.auth.roles-mapping.admin=Admin1 1 2
1 Map the admin role to Admin1 role. The SecurityIdentity will have both admin and Admin1 roles.
2 The /* path is not secured. You must secure your endpoints with standard security annotations or define HTTP permissions in addition to this configuration property.

Shared permission checks

一个关于未共享权限检查的重要规则是只应用一个路径匹配,即最具体的匹配。自然而然,你可以根据需要指定具有相同获胜路径的众多权限,并且所有权限都将应用。但是,可能有你想应用到许多路径的权限检查,而不必一次又一次地重复它们。这就是共享权限检查发挥作用的地方,当权限路径匹配时,它们始终被应用。

One important rule for unshared permission checks is that only one path match is applied, the most specific one. Naturally you can specify as many permissions with the same winning path as you want and they will all be applied. However, there can be permission checks you want to apply to many paths without repeating them over and over again. That’s where shared permission checks come in, they are always applied when the permission path is matched.

Example of custom named HttpSecurityPolicy applied on every HTTP request
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 Custom HttpSecurityPolicy will be also applied on the /admin/1 path together with the admin-policy1 policy.

配置许多共享权限检查不如配置未共享权限检查有效。使用共享权限来补充未共享权限检查,就像下面的示例中。

Configuring many shared permission checks is less effective than configuring unshared ones. Use shared permissions to complement unshared permission checks like in the example below.

Map SecurityIdentity roles with shared permission
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 Role root will be able to access /secured/user/ and /secured/admin/ paths.
2 The /secured/* path can only be accessed by authenticated users. This way, you have secured the /secured/all path and so on.
3 Shared permissions are always applied before unshared ones, therefore a SecurityIdentity with the root role will have the user role as well.

Authorization using annotations

{project-name} 包括内置安全功能,以便根据 REST 端点和 CDI bean 上常见的安全注释 @RolesAllowed@DenyAll@PermitAll 来允许 Role-Based Access Control (RBAC)

{project-name} includes built-in security to allow for Role-Based Access Control (RBAC) based on the common security annotations @RolesAllowed, @DenyAll, @PermitAll on REST endpoints and CDI beans.

Table 2. {project-name} annotation types summary
Annotation type Description

@DenyAll

Specifies that no security roles are allowed to invoke the specified methods.

@PermitAll

Specifies that all security roles are allowed to invoke the specified methods.

@PermitAll lets everybody in, even without authentication.

@RolesAllowed

Specifies the list of security roles allowed to access methods in an application.

As an equivalent to @RolesAllowed("**"), {project-name} also provides the io.quarkus.security.Authenticated annotation that permits any authenticated user to access the resource.

下面的 [subject-example] 演示了一个端点,它使用 Jakarta REST 和通用安全注释来描述和保护其端点。

The following [subject-example] demonstrates an endpoint that uses both Jakarta REST and Common Security annotations to describe and secure its endpoints.

SubjectExposingResource example
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("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("unsecured")
    @PermitAll 3
    public String getSubjectUnsecured(@Context SecurityContext sec) {
        Principal user = sec.getUserPrincipal(); 4
        String name = user != null ? user.getName() : "anonymous";
        return name;
    }

    @GET
    @Path("denied")
    @DenyAll 5
    public String getSubjectDenied(@Context SecurityContext sec) {
        Principal user = sec.getUserPrincipal();
        String name = user != null ? user.getName() : "anonymous";
        return name;
    }
}
1 The /subject/secured endpoint requires an authenticated user with the granted "Tester" role through the use of the @RolesAllowed("Tester") annotation.
2 The endpoint obtains the user principal from the Jakarta REST SecurityContext. This returns non-null for a secured endpoint.
3 The /subject/unsecured endpoint allows for unauthenticated access by specifying the @PermitAll annotation.
4 The call to obtain the user principal returns null if the caller is unauthenticated and non-null if the caller is authenticated.
5 The /subject/denied endpoint declares the @DenyAll annotation, disallowing all direct access to it as a REST method, regardless of the user calling it. The method is still invokable internally by other methods in this class.

如果您计划在 IO 线程上使用标准安全性注解,请查看 Proactive Authentication 中的信息。

If you plan to use standard security annotations on the IO thread, review the information in Proactive Authentication.

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

The @RolesAllowed annotation value supports property expressions including default values and nested property expressions. Configuration properties used with the annotation are resolved at runtime.

Table 3. Annotation value examples
Annotation Value explanation

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

The endpoint allows users with the role denoted by the value of the admin-role property.

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

An example showing that the value can contain multiple variables.

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

A default value demonstration. The required role is denoted by the value of the customer property. However, if that property is not specified, a role named User is required as a default.

Example of a property expressions usage in the @RolesAllowed annotation
admin=Administrator
tester.group=Software
tester.role=Tester
%prod.secured=User
%dev.secured=**
all-roles=Administrator,Software,Tester,User
Subject access control example
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("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 The @RolesAllowed annotation value is set to the value of Administrator.
2 This /subject/software-tester endpoint requires an authenticated user that has been granted the role of "Software-Tester". It is possible to use multiple expressions in the role definition.
3 This /subject/user endpoint requires an authenticated user that has been granted the role "User" through the use of the @RolesAllowed("${customer:User}") annotation because we did not set the configuration property customer.
4 In production, this /subject/secured endpoint requires an authenticated user with the User role. In development mode, it allows any authenticated user.
5 Property expression all-roles will be treated as a collection type List, therefore, the endpoint will be accessible for roles Administrator, Software, Tester and User.

Endpoint security annotations and Jakarta REST inheritance

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

Quarkus supports security annotations placed on the endpoint implementation or its class like in the example below:

@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 Class-level security annotation must be placed on the class where the endpoint implementation is declared.
2 Method-level security annotation must be placed on the endpoint implementation.

作为默认接口方法声明的 RESTEasy 子资源定位符无法通过标准安全注释得到保护.安全子资源定位符必须在接口实现中实现和保护,如下面的示例:

The RESTEasy subresource locators declared as default interface methods cannot be secured by standard security annotations. Secured subresource locators must be implemented and secured on the interface implementors like in the example below:

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

Permission annotation

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

Quarkus also provides the io.quarkus.security.PermissionsAllowed annotation, which authorizes any authenticated user with the given permission to access the resource. This annotation is an extension of the common security annotations and checks the permissions granted to a SecurityIdentity instance.

Example of endpoints secured with the @PermissionsAllowed annotation
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 The resource method createOrUpdate is only accessible for a user with both create and update permissions.
2 By default, at least one of the permissions specified through one annotation instance is required. You can require all permissions by setting inclusive=true. Both resource methods createOrUpdate have equal authorization requirements.
3 Access is granted to getItem if SecurityIdentity has either read permission or see permission and one of the all or detail actions.
4 You can use your preferred java.security.Permission implementation. By default, string-based permission is performed by io.quarkus.security.StringPermission.
5 Permissions are not beans, therefore the only way to obtain bean instances is programmatically by using Arc.container().

如果你计划在 IO 线程上使用 @12,请查阅 @13 中的信息。

If you plan to use the @PermissionsAllowed on the IO thread, review the information in Proactive Authentication.

@14 由于 Quarkus 拦截器的限制,在类级别上不可重复。有关详细信息,请参阅 Quarkus “CDI 参考”指南的 @15 部分。

@PermissionsAllowed is not repeatable on the class level due to a limitation with Quarkus interceptors. For more information, see the Repeatable interceptor bindings section of the Quarkus "CDI reference" guide.

向支持角色的 @16 实例添加权限的最简单方式是将角色映射到权限。使用 @19 为认证请求授予 @18 端点的所需 @17 权限,如下例所示:

The easiest way to add permissions to a role-enabled SecurityIdentity instance is to map roles to permissions. Use Authorization using configuration to grant the required SecurityIdentity permissions for CRUDResource endpoints to authenticated requests, as outlined in the following example:

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 Add the permission see and the action all to the SecurityIdentity instance of the user role. Similarly, for the @PermissionsAllowed annotation, io.quarkus.security.StringPermission is used by default.
2 Permissions create, update, and read are mapped to the role admin.
3 The role policy role-policy1 allows only authenticated requests to access /crud/modify and /crud/id sub-paths. For more information about the path-matching algorithm, see Matching multiple paths: longest path wins later in this guide.
4 You can specify a custom implementation of the java.security.Permission class. Your custom class must define exactly one constructor that accepts the permission name and optionally some actions, for example, String array. In this scenario, the permission list is added to the SecurityIdentity instance as new CustomPermission("list").

你还可以创建带有附加构造函数参数的自定义 @39 类。这些附加参数与使用 @40 注释的方法的自变量相匹配。随后,Quarkus 使用实际参数实体化你的自定义权限,已调用使用 @41 注释的方法。

You can also create a custom java.security.Permission class with additional constructor parameters. These additional parameters get matched with arguments of the method annotated with the @PermissionsAllowed annotation. Later, Quarkus instantiates your custom permission with actual arguments, with which the method annotated with the @PermissionsAllowed has been invoked.

Example of a custom java.security.Permission class that accepts additional arguments
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 There must be exactly one constructor of a custom Permission class. The first parameter is always considered to be a permission name and must be of type String. Quarkus can optionally pass permission actions to the constructor. For this to happen, declare the second parameter as String[].

如果 @46 被允许执行所需的操作之一(例如 @47、@48 或 @49),则 @45 类允许访问当前或父库。

The LibraryPermission class permits access to the current or parent library if SecurityIdentity is allowed to perform one of the required actions, for example, read, write, or list.

以下示例展示了如何使用 @50 类:

The following example shows how the LibraryPermission class can be used:

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 update) {
        update.description = newDesc;
        return update;
    }

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

}
1 The formal parameter update is identified as the first Library parameter and gets passed to the LibraryPermission class. However, the LibraryPermission must be instantiated each time the updateLibrary method is invoked.
2 Here, the first Library parameter is migrate; therefore, the library parameter gets marked explicitly through PermissionsAllowed#params. The permission constructor and the annotated method must have the parameter library set; otherwise, validation fails.
Example of a resource secured with the 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);
    }

}

与 @61 示例类似,以下示例展示了如何向具有 @62 角色的用户授予更新 @63 的权限:

Similarly to the CRUDResource example, the following example shows how you can grant a user with the admin role permissions to update 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 When building a native executable, the permission class must be registered for reflection unless it is also used in at least one io.quarkus.security.PermissionsAllowed#name parameter. More details about the @RegisterForReflection annotation can be found on the native application tips page.
2 We want to pass the MediaLibrary instance to the LibraryPermission constructor.
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 Grants the permission media-library, which permits read, write, and list actions. Because MediaLibrary is the TvLibrary class parent, a user with the admin role is also permitted to modify TvLibrary.

从 Keycloak 提供程序的 Dev UI 页面可以测试 /library/* 路径,因为由 Dev Services for Keycloak 自动创建的用户 alice`具有 `admin 角色。

The /library/* path can be tested from a Keycloak provider Dev UI page, because the user alice which is created automatically by the Dev Services for Keycloak has an admin role.

到目前为止,提供的示例演示了角色对权限的映射。还可以以编程方式向 SecurityIdentity 实例添加权限。在以下示例中,SecurityIdentity is customized 用来添加之前通过基于 HTTP 角色的策略授予的相同的权限。

The examples provided so far demonstrate role-to-permission mapping. It is also possible to programmatically add permissions to the SecurityIdentity instance. In the following example, SecurityIdentity is customized to add the same permission that was previously granted with the HTTP role-based policy.

Example of adding the LibraryPermission programmatically to SecurityIdentity
import java.security.Permission;
import java.util.function.Function;

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) {
        Permission possessedPermission = new MediaLibraryPermission("media-library",
                new String[] { "read", "write", "list"}); 1
        return QuarkusSecurityIdentity.builder(identity)
                .addPermissionChecker(new Function<Permission, Uni<Boolean>>() { 2
                    @Override
                    public Uni<Boolean> apply(Permission requiredPermission) {
                        boolean accessGranted = possessedPermission.implies(requiredPermission);
                        return Uni.createFrom().item(accessGranted);
                    }
                })
                .build();
    }

}
1 The permission media-library that was created can perform read, write, and list actions. Because MediaLibrary is the TvLibrary class parent, a user with the admin role is also permitted to modify TvLibrary.
2 You can add a permission checker through io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder#addPermissionChecker.

基于注释的权限不适用于自定义 Jakarta REST SecurityContexts,因为 jakarta.ws.rs.core.SecurityContext 中没有权限。

Annotation-based permissions do not work with custom Jakarta REST SecurityContexts because there are no permissions in jakarta.ws.rs.core.SecurityContext.