编辑此页面

使用 OAuth2 RBAC

本指南解释了您的 Quarkus 应用程序如何利用 OAuth2 令牌来提供对 Jakarta REST(以前称为 JAX-RS)端点的安全访问。

OAuth2 是一个授权框架,它使应用程序能够代表用户获取对 HTTP 资源的访问权限。它可以通过委托给外部服务器(身份验证服务器)用户身份验证并为身份验证上下文提供令牌来实现基于令牌的应用程序身份验证机制。

此扩展提供了使用不透明 Bearer Token 的轻量级支持,并通过调用自省端点来验证它们。

如果 OAuth2 身份验证服务器提供 JWT Bearer Token,请考虑使用 OIDC Bearer token 身份验证SmallRye JWT 扩展。如果 Quarkus 应用程序需要使用 OIDC 授权码流对用户进行身份验证,则必须使用 OpenID Connect 扩展。有关更多信息,请参阅 保护 Web 应用程序的 OIDC 代码流机制 指南。

解决方案

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

克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载存档。

该解决方案位于 security-oauth2-quickstart 目录中。它还包含一个非常简单的 UI 来使用此处创建的 Jakarta REST 资源。

创建 Maven 项目

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

CLI
quarkus create app org.acme:security-oauth2-quickstart \
    --extension='rest-jackson,security-oauth2' \
    --no-code
cd security-oauth2-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 \
    -DprojectArtifactId=security-oauth2-quickstart \
    -Dextensions='rest-jackson,security-oauth2' \
    -DnoCode
cd security-oauth2-quickstart

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

对于 Windows 用户

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

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

此命令生成一个项目并导入 elytron-security-oauth2 扩展,其中包括 OAuth2 不透明令牌支持。

如果您不想使用 Maven 插件,只需在构建文件中包含依赖项即可

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-elytron-security-oauth2</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-elytron-security-oauth2")

检查 Jakarta REST 资源

创建具有以下内容的 src/main/java/org/acme/security/oauth2/TokenSecuredResource.java 文件

package org.acme.security.oauth2;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/secured")
public class TokenSecuredResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}

这是一个基本的 REST 端点,没有任何 Elytron Security OAuth2 特定功能,所以让我们添加一些。

我们将使用 JSR 250 通用安全注解,它们在 使用安全 指南中进行了描述。

package org.acme.security.oauth2;

import java.security.Principal;

import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;

@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {


    @GET()
    @Path("permit-all")
    @PermitAll (1)
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) { (2)
        Principal caller =  ctx.getUserPrincipal(); (3)
        String name = caller == null ? "anonymous" : caller.getName();
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
        return helloReply; (4)
    }
}
1 @PermitAll 指示任何调用者(无论是否经过身份验证)都可以访问给定的端点。
2 在这里,我们注入 Jakarta REST SecurityContext 来检查调用的安全状态。
3 在这里,我们获取当前请求用户/调用者 Principal。对于不安全的调用,这将为 null,因此我们通过检查 caller 是否为 null 来构建用户名。
4 我们构建的回复使用了调用者名称、请求 SecurityContextisSecure()getAuthenticationScheme() 状态。

设置 application.properties

您需要使用以下最小属性配置您的应用程序

quarkus.oauth2.client-id=client_id
quarkus.oauth2.client-secret=secret
quarkus.oauth2.introspection-url=http://oauth-server/introspect

您需要指定身份验证服务器的自省 URL 以及您的应用程序将用于向身份验证服务器进行身份验证的 client-id / client-secret
然后,扩展将使用此信息来验证令牌并恢复与之关联的信息。

有关所有配置属性,请参阅本指南末尾的 配置参考 部分。

运行应用程序

现在我们准备好运行我们的应用程序了。使用

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

现在 REST 端点正在运行,我们可以使用像 curl 这样的命令行工具访问它

$ curl http://127.0.0.1:8080/secured/permit-all; echo
hello + anonymous, isSecure: false, authScheme: null

我们没有在请求中提供任何令牌,因此我们不希望端点看到任何安全状态,并且响应与此一致

  • 用户名是 anonymous

  • isSecure 为 false,因为未使用 https

  • authScheme 为 null

保护端点

现在让我们实际保护一些东西。查看以下新的端点方法 helloRolesAllowed

package org.acme.security.oauth2;

import java.security.Principal;

import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;

@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {

    @GET()
    @Path("permit-all")
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        Principal caller =  ctx.getUserPrincipal();
        String name = caller == null ? "anonymous" : caller.getName();
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
        return helloReply;
    }

    @GET()
    @Path("roles-allowed") (1)
    @RolesAllowed({"Echoer", "Subscriber"}) (2)
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowed(@Context SecurityContext ctx) {
        Principal caller =  ctx.getUserPrincipal();
        String name = caller == null ? "anonymous" : caller.getName();
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
        return helloReply;
    }
}
1 这个新端点将位于 /secured/roles-allowed
2 @RolesAllowed 指示如果调用者被分配了“Echoer”或“Subscriber”角色,则可以访问给定的端点。

