REST API 是任何现代软件应用程序的核心。保护对 REST API 的访问对于防止未经授权的操作和保护敏感数据至关重要。此外,公司必须遵守法规和标准才能成功运营。
本文介绍了如何在Quarkus Java框架中使用基于角色的访问控制(RBAC)来保护REST API。Quarkus 是一个开源的全栈 Java 框架,专为构建云原生容器化应用程序而设计。Quarkus Java 框架附带了对 RBAC 的原生支持,这将是本文的初始重点。此外,本文还将介绍如何构建自定义解决方案来保护 REST 端点。
概念
认证:身份验证是验证用户身份的过程,通常涉及使用用户名和密码。(但是,也可以采用其他方法,例如生物识别和双因素身份验证)。身份验证是安全的关键要素,对于保护系统和资源免受未经授权的访问至关重要。
授权:授权是验证用户是否具有访问特定资源或执行操作的必要权限的过程。通常,授权在身份验证之后进行。可以使用多种方法(例如基于角色的访问控制和基于属性的访问控制)来实现授权。
基于角色的访问控制:基于角色的访问控制 (RBAC) 是一种安全模型,它根据分配给用户的角色授予对资源的访问权限。在 RBAC 中,将用户分配给特定角色,并为每个角色授予执行其工作职能所需的权限。
网关:在传统的软件设置中,网关负责对客户端进行身份验证,并验证客户端是否具有访问资源所需的权限。网关身份验证在保护基于微服务的架构方面发挥着关键作用,因为它允许组织实施集中式身份验证。
基于令牌的身份验证:这是一种技术,网关在成功进行身份验证后向客户端提供访问令牌。然后,客户端在每个后续请求中向网关提供访问令牌。
智威汤逊:JSON Web 令牌 (JWT) 是一种广泛接受的标准,用于以 JSON 对象的形式在各方之间安全地传输信息。成功登录后,网关会生成一个 JWT 并将其发送回客户端。然后,客户端将 JWT 包含在对服务器的每个后续请求的标头中。JWT 可以包含所需的权限,这些权限可用于根据用户的授权级别允许或拒绝对 API 的访问。
应用实例
考虑一个简单的应用程序,其中包含用于创建和检索任务的 REST API。
应用程序有两个用户角色:
管理员:允许读取和写入。
成员:允许只读。
管理员和成员可以访问 GET API;但是,只有管理员有权使用 POST API。
@Path("/task")
public class TaskResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getTask() {
return "Task Data";
}
@POST
@Produces(MediaType.TEXT_PLAIN)
public String createTask() {
return "Valid Task received";
}
}
配置Quarkus安全模块
为了在 Quarkus 中处理和验证传入的 JWT,需要包含以下 JWT 安全模块。
对于基于 maven 的项目,请将以下内容添加到pom.xml
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-jwt</artifactId>
<scope>test</scope>
</dependency>
对于基于 gradle 的项目,请添加以下内容:
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-smallrye-jwt-build")
testImplementation("io.quarkus:quarkus-test-security-jwt")
实施 RBAC
Quarkus提供内置的RBAC支持,以基于用户角色保护REST API。这可以通过几个步骤完成。
第 1 步
利用 Quarkus 内置的 RBAC 支持的第一步是用允许访问它们的角色来注释 API。要添加的注解是 ,这是一个 JSR 250 安全注解,它指示只有当用户属于指定角色时才能访问给定的端点。@RolesAllowed
@GET
@RolesAllowed({"Admin", "Member"})
@Produces(MediaType.TEXT_PLAIN)
public String getTask() {
return "Task Data";
}
@POST
@RolesAllowed({"Admin"})
@Produces(MediaType.TEXT_PLAIN)
public String createTask() {
return "Valid Task received";
}
步骤 2
下一步是配置颁发者 URL 和公钥。这使Quarkus能够验证JWT并确保它没有被篡改。这可以通过将以下属性添加到位于文件夹中的文件来完成。application.properties /resources
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://myapp.com/issuer
quarkus.native.resources.includes=publicKey.pem
mp.jwt.verify.publickey.location- 此配置指定 Quarkus 的公钥位置,该公钥必须位于类路径中。Quarkus寻找的默认位置是文件夹。/resources
mp.jwt.verify.issuer- 此属性表示令牌的颁发者,该令牌的创建者使用其私钥对其进行签名。
quarkus.native.resources.includes- 此属性通知 Quarks 将公钥作为资源包含在原生可执行文件中。
步骤 3
最后一步是将公钥添加到应用程序。创建一个名为 的文件,将公钥保存在其中。将文件复制到位于目录中的文件夹。publicKey.pem/resources/src
测试
Quarkus为单元测试提供了强大的支持,以确保代码质量,特别是在RBAC方面。使用注解,可以定义用户角色,并可以生成 JWT 以从单元测试中调用 REST API。@TestSecurity
@Test
@TestSecurity(user = "testUser", roles = "Admin")
public void testTaskPostEndpoint() {
given().log().all()
.body("{id: task1}")
.when().post("/task")
.then()
.statusCode(200)
.body(is("Valid Task received"));
}
自定义 RBAC 实现
随着应用程序的增长并包含其他功能,内置的 RBAC 支持可能会变得不足。编写良好的应用程序允许用户创建具有与其关联的特定权限的自定义角色。请务必分离角色和权限,并避免在代码中对其进行硬编码。角色可以被视为权限的集合,并且每个 API 都可以使用访问它所需的权限进行标记。
为了分离角色和权限并为用户提供灵活性,让我们扩展我们的示例应用程序,以包含两个任务权限。
task:read:权限将允许用户读取任务
task:write:权限将允许用户创建或修改任务。
然后,我们可以将这些权限与两个角色相关联:“管理员”和“成员”
管理员:已分配读取和写入。[“task:read”, “task:write”]
成员:只会读。[“task:read”]
第 1 步
为了将每个 API 与权限相关联,我们需要一个自定义注释来简化其用法和应用。让我们创建一个名为 的新注解,它接受用户必须具有的字符串权限才能调用 API。@Permissions
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Permissions {
String[] value();
}
步骤 2
可以将注释添加到任务 API 中,以指定访问它们所需的权限。如果用户具有 OR 权限,则可以访问 GET 任务 API,而 POST 任务 API 只有在用户具有权限时才能访问。@Permissionstask: readtask: writetask: write
@GET
@Permissions({"task:read", "task:write"})
@Produces(MediaType.TEXT_PLAIN)
public String getTask() {
return "Task Data";
}
@POST
@Permissions("task:write")
@Produces(MediaType.TEXT_PLAIN)
public String createTask() {
return "Valid Task received";
}
步骤 3
最后一步涉及添加一个筛选器,用于拦截 API 请求并验证包含的 JWT 是否具有调用 REST API 所需的权限。JWT 必须将用户 ID 作为声明的一部分包含在声明中,这在典型应用程序中就是这种情况,因为 JWT 令牌中包含某种形式的用户标识
反射 API 用于确定调用的方法及其关联的注释。在提供的代码中,映射和映射存储在 HashMaps 中。在实际场景中,将从数据库中检索并缓存此信息,以便更快地访问。user -> rolerole -> permissions
@Provider
public class PermissionFilter implements ContainerRequestFilter {
@Context
ResourceInfo resourceInfo;
@Inject
JsonWebToken jwt;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Method method = resourceInfo.getResourceMethod();
Permissions methodPermAnnotation = method.getAnnotation(Permissions.class);
if(methodPermAnnotation != null && checkAccess(methodPermAnnotation)) {
System.out.println("Verified permissions");
} else {
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build());
}
}
/**
* Verify if JWT permissions match the API permissions
*/
private boolean checkAccess(Permissions perm) {
boolean verified = false;
if(perm == null) {
//If no permission annotation verification failed
verified = false;
} else if(jwt.getClaim("userId") == null) {
// Don’t support Anonymous users
verified = false;
}
else {
String userId = jwt.getClaim("userId");
String role = getRolesForUser(userId);
String[] userPermissions = getPermissionForRole(role);
if(Arrays.asList(userPermissions).stream()
.anyMatch(userPerm -> Arrays.asList(perm.value()).contains(userPerm))) {
verified = true;
}
}
return verified;
}
// role -> permission mapping
private String[] getPermissionForRole(String role) {
Map<String, String[]> rolePermissionMap = new HashMap<>();
rolePermissionMap.put("Admin", new String[] {"task:write", "task:read"});
rolePermissionMap.put("Member", new String[] {"task:read"});
return rolePermissionMap.get(role);
}
// userId -> role mapping
private String getRolesForUser(String userId) {
Map<String, String> userMap = new HashMap<>();
userMap.put("1234", "Admin");
userMap.put("6789", "Member");
return userMap.get(userId);
}
}
测试
与测试内置 RBAC 的方式类似,注释可用于创建用于测试目的的 JWT。此外,Quarkus库还提供了注释,允许向JWT添加额外的声明,包括userId声明。@TestSecurity @JwtSecurity
@Test
@TestSecurity(user = "testUser", roles = "Admin")
@JwtSecurity(claims = {
@Claim(key = "userId", value = "1234")
})
public void testTaskPosttEndpoint() {
given().log().all()
.body("{id: task1}")
.when().post("/task")
.then()
.statusCode(200)
.body(is("Task edited"));
}
@Test
@TestSecurity(user = "testUser", roles = "Admin")
@JwtSecurity(claims = {
@Claim(key = "userId", value = "6789")
})
public void testTaskPostMember() {
given().log().all()
.body("{id: task1}")
.when().post("/task")
.then()
.statusCode(403);
}
结论
随着网络攻击的不断增加,保护 REST API 变得越来越重要。潜在的安全漏洞可能会给公司带来巨大的经济损失和声誉损害。虽然 Quarkus 是一个多功能的 Java 框架,它提供了内置的 RBAC 支持来保护 REST API,但在某些情况下,它的原生支持可能不足,尤其是对于细粒度的访问控制。上面的文章涵盖了Quarkus中内置RBAC支持的实现,以及Quarkus中基于角色的自定义访问控制解决方案的开发和测试。