Quarkus Extension for Spring Security API

虽然鼓励用户使用 Java 标准注释来实现安全授权,但是 Quarkus 以 `spring-security`扩展的形式为 Spring Security 提供兼容层。 此指南解释了 Quarkus 应用程序如何利用著名的 Spring Security 注释,使用角色定义对 REST 服务的授权。

Prerequisites

include::./_includes/prerequisites.adoc[]* 熟悉 Spring Web 扩展

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

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

Creating the Maven project

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

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

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

适用于 Windows 用户:

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

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

此命令生成一个项目,该项目导入 spring-web, `spring-security`和 `security-properties-file`扩展。

如果您已配置 Quarkus 项目,则可以通过在项目基础目录中运行以下命令来将 spring-web, `spring-security`和 `security-properties-file`扩展添加到项目中:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

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

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"));
    }

}

Package and run the application

使用以下内容运行应用程序:

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

用浏览器打开 [role="bare"][role="bare"]http://localhost:8080/greeting.

结果应为: {"message": "hello"}.

Modify the controller to secure the hello method

为了限制对具有某些角色的用户访问 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`中定义用户和角色。有关此扩展的更多信息,请检查 the associated guide。示例配置如下:

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"));
    }

}

Test the changes

Automatically

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

Maven
./mvnw test
Gradle
./gradlew test

所有测试都应该成功。

Manually

Access allowed

Open your browser again to [role="bare"]http://localhost:8080/greeting and introduce scott and jb0ss in the dialog displayed.

会显示单词 hello

Access forbidden

Open your browser again to [role="bare"]http://localhost:8080/greeting and let empty the dialog displayed.

结果应该是:

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

一些浏览器会保存基本身份验证的凭证。如果没有显示该对话框,请尝试清除已保存的登录信息或使用私人模式。

Supported Spring Security annotations

Quarkus 目前只支持 Spring Security 提供的一部分功能,而且正在规划更多功能。具体而言,Quarkus 支持以下安全相关功能:基于角色的授权语义(不妨考虑使用 @Secured,而不是 @RolesAllowed)。

Annotations

下表总结了受支持的注释:

Table 1. Supported Spring Security annotations
Name Comments Spring documentation

@Secured

See secure

Authorizing Method Invocation with @Secured

@PreAuthorize

有关更多详细信息,请参见下一部分。

Authorizing Method Invocation with @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

In the same fashion as hasRole, users can use hasAnyRole to check if the logged-in user has any of the specified roles.

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

permitAll

Adding @PreAuthorize("permitAll()") to a method will ensure that method is accessible by any user (including anonymous users). Adding it to a class will ensure that all public methods of the class that are not annotated with any other Spring Security annotation will be accessible.

denyAll

Adding @PreAuthorize("denyAll()") to a method will ensure that method is not accessible by any user. Adding it to a class will ensure that all public methods of the class that are not annotated with any other Spring Security annotation will not be accessible to any user.

isAnonymous

When annotating a bean method with @PreAuthorize("isAnonymous()") the method will only be accessible if the current user is anonymous - i.e. a non logged-in user.

isAuthenticated

When annotating a bean method with @PreAuthorize("isAuthenticated()") the method will only be accessible if the current user is a logged-in user. Essentially the method is only unavailable for anonymous users.

#paramName == authentication.principal.username

This syntax allows users to check if a parameter (or a field of the parameter) of the secured method is equal to the logged-in 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

This is similar to the previous expression with the difference being that the method parameter must be different from the logged-in username.

@beanName.method()

This syntax allows developers to specify that the execution of method of a specific bean will determine if the current user can access the secured method.

语法最好通过示例来解释。让我们假设 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

Combining expressions

@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() {

    }

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

Important Technical Note

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

Conversion Table

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

Spring Jakarta REST Comments

@Secured("admin")

@RolesAllowed("admin")

@PreAuthorize

No direct replacement

Quarkus 以不同的方式处理复杂授权,有关详细信息,请参阅 this guide