编辑此页面

Quarkus Spring Security API 扩展

虽然鼓励用户使用 Java 标准注解进行安全授权,但 Quarkus 以 spring-security 扩展的形式为 Spring Security 提供了兼容层。

本指南介绍了 Quarkus 应用程序如何利用广为人知的 Spring Security 注解,在 RESTful 服务上使用角色定义授权。

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

  • 需要对 Spring Web 扩展有一定的了解

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git,或者下载一个 归档文件

解决方案位于 spring-security-quickstart 目录中。

创建 Maven 项目

首先,我们需要一个新项目。使用以下命令创建一个新项目

CLI
quarkus create app org.acme.spring.security:spring-security-quickstart \
    --extension='spring-web,spring-security,quarkus-elytron-security-properties-file,rest-jackson'
cd spring-security-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme.spring.security \
    -DprojectArtifactId=spring-security-quickstart \
    -Dextensions='spring-web,spring-security,quarkus-elytron-security-properties-file,rest-jackson'
cd spring-security-quickstart

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=spring-security-quickstart"

此命令生成一个项目,该项目导入 spring-webspring-securitysecurity-properties-file 扩展。

如果已经配置了 Quarkus 项目,可以通过在项目根目录中运行以下命令,将 spring-webspring-securitysecurity-properties-file 扩展添加到项目中

CLI
quarkus extension add spring-web,spring-security,quarkus-elytron-security-properties-file,rest-jackson
Maven
./mvnw quarkus:add-extension -Dextensions='spring-web,spring-security,quarkus-elytron-security-properties-file,rest-jackson'
Gradle
./gradlew addExtension --extensions='spring-web,spring-security,quarkus-elytron-security-properties-file,rest-jackson'

这会将以下内容添加到您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-spring-web</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-spring-security</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-spring-web")
implementation("io.quarkus:quarkus-spring-security")
implementation("io.quarkus:quarkus-elytron-security-properties-file")
implementation("io.quarkus:quarkus-rest-jackson")

有关 security-properties-file 的更多信息,您可以查看 quarkus-elytron-security-properties-file 扩展的指南。

GreetingController

Quarkus Maven 插件自动生成了一个带有 Spring Web 注解的控制器,用于定义我们的 REST 端点(而不是默认使用的 Jakarta REST 端点)。首先创建一个 src/main/java/org/acme/spring/security/GreetingController.java,一个带有 Spring Web 注解的控制器,用于定义我们的 REST 端点,如下所示

package org.acme.spring.security;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/greeting")
public class GreetingController {

    @GetMapping
    public String hello() {
        return "Hello Spring";
    }
}

GreetingControllerTest

请注意,也为控制器创建了一个测试

package org.acme.spring.security;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
class GreetingControllerTest {
    @Test
    void testHelloEndpoint() {
        given()
          .when().get("/greeting")
          .then()
             .statusCode(200)
             .body(is("Hello Spring"));
    }

}

打包并运行应用程序

使用以下命令运行应用程序

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

在浏览器中打开 https://:8080/greeting

结果应该是: {"message": "hello"}

修改控制器以保护 hello 方法

为了限制对具有特定角色的用户的 hello 方法的访问,将使用 @Secured 注解。更新后的控制器将是

package org.acme.spring.security;

import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/greeting")
public class GreetingController {

    @Secured("admin")
    @GetMapping
    public String hello() {
        return "hello";
    }
}

为我们的示例设置用户和角色最简单的方法是使用 security-properties-file 扩展。此扩展本质上允许在主 Quarkus 配置文件 - application.properties 中定义用户和角色。有关此扩展的更多信息,请查看相关指南。一个示例配置如下

quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.scott=jb0ss
quarkus.security.users.embedded.roles.scott=admin,user
quarkus.security.users.embedded.users.stuart=test
quarkus.security.users.embedded.roles.stuart=user

请注意,还需要更新测试。它可以像这样

GreetingControllerTest

package org.acme.spring.security;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class GreetingControllerTest {

    @Test
    public void testHelloEndpointForbidden() {
        given().auth().preemptive().basic("stuart", "test")
                .when().get("/greeting")
                .then()
                .statusCode(403);
    }

    @Test
    public void testHelloEndpoint() {
        given().auth().preemptive().basic("scott", "jb0ss")
                .when().get("/greeting")
                .then()
                .statusCode(200)
                .body(is("hello"));
    }

}

测试更改

自动

在开发模式下,按 r,或使用以下命令运行应用程序

Maven
./mvnw test
Gradle
./gradlew test

所有测试都应该成功。

手动

允许访问

再次在浏览器中打开 https://:8080/greeting 并在显示的对话框中输入 scottjb0ss

应该显示单词 hello

禁止访问

再次在浏览器中打开 https://:8080/greeting 并让显示的对话框为空。

结果应该是

Access to localhost was denied
You don't have authorization to view this page.
HTTP ERROR 403

一些浏览器会保存基本身份验证的凭据。如果未显示对话框,请尝试清除保存的登录名或使用隐私模式

支持的 Spring Security 注解

Quarkus 目前仅支持 Spring Security 提供的一部分功能,并且计划提供更多功能。更具体地说,Quarkus 支持基于角色的授权语义的安全相关功能(想想 @Secured 而不是 @RolesAllowed)。

注解