在您将此添加到您的 TokenSecuredResource 后,尝试 curl -v http://127.0.0.1:8080/secured/roles-allowed; echo 以尝试访问新端点。您的输出应该是

$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /secured/roles-allowed HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Connection: keep-alive
< Content-Type: text/html;charset=UTF-8
< Content-Length: 14
< Date: Sun, 03 Mar 2019 16:32:34 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Not authorized

太棒了,我们没有在请求中提供任何 OAuth2 令牌,所以我们不应该能够访问该端点,而我们也没有。相反,我们收到了 HTTP 401 Unauthorized 错误。我们需要获取并传入有效的 OAuth2 令牌才能访问该端点。这需要两个步骤,1) 使用有关如何验证令牌的信息配置我们的 Elytron Security OAuth2 扩展,以及 2) 生成具有适当声明的匹配令牌。

生成令牌

您需要使用令牌端点从标准 OAuth2 身份验证服务器(例如 Keycloak)获取令牌。

您可以在下面找到 client_credential 流的此类调用的 curl 示例

curl -X POST "http://oauth-server/token?grant_type=client_credentials" \
-H  "Accept: application/json" -H  "Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ="

它应该像这样响应……

{"access_token":"60acf56d-9daf-49ba-b3be-7a423d9c7288","token_type":"bearer","expires_in":1799,"scope":"READER"}

最后,向 /secured/roles-allowed 发出安全请求

现在让我们使用它来向 /secured/roles-allowed 端点发出安全请求

$ curl -H "Authorization: Bearer 60acf56d-9daf-49ba-b3be-7a423d9c7288" http://127.0.0.1:8080/secured/roles-allowed; echo
hello + client_id isSecure: false, authScheme: OAuth2

成功!我们现在有

  • client_id 的非匿名调用者名称

  • OAuth2 的身份验证方案

角色映射

角色是从自省端点响应的声明之一映射的。默认情况下,它是 scope 声明。通过使用空格分隔符拆分声明来获取角色。如果声明是一个数组,则不进行拆分,角色从数组中获取。

您可以使用 quarkus.oauth2.role-claim 属性自定义用于角色的声明的名称。

打包并运行应用程序

与往常一样,可以使用以下命令打包应用程序

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

并使用 java -jar target/quarkus-app/quarkus-run.jar 执行

[INFO] Scanning for projects...
...
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO  [io.quarkus] (main) Quarkus 3.24.4 started in 0.796s. Listening on: http://[::]:8080
2019-03-28 14:27:48,841 INFO  [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]

您还可以使用以下命令生成原生可执行文件

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true
[INFO] Scanning for projects...
...
[security-oauth2-quickstart-runner:25602]     universe:     493.17 ms
[security-oauth2-quickstart-runner:25602]      (parse):     660.41 ms
[security-oauth2-quickstart-runner:25602]     (inline):   1,431.10 ms
[security-oauth2-quickstart-runner:25602]    (compile):   7,301.78 ms
[security-oauth2-quickstart-runner:25602]      compile:  10,542.16 ms
[security-oauth2-quickstart-runner:25602]        image:   2,797.62 ms
[security-oauth2-quickstart-runner:25602]        write:     988.24 ms
[security-oauth2-quickstart-runner:25602]      [total]:  43,778.16 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  51.500 s
[INFO] Finished at: 2019-06-28T14:30:56-07:00
[INFO] ------------------------------------------------------------------------

$ ./target/security-oauth2-quickstart-runner
2019-03-28 14:31:37,315 INFO  [io.quarkus] (main) Quarkus 0.20.0 started in 0.006s. Listening on: http://[::]:8080
2019-03-28 14:31:37,316 INFO  [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]

集成测试

如果您不想为集成测试使用真实的 OAuth2 授权服务器,则可以使用 基于属性的安全 扩展进行测试,或使用 Wiremock 模拟授权服务器。

首先,需要将 Wiremock 添加为测试依赖项。对于 Maven 项目,这将像这样发生

pom.xml
<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
    <version>${wiremock.version}</version> (1)
</dependency>
1 使用正确的 Wiremock 版本。所有可用版本都可以在 这里 找到。
build.gradle
testImplementation("org.wiremock:wiremock:${wiremock.version}") (1)
1 使用正确的 Wiremock 版本。所有可用版本都可以在 这里 找到。

在 Quarkus 测试中,当需要在运行 Quarkus 测试之前启动某些服务时,我们利用 @io.quarkus.test.common.QuarkusTestResource 注解来指定一个 io.quarkus.test.common.QuarkusTestResourceLifecycleManager,它可以启动服务并提供 Quarkus 将使用的配置值。

有关 @QuarkusTestResource 的更多详细信息,请参阅 文档的这一部分

让我们创建一个名为 MockAuthorizationServerTestResourceQuarkusTestResourceLifecycleManager 实现,如下所示

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

import java.util.Collections;
import java.util.Map;

public class MockAuthorizationServerTestResource implements QuarkusTestResourceLifecycleManager {  (1)

    private WireMockServer wireMockServer;

