Quarkus Extension for Spring Security API

虽然鼓励用户使用 Java 标准注释来实现安全授权,但是 Quarkus 以 `spring-security`扩展的形式为 Spring Security 提供兼容层。

While users are encouraged to use Java standard annotations for security authorizations, Quarkus provides a compatibility layer for Spring Security in the form of the spring-security extension.

此指南解释了 Quarkus 应用程序如何利用著名的 Spring Security 注释,使用角色定义对 REST 服务的授权。

This guide explains how a Quarkus application can leverage the well-known Spring Security annotations to define authorizations on RESTful services using roles.

Prerequisites

include::{includes}/prerequisites.adoc[]* 熟悉 Spring Web 扩展

Unresolved directive in spring-security.adoc - include::{includes}/prerequisites.adoc[] * Some familiarity with the Spring Web extension

Solution

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

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

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

The solution is located in the spring-security-quickstart directory.

Creating the Maven project

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

First, we need a new project. Create a new project with the following command:

Unresolved directive in spring-security.adoc - include::{includes}/devtools/create-app.adoc[]

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

This command generates a project which imports the spring-web, spring-security and security-properties-file extensions.

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

If you already have your Quarkus project configured, you can add the spring-web, spring-security and security-properties-file extensions to your project by running the following command in your project base directory:

Unresolved directive in spring-security.adoc - include::{includes}/devtools/extension-add.adoc[]

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

This will add the following to your build file:

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扩展指南。

For more information about security-properties-file, you can check out the guide of the quarkus-elytron-security-properties-file extension.

GreetingController

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

The Quarkus Maven plugin automatically generated a controller with the Spring Web annotations to define our REST endpoint (instead of the Jakarta REST ones used by default). First create a src/main/java/org/acme/spring/security/GreetingController.java, a controller with the Spring Web annotations to define our REST endpoint, as follows:

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

请注意,还创建了该控制器的测试:

Note that a test for the controller has been created as well:

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

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

Run the application with:

Unresolved directive in spring-security.adoc - include::{includes}/devtools/dev.adoc[]

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

Open your browser to [role="bare"]http://localhost:8080/greeting.

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

The result should be: {"message": "hello"}.

Modify the controller to secure the hello method

为了限制对具有某些角色的用户访问 hello 方法,将使用 @Secured 注解。更新后的控制器如下:

In order to restrict access to the hello method to users with certain roles, the @Secured annotation will be utilized. The updated controller will be:

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。示例配置如下:

The easiest way to set up users and roles for our example is to use the security-properties-file extension. This extension essentially allows users and roles to be defined in the main Quarkus configuration file - application.properties. For more information about this extension check the associated guide. An example configuration would be the following:

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

请注意,测试也需要更新。它可能如下所示:

Note that the test also needs to be updated. It could look like:

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,或使用以下命令运行此应用程序:

Press r, while in dev mode, or run the application with:

Unresolved directive in spring-security.adoc - include::{includes}/devtools/test.adoc[]

所有测试都应该成功。

All tests should succeed.

Manually

Access allowed

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

会显示单词 hello

The word hello should be displayed.

Access forbidden

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

结果应该是:

The result should be:

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

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

Some browsers save credentials for basic authentication. If the dialog is not displayed, try to clear saved logins or use the Private mode

Supported Spring Security annotations

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

Quarkus currently only supports a subset of the functionality that Spring Security provides with more features being planned. More specifically, Quarkus supports the security related features of role-based authorization semantics (think of @Secured instead of @RolesAllowed).

Annotations

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

The table below summarizes the supported annotations:

Table 1. Supported Spring Security annotations
Name Comments Spring documentation

@Secured

See secure

Authorizing Method Invocation with @Secured

@PreAuthorize

See next section for more details

Authorizing Method Invocation with @PreAuthorize

@PreAuthorize

Quarkus 支持 Spring Security 的 @PreAuthorize 注解的一些最常用功能。支持的表达式如下所示:

Quarkus provides support for some of the most used features of Spring Security’s @PreAuthorize annotation. The expressions that are supported are the following:

hasRole

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

To test if the current user has a specific role, the hasRole expression can be used inside @PreAuthorize.

一些示例包括: @PreAuthorize("hasRole('admin')")@PreAuthorize("hasRole(@roles.USER)"),其中 roles 是一个可能已如下定义的 Bean:

Some examples are: @PreAuthorize("hasRole('admin')"), @PreAuthorize("hasRole(@roles.USER)") where the roles is a bean that could be defined like so:

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')")

Some examples are: @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.

此用例的一些示例包括:

Examples of this use case are:

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 doSomething can be executed if the current logged-in user is the same as the username method parameter
2 doSomethingElse can be executed if the current logged-in user is the same as the name field of person method parameter

使用 authentication. 是可选的,因此使用 principal.username 会得到相同的结果。

the use of authentication. is optional, so using principal.username has the same result.

#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 是这样创建的:

The syntax is best explained with an example. Let’s assume that a MyComponent bean has been created like so:

@Component
public class MyComponent {

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

    }
}

doSomething 方法已使用 @PreAuthorize 进行注释,该注释中包含一个表达式,指明需要调用名称为 personChecker 的 Bean 的方法 check 以确定当前用户是否有权调用 doSomething 方法。

The doSomething method has been annotated with @PreAuthorize using an expression that indicates that method check of a bean named personChecker needs to be invoked to determine whether the current user is authorized to invoke the doSomething method.

PersonChecker 的示例可能是:

An example of the PersonChecker could be:

@Component
public class PersonChecker {

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

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

Note that for the check method the parameter types must match what is specified in @PreAuthorize and that the return type must be a boolean.

Combining expressions

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

The @PreAuthorize annotations allows for the combination of expressions using logical AND / OR. Currently, there is a limitation where only a single logical operation can be used (meaning mixing AND and OR isn’t allowed).

一些允许的表达式的示例如下:

Some examples of allowed expressions are:

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

    }

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

Currently, expressions do not support parentheses for logical operators and are evaluated from left to right

Important Technical Note

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

Please note that the Spring support in Quarkus does not start a Spring Application Context nor are any Spring infrastructure classes run. Spring classes and annotations are only used for reading metadata and / or are used as user code method return types or parameter types. What that means for end users, is that adding arbitrary Spring libraries will not have any effect. Moreover, Spring infrastructure classes (like org.springframework.beans.factory.config.BeanPostProcessor for example) will not be executed.

Conversion Table

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

The following table shows how Spring Security annotations can be converted to Jakarta REST annotations.

Spring Jakarta REST Comments

@Secured("admin")

@RolesAllowed("admin")

@PreAuthorize

No direct replacement

Quarkus handles complex authorisation differently, see this guide for details