下表总结了支持的注解

表 1. 支持的 Spring Security 注解
名称 注释 Spring 文档

@Secured

参见上文

使用 @Secured 授权方法调用

@PreAuthorize

有关更多详细信息,请参见下一节

使用 @PreAuthorize 授权方法调用

@PreAuthorize

Quarkus 提供对 Spring Security 的 @PreAuthorize 注解的一些最常用功能的支持。支持的表达式如下

hasRole

要测试当前用户是否具有特定角色,可以在 @PreAuthorize 中使用 hasRole 表达式。

一些示例为:@PreAuthorize("hasRole('admin')")@PreAuthorize("hasRole(@roles.USER)"),其中 roles 是一个可以像这样定义的 bean

import org.springframework.stereotype.Component;

@Component
public class Roles {

    public final String ADMIN = "admin";
    public final String USER = "user";
}
hasAnyRole

hasRole 类似,用户可以使用 hasAnyRole 来检查登录用户是否具有任何指定的角色。

一些示例为:@PreAuthorize("hasAnyRole('admin')")@PreAuthorize("hasAnyRole(@roles.USER, 'view')")

permitAll

@PreAuthorize("permitAll()") 添加到方法将确保任何用户(包括匿名用户)都可以访问该方法。将其添加到类将确保该类的所有未用任何其他 Spring Security 注解注释的公共方法都可以访问。

denyAll

@PreAuthorize("denyAll()") 添加到方法将确保任何用户都无法访问该方法。将其添加到类将确保该类的所有未用任何其他 Spring Security 注解注释的公共方法对任何用户都不可访问。

isAnonymous

当用 @PreAuthorize("isAnonymous()") 注释 bean 方法时,只有当前用户是匿名用户(即未登录用户)时,该方法才可访问。

isAuthenticated

当用 @PreAuthorize("isAuthenticated()") 注释 bean 方法时,只有当前用户是登录用户时,该方法才可访问。本质上,该方法仅对匿名用户不可用。

#paramName == authentication.principal.username

此语法允许用户检查受保护方法的参数(或参数字段)是否等于登录用户名。

此用例的示例如下

public class Person {

    private final String name;

    public Person(String name) {
        this.name = name;
    }

    // this syntax requires getters for field access
    public String getName() {
        return name;
    }
}

@Component
public class MyComponent {

    @PreAuthorize("#username == authentication.principal.username") (1)
    public void doSomething(String username, String other){

    }

    @PreAuthorize("#person.name == authentication.principal.username") (2)
    public void doSomethingElse(Person person){

    }
}
1 如果当前登录用户与 username 方法参数相同,则可以执行 doSomething
2 如果当前登录用户与 person 方法参数的 name 字段相同,则可以执行 doSomethingElse
使用 authentication. 是可选的,因此使用 principal.username 具有相同的结果。
#paramName != authentication.principal.username

这与前面的表达式类似,不同之处在于方法参数必须与登录用户名不同。

@beanName.method()

此语法允许开发人员指定特定 bean 的方法执行将确定当前用户是否可以访问受保护的方法。

最好用一个例子来解释该语法。假设已经创建了一个 MyComponent bean,如下所示

@Component
public class MyComponent {

    @PreAuthorize("@personChecker.check(#person, authentication.principal.username)")
    public void doSomething(Person person){

    }
}

doSomething 方法已使用一个表达式用 @PreAuthorize 注释,该表达式指示需要调用名为 personChecker 的 bean 的方法 check,以确定当前用户是否有权调用 doSomething 方法。

PersonChecker 的一个示例可以是

@Component
public class PersonChecker {

    public boolean check(Person person, String username) {
        return person.getName().equals(username);
    }
}

请注意,对于 check 方法,参数类型必须与 @PreAuthorize 中指定的类型匹配,并且返回类型必须为 boolean

组合表达式

@PreAuthorize 注解允许使用逻辑 AND / OR 组合表达式。目前,存在一个限制,即只能使用单个逻辑运算(这意味着不允许混合使用 ANDOR)。

一些允许的表达式示例

    @PreAuthorize("hasAnyRole('user', 'admin') AND #user == principal.username")
    public void allowedForUser(String user) {

    }

    @PreAuthorize("hasRole('user') OR hasRole('admin')")
    public void allowedForUserOrAdmin() {

    }

    @PreAuthorize("hasAnyRole('view1', 'view2') OR isAnonymous() OR hasRole('test')")
    public void allowedForAdminOrAnonymous() {

    }

目前,表达式不支持逻辑运算符的括号,并且从左到右进行评估

重要的技术说明

请注意,Quarkus 中的 Spring 支持不会启动 Spring 应用程序上下文,也不会运行任何 Spring 基础结构类。Spring 类和注解仅用于读取元数据和/或用作用户代码方法返回类型或参数类型。这对最终用户来说意味着添加任意 Spring 库不会有任何影响。此外,Spring 基础结构类(例如 org.springframework.beans.factory.config.BeanPostProcessor)将不会执行。

转换表

下表显示了如何将 Spring Security 注解转换为 Jakarta REST 注解。

Spring Jakarta REST 注释

@Secured("admin")

@RolesAllowed("admin")

@PreAuthorize

没有直接替换

Quarkus 以不同的方式处理复杂的授权,有关详细信息,请参见本指南

相关内容