    @Override
    public Map<String, String> start() {
        wireMockServer = new WireMockServer();
        wireMockServer.start(); (2)

        // define the mock for the introspect endpoint
        WireMock.stubFor(WireMock.post("/introspect").willReturn(WireMock.aResponse() (3)
                .withBody(
                        "{\"active\":true,\"scope\":\"Echoer\",\"username\":null,\"iat\":1562315654,\"exp\":1562317454,\"expires_in\":1458,\"client_id\":\"my_client_id\"}")));


        return Collections.singletonMap("quarkus.oauth2.introspection-url", wireMockServer.baseUrl() + "/introspect"); (4)
    }

    @Override
    public void stop() {
        if (null != wireMockServer) {
            wireMockServer.stop();  (5)
        }
    }
}
1 start 方法在任何测试运行之前由 Quarkus 调用,并返回一个配置属性 Map,该配置属性在测试执行期间适用。
2 启动 Wiremock。
3 配置 Wiremock 以通过返回 OAuth2 自省响应来存根对 /introspect 的调用。您需要自定义此行以返回您的应用程序所需的内容(至少 scope 属性,因为角色是从 scope 派生的)。
4 由于 start 方法返回适用于测试的配置,因此我们设置 quarkus.oauth2.introspection-url 属性,该属性控制 OAuth2 扩展使用的自省端点的 URL。
5 当所有测试完成后,关闭 Wiremock。

您的测试类需要使用 @QuarkusTestResource(MockAuthorizationServerTestResource.class) 进行注解,以使用此 QuarkusTestResourceLifecycleManager

下面是一个使用 MockAuthorizationServerTestResource 的测试示例。

@QuarkusTest
@QuarkusTestResource(MockAuthorizationServerTestResource.class) (1)
class TokenSecuredResourceTest {
    // use whatever token you want as the mock OAuth server will accept all tokens
    private static final String BEARER_TOKEN = "337aab0f-b547-489b-9dbd-a54dc7bdf20d"; (2)

    @Test
    void testPermitAll() {
        RestAssured.given()
                .when()
                .header("Authorization", "Bearer " + BEARER_TOKEN) (3)
                .get("/secured/permit-all")
                .then()
                .statusCode(200)
                .body(containsString("hello"));
    }

    @Test
    void testRolesAllowed() {
        RestAssured.given()
                .when()
                .header("Authorization", "Bearer " + BEARER_TOKEN)
                .get("/secured/roles-allowed")
                .then()
                .statusCode(200)
                .body(containsString("hello"));
    }
}
1 将先前创建的 MockAuthorizationServerTestResource 用作 Quarkus 测试资源。
2 定义您想要的任何令牌,它不会被 OAuth2 模拟授权服务器验证。
3 Authorization 标头中使用此令牌以触发 OAuth2 身份验证。

@QuarkusTestResource 适用于所有测试,而不仅仅是 TokenSecuredResourceTest

配置参考

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

确定是否启用 OAuth2 扩展。如果您包含 elytron-security-oauth2 依赖项,则默认启用,因此这将用于禁用它。

环境变量:QUARKUS_OAUTH2_ENABLED

显示更多

布尔值

true

在自省端点响应中用于加载角色的声明。

环境变量:QUARKUS_OAUTH2_ROLE_CLAIM

显示更多

字符串

scope

用于验证令牌的 OAuth2 客户端 ID。如果启用了扩展,则为必需。

环境变量:QUARKUS_OAUTH2_CLIENT_ID

显示更多

字符串

用于验证令牌的 OAuth2 客户端密钥。如果启用了扩展,则为必需。

环境变量:QUARKUS_OAUTH2_CLIENT_SECRET

显示更多

字符串

用于验证令牌并收集身份验证声明的 OAuth2 自省端点 URL。如果启用了扩展,则为必需。

环境变量:QUARKUS_OAUTH2_INTROSPECTION_URL

显示更多

字符串

OAuth2 服务器证书文件。警告:本机模式不支持此功能,其中证书必须包含在本机映像生成期间使用的信任存储中,请参阅 将 SSL 与本机可执行文件一起使用

环境变量:QUARKUS_OAUTH2_CA_CERT_FILE

显示更多

字符串

令牌自省的客户端连接超时。如果未设置,则为无限。

环境变量:QUARKUS_OAUTH2_CONNECTION_TIMEOUT

显示更多

Duration 

令牌自省的客户端读取超时。如果未设置,则为无限。

环境变量:QUARKUS_OAUTH2_READ_TIMEOUT

显示更多

Duration 

关于 Duration 格式

要写入 duration 值,请使用标准 java.time.Duration 格式。有关更多信息,请参阅 Duration#parse() Java API 文档

您还可以使用简化的格式,以数字开头

  • 如果该值仅为一个数字,则表示以秒为单位的时间。

  • 如果该值是一个数字后跟 ms,则表示以毫秒为单位的时间。

在其他情况下,简化格式将被转换为 java.time.Duration 格式以进行解析

  • 如果该值是一个数字后跟 hms,则在其前面加上 PT

  • 如果该值是一个数字后跟 d,则在其前面加上 P

相关内容