1.2. 使用注解进行授权
红帽构建的 Quarkus 包含内置的安全性,允许基于常见安全注解 @RolesAllowed、@DenyAll、@PermitAll on REST 端点和 CDI Bean 来允许 基于角色的访问控制(RBAC)。
| 注解类型 | 描述 |
|---|---|
|
| 指定不允许安全角色调用指定的方法。 |
|
| 指定允许所有安全角色调用指定的方法。
|
|
| 指定允许访问应用程序中方法的安全角色列表。 |
|
|
红帽构建的 Quarkus 提供了 |
|
| 指定允许调用指定方法的权限列表。 |
|
|
指定 named |
以下 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")
public String getSubjectSecured(@Context SecurityContext sec) {
Principal user = sec.getUserPrincipal();
String name = user != null ? user.getName() : "anonymous";
return name;
}
@GET
@Path("authenticated")
@Authenticated
public String getSubjectAuthenticated(@Context SecurityContext sec) {
Principal user = sec.getUserPrincipal();
String name = user != null ? user.getName() : "anonymous";
return name;
}
@GET
@Path("unsecured")
@PermitAll
public String getSubjectUnsecured(@Context SecurityContext sec) {
Principal user = sec.getUserPrincipal();
String name = user != null ? user.getName() : "anonymous";
return name;
}
@GET
@Path("denied")
@DenyAll
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获取用户主体。这会为安全端点返回非null。 - 3
/subject/authenticated端点可通过指定@Authenticated注释来允许任何经过身份验证的用户。- 4
/subject/unsecured端点通过指定@PermitAll注释来允许未经身份验证的访问。- 5
- 如果调用者被验证,则获取用户主体的调用会返回
。null - 6
/subject/denied端点声明@DenyAll注释,不允许以 REST 方法直接访问它,而不考虑调用它的用户。此方法仍然可由此类中的其他方法在内部执行。
如果您计划在 IO 线程上使用标准安全注解,请查看 主动身份验证 中的信息。
@RolesAllowed 注释值支持属性 表达式,包括默认值和嵌套属性表达式。与注解搭配使用的配置属性会在运行时解决。
| 注解 | 值解释 |
|---|---|
|
|
端点允许具有由 |
|
| 显示该值可包含多个变量的示例。 |
|
|
默认值演示。所需的角色由 |
@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}")
public String getSubjectSecuredAdmin(@Context SecurityContext sec) {
return getUsername(sec);
}
@GET
@Path("software-tester")
@RolesAllowed("${tester.group}-${tester.role}")
public String getSubjectSoftwareTester(@Context SecurityContext sec) {
return getUsername(sec);
}
@GET
@Path("user")
@RolesAllowed("${customer:User}")
public String getSubjectUser(@Context SecurityContext sec) {
return getUsername(sec);
}
@GET
@Path("secured")
@RolesAllowed("${secured}")
public String getSubjectSecured(@Context SecurityContext sec) {
return getUsername(sec);
}
@GET
@Path("list")
@RolesAllowed("${all-roles}")
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端点需要一个经过身份验证的用户,该用户已通过使用@RolesAllowed ("${customer:User}")注解授予角色"User",因为我们没有设置配置属性客户。 - 4
- 在生产环境中,这个
/subject/secured端点需要一个具有User角色的经过身份验证的用户。在开发模式中,它允许任何经过身份验证的用户。 - 5
- 属性表达式
all-roles将被视为集合类型列表,因此可为角色管理员、软件、测试程序和用户访问端点。
1.2.1. 端点安全注解和 Jakarta REST 继承 复制链接链接已复制到粘贴板!
Quarkus 支持放置在端点实现或其类上,如下例所示:
@Path("hello")
public interface HelloInterface {
@GET
String hello();
}
@DenyAll
public class HelloInterfaceImpl implements HelloInterface {
@RolesAllowed("admin")
@Override
public String hello() {
return "Hello";
}
}
声明为默认接口方法的 RESTEasy 子资源 locators 无法被标准安全注解保护。必须在接口实现器上实现并保护安全的子资源 locators,如下例所示:
@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();
}
}
1.2.2. 权限注解 复制链接链接已复制到粘贴板!
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")
@PermissionsAllowed("update")
@POST
@Path("/modify/repeated")
public String createOrUpdate() {
return "modified";
}
@PermissionsAllowed(value = {"create", "update"}, inclusive=true)
@POST
@Path("/modify/inclusive")
public String createOrUpdate(Long id) {
return id + " modified";
}
@PermissionsAllowed({"see:detail", "see:all", "read"})
@GET
@Path("/id/{id}")
public String getItem(String id) {
return "item-detail-" + id;
}
@PermissionsAllowed(value = "list", permission = CustomPermission.class)
@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();
var publicContent = "public-content".equals(event.request().params().get("query-options"));
var hasPermission = getName().equals(permission.getName());
return hasPermission && publicContent;
}
}
}
- 1
- 资源方法
createOrUpdate只能被具有create和update权限的用户访问。 - 2
- 默认情况下,至少需要通过一个注解实例指定的权限之一。您可以通过设置
inclusive=true来要求所有权限。两种资源方法createOrUpdate都有相等的授权要求。 - 3
- 如果
SecurityIdentity有读取权限,则授予getItem权限或查看权限以及all或detail操作之一。 - 4
- 您可以使用您首选的
java.security.Permission实现。默认情况下,基于字符串的权限由io.quarkus.security.StringPermission执行。 - 5
- 权限不是 Bean,因此获取 bean 实例的唯一方式是使用
Arc.container ()以编程方式。
如果您计划在 IO 线程上使用 @PermissionsAllowed,请查看 主动身份验证 中的信息。
由于 Quarkus 拦截器的限制,@PermissionsAllowed 在类级别上不能可重复。如需更多信息,请参阅 Quarkus "CDI 参考"指南中的 Repeatable interceptor bindings 部分。
为启用了角色的 SecurityIdentity 实例添加权限的最简单方法是将角色映射到权限。使用 Authorization using configuration 将 CRUDResource 端点所需的 SecurityIdentity 权限授予经过身份验证的用户,如下例所示:
quarkus.http.auth.policy.role-policy1.permissions.user=see:all
quarkus.http.auth.policy.role-policy1.permissions.admin=create,update,read
quarkus.http.auth.permission.roles1.paths=/crud/modify/*,/crud/id/*
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
quarkus.http.auth.permission.roles2.paths=/crud/list
quarkus.http.auth.permission.roles2.policy=role-policy2
- 1
- 将权限
see和all添加到用户角色的SecurityIdentity实例中。同样,对于@PermissionsAllowed注释,默认使用io.quarkus.security.StringPermission。 - 2
- 权限
创建、update和read被映射到角色admin。 - 3
- 4
- 您可以指定
java.security.Permission类的自定义实现。自定义类必须精确定义一个构造器,它接受权限名称和可选的一些操作,如String数组。在这种情况下,权限列表被添加到SecurityIdentity实例中,作为新的 CustomPermission ("。list")
您还可以使用额外的构造器参数创建自定义 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) {
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
- 自定义
权限类必须只有一个构造器。第一个参数始终被视为权限名称,必须是String类型。Quarkus 可以选择性地将权限操作传递给构造器。为此,请将第二个参数声明为String[]。
如果允许 SecurityIdentity 执行其中一个必要操作(如 读取、写入 或 列表 ),则 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)
public Library updateLibrary(String newDesc, Library library) {
library.description = newDesc;
return library;
}
@PermissionsAllowed(value = "tv:write", permission = LibraryPermission.class)
@PermissionsAllowed(value = {"tv:read", "tv:list"}, permission = LibraryPermission.class)
public Library migrateLibrary(Library migrate, Library library) {
// migrate libraries
return library;
}
}
使用 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
public class MediaLibraryPermission extends LibraryPermission {
public MediaLibraryPermission(String libraryName, String[] actions) {
super(libraryName, actions, new MediaLibrary());
}
}
quarkus.http.auth.policy.role-policy3.permissions.admin=media-library:list,media-library:read,media-library:write
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,允许读取、写入和列出操作。因为MediaLibrary是TvLibrary类父级,因此还允许具有admin角色的用户修改TvLibrary。
可以在 Keycloak 供应商 Dev UI 页面中测试 /library RAID 路径,因为用户 alice 由 Keycloak 的 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"});
.build();
}
}
- 1
- 添加可执行
读取、写入和列出操作的media-library权限。因为MediaLibrary是TvLibrary类父级,因此还允许具有admin角色的用户修改TvLibrary。
基于注解的权限不适用于自定义 Jakarta REST SecurityContexts,因为 jakarta.ws.rs.core.SecurityContext 中没有权限。
1.2.2.1. 创建权限检查器 复制链接链接已复制到粘贴板!
默认情况下,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")
@POST
public void renameProject(@RestPath String projectName, @RestForm String newName) {
Project project = Project.findByName(projectName);
project.name = newName;
}
@PermissionChecker("rename-project")
boolean canRenameProject(SecurityIdentity identity, String projectName) {
var principalName = identity.getPrincipal().getName();
var user = User.getUserByName(principalName);
return userOwnsProject(projectName, user);
}
}
权限检查器方法可以在普通范围的 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
public class ProjectPermissionChecker {
@PermissionChecker("rename-project")
boolean canRenameProject(String projectName, SecurityIdentity identity) {
var principalName = identity.getPrincipal().getName();
var user = User.getUserByName(principalName);
return userOwnsProject(projectName, user);
}
}
默认情况下,权限检查在事件循环上运行。如果要在 worker 线程上运行检查,使用 io.smallrye.common.annotation.Blocking 注解注解权限检查程序方法。
@PermissionsAllowed 值和 @PermissionChecker 值之间的匹配基于字符串 equality,如下例所示:
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" })
void deleteDirectory(Path directoryPath) {
// delete directory
}
@PermissionsAllowed(value = { "delete:service", "delete:file" }, inclusive = true)
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.2.2.2. 创建权限 meta-annotations 复制链接链接已复制到粘贴板!
@PermissionsAllowed 也可以用于 meta-annotations。例如,可以创建一个新的 @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)
public @interface CanWrite {
}
- 1
- 使用
@CanWrite注释标注的任何方法或类都通过@PermissionsAllowed注释实例进行保护。
1.2.2.3. 将 @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")
@GET
public String sayHello(@BeanParam SimpleBeanParam beanParam) {
return "Hello from " + beanParam.uriInfo.getPath();
}
}
- 1
params注解属性指定用户主体名称应传递给BeanParamPermissionChecker#canSayHello方法。其他BeanParamPermissionChecker#canSayHello方法参数(如customAuthorizationHeader和查询)会自动匹配。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;
public SecurityContext getSecurityContext() {
return securityContext;
}
public String customAuthorizationHeader() {
return customAuthorizationHeader;
}
}
下面是一个 @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 Serialparams 属性引用 @BeanParam 字段的功能很有用。