Quarkus Extension for Spring Security API
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.
This guide explains how a Quarkus application can leverage the well known Spring Security annotations to define authorizations on RESTful services using roles.
先决条件
要完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
安装JDK 11以上版本并正确配置了
JAVA_HOME -
Apache Maven 3.8.6
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
-
对Spring Web扩展有一定的熟悉程度
解决方案
我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。
克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或下载一个 存档 。
The solution is located in the spring-security-quickstart directory.
创建Maven项目
首先,我们需要一个新的项目。使用以下命令创建一个新的项目:
This command generates a project which imports the spring-web, spring-security and security-properties-file extensions.
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:
quarkus extension add 'spring-web,spring-security,quarkus-elytron-security-properties-file,resteasy-reactive-jackson'
./mvnw quarkus:add-extension -Dextensions='spring-web,spring-security,quarkus-elytron-security-properties-file,resteasy-reactive-jackson'
./gradlew addExtension --extensions='spring-web,spring-security,quarkus-elytron-security-properties-file,resteasy-reactive-jackson'
这将在您的构建文件中添加以下内容:
<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-resteasy-reactive-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-spring-web")
implementation("io.quarkus:quarkus-spring-security")
implementation("io.quarkus:quarkus-elytron-security-properties-file")
implementation("io.quarkus:quarkus-resteasy-reactive-jackson")
For more information about security-properties-file, you can check out the guide of the quarkus-elytron-security-properties-file extension.
GreetingController
The Quarkus Maven plugin automatically generated a controller with the Spring Web annotations to define our REST endpoint (instead of the JAX-RS ones used by default).
First create a src/main/java/org/acme/spring/web/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";
}
}
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
public class GreetingControllerTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/greeting")
.then()
.statusCode(200)
.body(is("hello"));
}
}
打包并运行该应用程序
使用以下命令运行该应用程序:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
在你的浏览器中打开 http://localhost:8080/greeting 网页。
The result should be: {"message": "hello"}.
Modify the controller to secure the hello method
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";
}
}
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
- Access allowed
-
Open your browser again to http://localhost:8080/greeting and introduce
scottandjb0ssin the dialog displayed.The word
helloshould be displayed. - Access forbidden
-
Open your browser again to 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
以本地可执行文件的方式运行该应用程序
You can generate the native executable with:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
Supported Spring Security functionalities
Quarkus currently only supports a subset of the functionalities 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:
| 名称 | 备注 |
|---|---|
@Secured |
|
@PreAuthorize |
See next section for more details |
@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
-
To test if the current user has a specific role, the
hasRoleexpression can be used inside@PreAuthorize.Some examples are:
@PreAuthorize("hasRole('admin')"),@PreAuthorize("hasRole(@roles.USER)")where therolesis 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 usehasAnyRoleto check if the logged-in user has any of the specified roles.Some examples are:
@PreAuthorize("hasAnyRole('admin')"),@PreAuthorize("hasAnyRole(@roles.USER, 'view')") - permitAll
-
Adding
@PreAuthorize("permitAll()")to a method will ensure that 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 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; } 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 doSomethingcan be executed if the current logged-in user is the same as theusernamemethod parameter2 doSomethingElsecan be executed if the current logged-in user is the same as thenamefield ofpersonmethod parameterthe use of authentication.is optional, so usingprincipal.usernamehas 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.
The syntax is best explained with an example. Let’s assume that a
MyComponentbean has been created like so:@Component public class MyComponent { @PreAuthorize("@personChecker.check(#person, authentication.principal.username)") public void doSomething(Person person){ } }The
doSomethingmethod has been annotated with@PreAuthorizeusing an expression that indicates that methodcheckof a bean namedpersonCheckerneeds to be invoked to determine whether the current user is authorized to invoke thedoSomethingmethod.An example of the
PersonCheckercould be:@Component public class PersonChecker { @Override public boolean check(Person person, String username) { return person.getName().equals(username); } }Note that for the
checkmethod the parameter types must match what is specified in@PreAuthorizeand that the return type must be aboolean.
Combining expressions
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() {
}
Also to be noted that currently parentheses are not supported and expressions are evaluated from left to right when needed.
重要技术说明
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.