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.
准备
要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
JDK 11+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.6
-
Optionally the Quarkus CLI if you want to use it
-
Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)
-
Some familiarity with the Spring Web extension
完整源码
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.
Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git
, or download
an archive.
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:
For Windows users:
-
If using cmd, (don’t use backward slash
\
and put everything on the same line) -
If using Powershell, wrap
-D
parameters in double quotes e.g."-DprojectArtifactId=spring-security-quickstart"
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'
This will add the following to your build file:
<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 Jakarta
REST 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"));
}
}
Package and run the application
Run the application with:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
Open your browser to 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
scott
andjb0ss
in the dialog displayed.The word
hello
should 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
Run the application as a native executable
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:
Name |
Comments |
@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
hasRole
expression can be used inside@PreAuthorize
.Some examples are:
@PreAuthorize("hasRole('admin')")
,@PreAuthorize("hasRole(@roles.USER)")
where theroles
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 usehasAnyRole
to 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 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; } 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 theusername
method parameter2 doSomethingElse
can be executed if the current logged-in user is the same as thename
field ofperson
method parameterthe use of authentication.
is optional, so usingprincipal.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.
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){ } }
The
doSomething
method has been annotated with@PreAuthorize
using an expression that indicates that methodcheck
of a bean namedpersonChecker
needs to be invoked to determine whether the current user is authorized to invoke thedoSomething
method.An example of the
PersonChecker
could be:@Component public class PersonChecker { @Override public boolean check(Person person, String username) { return person.getName().equals(username); } }
Note that for the
check
method the parameter types must match what is specified in@PreAuthorize
and 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.
Important Technical Note
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
The following table shows how Spring Security annotations can be converted to Jakarta REST annotations.
Spring |
Jakarta REST |
Comments |
@Secured("admin") |
@RolesAllowed("admin") |
More Spring guides
Quarkus has more Spring compatibility features. See the following guides for more details: