Using JWT RBAC

本指南解释了您的 Quarkus 应用程序如何利用 SmallRye JWT验证 JSON Web Tokens,将它们表示为 MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken,并使用 Bearer 令牌授权和 Role-Based Access Control为 Quarkus HTTP 端点提供安全访问。

Quarkus OpenID Connect quarkus-oidc`扩展也支持 Bearer 令牌授权,并使用 `smallrye-jwt`将 Bearer 令牌表示为 `JsonWebToken。有关更多信息,请阅读 OIDC Bearer token authentication指南。如果 Quarkus 应用程序需要使用 OIDC 授权码流程对用户进行身份验证,则必须使用 OpenID Connect 扩展。有关更多信息,请参阅 OIDC code flow mechanism for protecting web applications

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Quickstart

Solution

我们建议你按照后续章节中的说明,逐步创建应用程序。但是,你可以直接跳到已完成的示例。

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

解决方案内置于 security-jwt-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}"

此命令会生成 Maven 项目并导入包含了 MicroProfile JWT RBAC 支持的 smallrye-jwt 扩展。

如果您已经配置了 Quarkus 项目,则可以通过在项目基本目录中运行以下命令,将 smallrye-jwt 扩展添加到您的项目中:

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-smallrye-jwt</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-smallrye-jwt-build")

Examine the Jakarta REST resource

使用以下内容在 src/main/java/org/acme/security/jwt/TokenSecuredResource.java 中创建一个 REST 端点:

REST Endpoint V1
package org.acme.security.jwt;

import java.security.Principal;

import jakarta.annotation.security.PermitAll;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
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;

import org.eclipse.microprofile.jwt.JsonWebToken;

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

    @Inject
    JsonWebToken jwt; (1)

    @GET()
    @Path("permit-all")
    @PermitAll (2)
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        return getResponseString(ctx); (3)
    }

    private String getResponseString(SecurityContext ctx) {
        String name;
        if (ctx.getUserPrincipal() == null) { (4)
            name = "anonymous";
        } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { (5)
            throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
        } else {
            name = ctx.getUserPrincipal().getName(); (6)
        }
        return String.format("hello + %s,"
            + " isHttps: %s,"
            + " authScheme: %s,"
            + " hasJWT: %s",
            name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); (7)
    }

    private boolean hasJwt() {
        return jwt.getClaimNames() != null;
    }
}
1 在此处,我们注入 JsonWebToken 接口,它是 java.security.Principalinterface 的扩展,可提供访问当前身份验证令牌关联请求的权限。
2 @PermitAll 是一个 Jakarta 通用安全性注释,它指示给定的端点可被任何调用者(经过身份验证或未经身份验证)访问。
3 在此处,我们注入 Jakarta REST SecurityContext 以检查调用的安全状态并使用 getResponseString() 函数填充响应字符串。
4 在此处,我们通过根据空值检查请求用户/调用者 Principal 来检查调用是否不安全。
5 此处,我们检查 Principal 和 JsonWebToken 是否具有相同名称,因为 JsonWebToken 代表当前 Principal。
6 在此处,我们获取 Principal 名称。
7 我们构建的回复利用了调用者名称、请求 SecurityContextisSecure()getAuthenticationScheme() 状态,以及是否注入了非空 JsonWebToken

Run the application

现在,我们可以运行我们的应用程序了。使用:

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

您应该会看到类似以下内容的输出:

quarkus:dev Output
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< org.acme:security-jwt-quickstart >-----------------------
[INFO] Building security-jwt-quickstart 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
...
Listening for transport dt_socket at address: 5005
2020-07-15 16:09:50,883 INFO  [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080
2020-07-15 16:09:50,885 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-07-15 16:09:50,885 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, rest, rest-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]

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

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

我们在请求中未提供任何 JWT,因此我们不会期望端点看到任何安全状态,而响应与此一致:

  • username is anonymous

  • 由于未使用 https,因此 isHttps 为 false

  • authScheme is null

  • hasJWT is false

使用 Ctrl-C 停止 Quarkus 服务器。

因此,现在我们实际上需要保护一些东西。在以下内容中查看新的端点方法 helloRolesAllowed

REST Endpoint V2
package org.acme.security.jwt;

import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
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;

import org.eclipse.microprofile.jwt.JsonWebToken;

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

    @Inject
    JsonWebToken jwt; (1)

    @GET
    @Path("permit-all")
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        return getResponseString(ctx);
    }

    @GET
    @Path("roles-allowed") (2)
    @RolesAllowed({ "User", "Admin" }) (3)
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowed(@Context SecurityContext ctx) {
        return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); (4)
    }

    private String getResponseString(SecurityContext ctx) {
        String name;
        if (ctx.getUserPrincipal() == null) {
            name = "anonymous";
        } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
            throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
        } else {
            name = ctx.getUserPrincipal().getName();
        }
        return String.format("hello + %s,"
            + " isHttps: %s,"
            + " authScheme: %s,"
            + " hasJWT: %s",
            name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
    }

    private boolean hasJwt() {
        return jwt.getClaimNames() != null;
    }
}
1 Here we inject JsonWebToken
2 此新端点将位于 /secured/roles-allowed
3 @RolesAllowed 是一个 Jakarta 通用安全性注释,它指示给定的端点可被具有分配的“用户”或“管理员”角色的调用者访问。
4 此处,我们采用与 hello 方法相同的方式构建回复,但还通过直接调用注入的 JsonWebToken 来添加 JWT birthdate 声明的值。

在您的 TokenSecuredResource 中添加此项补充后,重新运行 ./mvnw compile quarkus:dev 命令,然后尝试 curl -v [role="bare"]http://127.0.0.1:8080/secured/roles-allowed; echo,以便尝试访问新的终结点。

您的输出应如下所示:

curl command for /secured/roles-allowed
$ 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

出色,我们在请求中尚未提供任何 JWT,因此我们不应能够访问该终结点,并且我们也没有访问。相反,我们收到一个 HTTP 401 未授权错误。我们需要获取一个有效的 JWT 并将其传入才能访问该终结点。此过程分为两步,1) 使用有关如何验证 JWT 的信息配置我们的 SmallRye JWT 扩展,以及 2) 生成具有适当声明的匹配 JWT。

Configuring the SmallRye JWT Extension Security Information

使用以下内容创建 security-jwt-quickstart/src/main/resources/application.properties

application.properties for TokenSecuredResource
mp.jwt.verify.publickey.location=publicKey.pem (1)
mp.jwt.verify.issuer=https://example.com/issuer (2)

quarkus.native.resources.includes=publicKey.pem (3)
1 我们设置公钥位置以指向类路径 publicKey.pem 位置。我们将在第 B 部分 Adding a Public Key 中添加此密钥。
2 我们设置颁发者到 URL 字符串 https://example.com/issuer
3 我们正在将公钥包含在本地可执行文件中的资源中。

Adding a Public Key

JWT specification 定义了可以使用的 JWT 的不同安全等级。“MicroProfile JWT RBAC” 规范要求使用 RSA-256 签名算法来签署 JWT。这反过来要求使用一个 RSA 公钥对。在 REST 终结点服务器端,您需要配置要用来验证随请求一起发送的 JWT 的 RSA 公钥的位置。之前配置的 mp.jwt.verify.publickey.location=publicKey.pem 设置期望公钥作为 publicKey.pem 在类路径上可用。为实现此目的,将以下内容复制到 security-jwt-quickstart/src/main/resources/publicKey.pem 文件中。

RSA Public Key PEM Content
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----

Generating a JWT

在很多情况下,人们会从某个身份管理器(如 Keycloak)获取 JWT,但对于此快速入门教程,我们将使用 smallrye-jwt 提供的 JWT 生成 API 生成我们自己的 JWT。详情请参阅 Generate JWT tokens with SmallRye JWT

从以下清单中获取代码并将其放入 security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java

GenerateToken main Driver Class
package org.acme.security.jwt;

import java.util.Arrays;
import java.util.HashSet;

import org.eclipse.microprofile.jwt.Claims;

import io.smallrye.jwt.build.Jwt;

public class GenerateToken {
    /**
     * Generate JWT token
     */
    public static void main(String[] args) {
        String token =
           Jwt.issuer("https://example.com/issuer") (1)
             .upn("jdoe@quarkus.io") (2)
             .groups(new HashSet<>(Arrays.asList("User", "Admin"))) (3)
             .claim(Claims.birthdate.name(), "2001-07-13") (4)
           .sign();
        System.out.println(token);
    }
}
1 iss 声明是 JWT 的颁发者。这需要与服务器端的 mp.jwt.verify.issuer 匹配,以便令牌才可被接受为有效。
2 upn 声明由“MicroProfile JWT RBAC”规范定义为首选声明,用于通过容器安全性 API 看到的 Principal
3 group 声明提供了与 JWT 持有者相关的组和顶级角色。
4 birthday 声明。它可被认为是一种敏感声明,因此您可能需要考虑加密该声明,请参阅 Generate JWT tokens with SmallRye JWT

请注意,为了让此代码正常工作,我们需要与 TokenSecuredResource 应用程序中所包含的公钥对应的 RSA 私钥的内容。获取以下 PEM 内容并将其放入 security-jwt-quickstart/src/test/resources/privateKey.pem

RSA Private Key PEM Content
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
f3cg+fr8aou7pr9SHhJlZCU=
-----END PRIVATE KEY-----

我们将使用 smallrye.jwt.sign.key.location 属性指向此私有签名密钥。

Generating Keys with OpenSSL

还可以使用 OpenSSL 命令行工具生成一个公钥和私钥对。

openssl commands for generating keys
openssl genrsa -out rsaPrivateKey.pem 2048
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem

生成私钥需要一个附加步骤,用于将它转换成 PKCS#8 格式。

openssl command for converting private key
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem

您可以使用生成的密钥对来替代本快速入门教程中使用的密钥。

现在,我们可以生成一个 JWT 来配合 TokenSecuredResource 端点使用。为此,请运行以下命令:

Sample JWT Generation Output
$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem

eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjU5Njc2LCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1OTk3NiwiaWF0IjoxNTUxNjU5Njc2LCJqdGkiOiJhLTEyMyJ9.O9tx_wNNS4qdpFhxeD1e7v4aBNWz1FCq0UV8qmXd7dW9xM4hA5TO-ZREk3ApMrL7_rnX8z81qGPIo_R8IfHDyNaI1SLD56gVX-NaOLS2OjfcbO3zOWJPKR_BoZkYACtMoqlWgIwIRC-wJKUJU025dHZiNL0FWO4PjwuCz8hpZYXIuRscfFhXKrDX1fh3jDhTsOEFfu67ACd85f3BdX9pe-ayKSVLh_RSbTbBPeyoYPE59FW7H5-i8IE-Gqu838Hz0i38ksEJFI25eR-AJ6_PSUD0_-TV3NjXhF3bFIeT4VSaIZcpibekoJg0cQm-4ApPEcPLdgTejYHA-mupb8hSwg

JWT 字符串是通过 base64 URL 编码的字符串,有 3 个部分,用 '.' 字符分隔。第一部分 - JWT 头部,第二部分 - JWT 声明,第三部分 - JWT 签名。

Finally, Secured Access to /secured/roles-allowed

现在,让我们使用它向 /secured/roles-allowed 端点发出安全请求。确保 Quarkus 服务器仍在 dev 模式下运行,然后运行以下命令,务必使用你从上一步中生成的 JWT 的版本:

curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed; echo
curl Command for /secured/roles-allowed With JWT
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13

成功!我们现在拥有:

  • 一个非匿名调用者名称,即 jdoe@quarkus.io

  • Bearer 身份验证方案

  • a non-null JsonWebToken

  • birthdate claim value

Using the JsonWebToken and Claim Injection

既然我们可以生成一个 JWT 来访问安全的 REST 端点,那么让我们看看我们可以使用 JsonWebToken 接口和 JWT 声明做什么。org.eclipse.microprofile.jwt.JsonWebToken 接口扩展了 java.security.Principal 接口,实际上是 jakarta.ws.rs.core.SecurityContext#getUserPrincipal() 调用返回的对象类型,我们之前使用了该调用。这意味着未使用 CDI 但能够访问 REST 容器 SecurityContext 的代码可以通过强制转换 SecurityContext#getUserPrincipal() 来获得调用者 JsonWebToken 接口。

JsonWebToken 接口定义了用于访问基础 JWT 的方法。它可以访问 MicroProfile JWT RBAC 规范所需的常见声明,以及 JWT 中可能存在的任意声明。

所有 JWT 声明都可以注入。让我们使用另一个使用已注入的 birthdate 声明的端点 /secured/roles-allowed-admin 扩展我们的 TokenSecuredResource (而不是从 JsonWebToken 获取该声明):

package org.acme.security.jwt;

import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
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;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

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

    @Inject
    JsonWebToken jwt; (1)
    @Inject
    @Claim(standard = Claims.birthdate)
    String birthdate; (2)

    @GET
    @Path("permit-all")
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        return getResponseString(ctx);
    }

    @GET
    @Path("roles-allowed")
    @RolesAllowed({ "User", "Admin" })
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowed(@Context SecurityContext ctx) {
        return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString();
    }

    @GET
    @Path("roles-allowed-admin")
    @RolesAllowed("Admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowedAdmin(@Context SecurityContext ctx) {
        return getResponseString(ctx) + ", birthdate: " + birthdate; (3)
    }

    private String getResponseString(SecurityContext ctx) {
        String name;
        if (ctx.getUserPrincipal() == null) {
            name = "anonymous";
        } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
            throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
        } else {
            name = ctx.getUserPrincipal().getName();
        }
        return String.format("hello + %s,"
            + " isHttps: %s,"
            + " authScheme: %s,"
            + " hasJWT: %s",
            name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
    }

    private boolean hasJwt() {
        return jwt.getClaimNames() != null;
    }
}
1 我们在此注入 JsonWebToken。
2 在此处,我们注入 birthday 声明为 String - 这就是现在需要 @RequestScoped 范围的原因。
3 在此处,我们使用已注入的 birthday 声明构建最终答复。

现在,再次生成令牌并运行:

curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed-admin; echo
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13

Package and run the application

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

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

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

Runner jar Example
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO  [io.quarkus] (main) Quarkus {quarkus-version} 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, smallrye-jwt]

你还可以按如下方式生成本机可执行文件:

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

$ ./target/security-jwt-quickstart-runner
2019-03-28 14:31:37,315 INFO  [io.quarkus] (main) Quarkus 0.12.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, smallrye-jwt]

Explore the Solution

位于 security-jwt-quickstart directory 的解决方案存储库包含我们已在本快速入门指南中用过的所有版本,以及一些说明注入 JsonWebToken`s and their claims into those using the CDI APIs. We suggest that you check out the quickstart solutions and explore the `security-jwt-quickstart 目录的子资源的附加端点,以了解有关 SmallRye JWT 扩展功能的更多信息。

Reference Guide

Supported Injection Scopes

当注入 org.eclipse.microprofile.jwt.JsonWebToken 时,所有 @ApplicationScoped@Singleton@RequestScoped 外部 bean 注入范围都受支持,且对 JsonWebToken 强制执行 @RequestScoped 范围,以确保当前令牌得到体现。

但是,当将单个令牌声明注入为简单类型(例如 String)时,必须使用 @RequestScoped,例如:

package org.acme.security.jwt;

import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;

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

    @Inject
    @Claim(standard = Claims.birthdate)
    String birthdate;
}

请注意,你还可以使用已注入的 JsonWebToken 访问单个声明,在这种情况下,设置 @RequestScoped 不是必需的。

Supported Public Key Formats

公钥可以采用以下任何格式,按优先级顺序指定:

  • 公钥加密标准 #8 (PKCS#8) PEM

  • JSON Web Key (JWK)

  • JSON Web 密钥集 (JWKS)

  • JSON Web 密钥 (JWK) Base64 URL 编码

  • JSON Web 密钥集 (JWKS) Base64 URL 编码

Dealing with the verification keys

如果您需要使用非对称 RSA 或椭圆曲线 (EC) 密钥验证令牌签名,则使用 mp.jwt.verify.publickey.location 属性来引用本地或远程验证密钥。

使用 mp.jwt.verify.publickey.algorithm 来自定义验证算法(默认值为 RS256),例如,在使用 EC 密钥时将其设置为 ES256

如果您需要使用对称密钥验证令牌签名,则必须使用 JSON Web Key (JWK) 或 JSON Web Key Set (JWK 集) 格式来表示此密钥,例如:

{
 "keys": [
   {
     "kty":"oct",
     "kid":"secretKey",
     "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
   }
 ]
}

此密钥 JWK 也需要使用 smallrye.jwt.verify.key.location 引用。应该将 smallrye.jwt.verify.algorithm 设置为 HS256/HS384/HS512

Parse and Verify JsonWebToken with JWTParser

如果 JWT 令牌无法注入,例如,如果它嵌入在服务请求负载中或服务端点通过带外获得它,则可以使用 JWTParser

import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
...
@Inject JWTParser parser;

String token = getTokenFromOidcServer();

// Parse and verify the token
JsonWebToken jwt = parser.parse(token);

您还可以使用它来自定义令牌的验证或解密方式。例如,可以提供本地 SecretKey

package org.acme.security.jwt;

import io.smallrye.jwt.auth.principal.ParseException;
import jakarta.inject.Inject;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
import io.smallrye.jwt.build.Jwt;

@Path("/secured")
public class SecuredResource {
    private static final String SECRET = "AyM1SysPpbyDfgZld3umj1qzKObwVMko";

    @Inject
    JWTParser parser;

    @GET
    @Produces("text/plain")
    public Response getUserName(@CookieParam("jwt") String jwtCookie) throws ParseException {
        if (jwtCookie == null) {
            // Create a JWT token signed using the 'HS256' algorithm
            String newJwtCookie = Jwt.upn("Alice").signWithSecret(SECRET);
            // or create a JWT token encrypted using the 'A256KW' algorithm
            // Jwt.upn("alice").encryptWithSecret(secret);
            return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build();
        } else {
            // All mp.jwt and smallrye.jwt properties are still effective, only the verification key is customized.
            JsonWebToken jwt = parser.verify(jwtCookie, SECRET);
            // or jwt = parser.decrypt(jwtCookie, secret);
            return Response.ok(jwt.getName()).build();
        }
    }
}

另请参阅 How to Add SmallRye JWT directly 部分,了解如何在没有 quarkus-smallrye-jwt 提供的 HTTP 支持的情况下使用 JWTParser

Token Decryption

如果您的应用程序需要使用加密声明或加密内签名声明来接受令牌,您需要做的就是设置指向解密密钥的 smallrye.jwt.decrypt.key.location

如果这是设置的唯一密钥属性,则传入令牌预期仅包含加密声明。如果同时也设置了 mp.jwt.verify.publickeymp.jwt.verify.publickey.location 验证属性,则传入令牌预期包含加密内签名令牌。

参见 Generate JWT tokens with SmallRye JWT 并了解如何快速生成加密或内签名且然后加密令牌。

Custom Factories

默认情况下使用 io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory 来解析和验证 JWT 令牌,并将它们转换为 JsonWebToken 主体。它使用 Configuration 中列出的 MP JWTsmallrye-jwt 属性来验证和自定义 JWT 令牌。

如果您需要提供自己的工厂,例如,为了避免再次验证已经由防火墙验证过的令牌,则可以通过提供 META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory 资源使用 ServiceLoader 机制,或者只需有一个 Alternative CDI Bean 实现,如下所示:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory;
import io.smallrye.jwt.auth.principal.ParseException;

@ApplicationScoped
@Alternative
@Priority(1)
public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory {

    @Override
    public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException {
        try {
            // Token has already been verified, parse the token claims only
            String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8);
            return new DefaultJWTCallerPrincipal(JwtClaims.parse(json));
        } catch (InvalidJwtException ex) {
            throw new ParseException(ex.getMessage());
        }
    }
}

Blocking calls

quarkus-smallrye-jwt 扩展使用 SmallRye JWT 库,而该库当前不是响应式的。

从作为响应式 Quarkus 安全架构一部分的 quarkus-smallrye-jwt 的角度来看,这意味着进入 SmallRye JWT 验证或解密代码的 IO 线程可能会在以下情况下之一中阻塞:

  • 默认密钥解析器刷新包含密钥的 JsonWebKey 集,其中涉及对 OIDC 端点的远程调用

  • 自定义密钥解析器,例如 AWS Application Load BalancerALB)密钥解析器,使用当前令牌的密钥标识符头值来相对于 AWS ALB 密钥端点解析密钥

在这种情况下,如果连接缓慢,例如,从密钥端点获取响应可能需要 3 秒以上,则当前事件循环线程很可能会阻塞。

要防止这种情况,请设置 quarkus.smallrye-jwt.blocking-authentication=true

Token Propagation

请参阅 Token Propagation 关于将 Bearer 访问令牌传播到下游服务的部分。

Testing

Wiremock

如果将 mp.jwt.verify.publickey.location 配置为指向基于 HTTP 的 JsonWebKey (JWK),则可以采用与 OpenID Connect Bearer Token Integration testing Wiremock 部分中描述相同的方法,但仅将 application.properties 更改为使用 MP JWT 配置属性:

# keycloak.url is set by OidcWiremockTestResource
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus

Keycloak

如果使用 Keycloak,并将 mp.jwt.verify.publickey.location 配置为指向基于 HTTPS 或 HTTP 的 JsonWebKey (JWK),则可以采用与 OpenID Connect Bearer Token Integration testing Keycloak 部分中描述相同的方法,但仅将 application.properties 更改为使用 MP JWT 配置属性:

# keycloak.url is set by DevServices for Keycloak
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus

请注意,Keycloak 发行的令牌的 iss(发布者)声明被设置为领域端点地址。

如果 Quarkus 应用程序在 Docker 容器中运行,它可能会与 DevServices 通过 Keycloak 为 Keycloak 启动的 Keycloak Docker 容器共享网络接口,其中 Quarkus 应用程序和 Keycloak 会通过内部共享 Docker 网络互相通信。

在这种情况下,请使用以下配置:

# keycloak.url is set by DevServices for Keycloak,
# Quarkus will access it via an internal shared docker network interface.
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs

# Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property
mp.jwt.verify.issuer=${client.quarkus.oidc.auth-server-url}

Local Public Key

可以采用与 OpenID Connect Bearer Token Integration testing Local Public Key 部分中描述相同的方法,但仅将 application.properties 更改为使用 MP JWT 配置属性:

mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
# set it to the issuer value which is used to generate the tokens
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus

# required to sign the tokens
smallrye.jwt.sign.key.location=privateKey.pem

TestSecurity annotation

添加以下依赖项:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-security-jwt</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-security-jwt")

并编写以下测试代码:

import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.jwt.Claim;
import io.quarkus.test.security.jwt.JwtSecurity;
import io.restassured.RestAssured;

@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {

    @Test
    @TestSecurity(user = "userJwt", roles = "viewer")
    public void testJwt() {
        RestAssured.when().get("test-security-jwt").then()
                .body(is("userJwt:viewer"));
    }

    @Test
    @TestSecurity(user = "userJwt", roles = "viewer")
    @JwtSecurity(claims = {
            @Claim(key = "email", value = "user@gmail.com")
    })
    public void testJwtWithClaims() {
        RestAssured.when().get("test-security-jwt-claims").then()
                .body(is("userJwt:viewer:user@gmail.com"));
    }

}

其中 ProtectedResource 类可能如下所示:

@Path("/web-app")
@Authenticated
public class ProtectedResource {

    @Inject
    JsonWebToken accessToken;

    @GET
    @Path("test-security-jwt")
    public String testSecurityOidc() {
        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
    }

    @GET
    @Path("test-security-jwt-claims")
    public String testSecurityOidcUserInfoMetadata() {
        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
                + ":" + accessToken.getClaim("email");
    }
}

请注意,始终必须使用 @TestSecurity 注释,而其 user 属性会返回为 JsonWebToken.getName(),而 roles 属性返回为 JsonWebToken.getGroups().@JwtSecurity 注释为可选项,可用于设置其他令牌声明。

@TestSecurity@JwtSecurity 可以根据以下情况组合到元注释中:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD })
    @TestSecurity(user = "userOidc", roles = "viewer")
    @OidcSecurity(introspectionRequired = true,
        introspection = {
            @TokenIntrospection(key = "email", value = "user@gmail.com")
        }
    )
    public @interface TestSecurityMetaAnnotation {

    }

如果需要在多个测试方法中使用同一组安全设置,这会尤其有用。

How to check the errors in the logs

请启用 io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator TRACE 级别日志记录,以查看有关令牌验证或解密错误的更多详细信息:

quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE

Proactive Authentication

当调用公共端点方法时,如果你想要跳过令牌验证,请禁用 proactive authentication

请注意,如果尚未执行令牌验证,则无法在公共方法中访问注入的 JsonWebToken

How to Add SmallRye JWT directly

parse and verify JsonWebToken with JWTParser,请在以下情况下直接对 quarkus-smallrye-jwt 使用 smallrye-jwt

  • 您使用 Quarkus 扩展而其中不支持 HTTP,例如 Quarkus GRPC

  • 您提供一个扩展特定的 HTTP,其支持与由 quarkus-smallrye-jwt`和 `Vert.x HTTP`提供的支持冲突,例如 `Quarkus AWS Lambda

首先添加 `smallrye-jwt`依赖:

pom.xml
<dependency>
    <groupId>io.smallrye</groupId>
    <artifactId>smallrye-jwt</artifactId>
</dependency>
build.gradle
implementation("io.smallrye:smallrye-jwt")

并更新 `application.properties`以将所有 `smallrye-jwt`提供的 CDI 生产者包含如下:

quarkus.index-dependency.smallrye-jwt.group-id=io.smallrye
quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt

Configuration Reference

Quarkus configuration

Unresolved include directive in modules/ROOT/pages/security-jwt.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-smallrye-jwt.adoc[]

MicroProfile JWT configuration

Property Name Default Description

mp.jwt.verify.publickey

none

`mp.jwt.verify.publickey`配置属性允许将公钥文本本身作为字符串提供。公钥将从按照 Supported Public Key Formats中定义的顺序提供的字符串解析。

mp.jwt.verify.publickey.location

none

配置属性允许指定公钥的外部或内部位置。该值可以是相对路径或 URL。如果该值指向基于 HTTPS 的 JWK 集,则它在原生模式下工作的前提是 quarkus.ssl.native`属性也必须设置为 `true,详情请参阅 Using SSL With Native Executables

mp.jwt.verify.publickey.algorithm

RS256

签名算法。将它设置为 `ES256`以支持椭圆曲线签名算法。

mp.jwt.decrypt.key.location

none

配置属性允许指定私钥解密密钥的外部或内部位置。

mp.jwt.verify.issuer

none

配置属性指定服务器将接受为有效的 JWT 的 iss(颁发者)声明的值。

mp.jwt.verify.audiences

none

包含令牌 `aud`声明可能包含的受众的逗号分隔列表。

mp.jwt.verify.clock.skew

60

令牌过期和年龄验证期间使用的秒数时钟偏差。如果当前时间在令牌过期时间后在此属性指定的秒数内,则接受过期的令牌。默认值为 60 秒。

mp.jwt.verify.token.age

none

令牌 iat(签发时间)之后不能经过的秒数。

mp.jwt.token.header

Authorization

如果您使用另一个标头(例如 Cookie)来传递令牌,则设置此属性。

mp.jwt.token.cookie

none

包含令牌的 cookie 的名称。此属性仅在将 `mp.jwt.token.header`设置为 `Cookie`时有效。

Additional SmallRye JWT configuration

SmallRye JWT 提供更多用于自定义令牌处理的属性:

Property Name Default Description

smallrye.jwt.verify.key.location

NONE

验证密钥的位置,它可以指向公钥和私钥。私钥只能采用 JWK 格式。请注意,如果设置了此属性,那么会忽略“mp.jwt.verify.publickey.location”。

smallrye.jwt.verify.algorithm

签名算法。此属性只应用于设置必需的对称算法,例如 HS256。不赞成用于设置诸如 `ES256`之类的非对称算法 - 相反,应使用“mp.jwt.verify.publickey.algorithm”。

smallrye.jwt.verify.key-format

ANY

将此属性设置为特定的密钥格式(例如 PEM_KEYPEM_CERTIFICATEJWK`或 `JWK_BASE64URL),以优化加载验证密钥的方式。

smallrye.jwt.verify.key-provider

DEFAULT

默认情况下,根据 MicroProfile JWT 规范,可以从本地文件系统读取 PEM、JWK 或 JWK 密钥组,或从 URI 中获取。将此属性设置为 `AWS_ALB`以支持 AWS 应用程序负载均衡器验证密钥解析。

smallrye.jwt.verify.relax-key-validation

false

放宽验证密钥的验证,将此属性设置为 true 将允许长度小于 2048 位的 RSA 公钥。

smallrye.jwt.verify.certificate-thumbprint

false

如果启用了此属性,则签名 令牌必须包含“x5t”或“x5t#S256”X509Certificate 缩略指纹头。在这种情况下,验证密钥只能采用 JWK 或 PEM 证书密钥格式。JWK 密钥必须具有“x5c”(Base64 编码的 X509Certificate) 属性集。

smallrye.jwt.token.header

Authorization

如果使用 Cookie 等其他头来传递标记,请设置此属性。此属性已弃用 - 使用 'mp.jwt.token.header'。

smallrye.jwt.key-cache-size

100

密钥缓存大小。使用此属性以及 smallrye.jwt.key-cache-time-to-live 来控制密钥缓存,当诸如 AWS_ALB 的密钥提供程序用 smallrye.jwt.verify.key-provider=AWS_ALB 配置为动态解析密钥时。

smallrye.jwt.key-cache-time-to-live

10

密钥缓存条目存活时间(分钟)。使用此属性以及 smallrye.jwt.key-cache-size 来控制密钥缓存,当诸如 AWS_ALB 的密钥提供程序用 smallrye.jwt.verify.key-provider=AWS_ALB 配置为动态解析密钥时。

smallrye.jwt.token.cookie

none

包含令牌的 cookie 名称。此属性仅在 smallrye.jwt.token.header 设置为 Cookie`时才有效。此属性已弃用 - 使用 `mp.jwt.token.cookie

smallrye.jwt.always-check-authorization

false

将此属性设置为 true,以便在 smallrye.jwt.token.header 设置为 Cookie 但不存在带有 smallrye.jwt.token.cookie 名称的 cookie 时仍检查 Authorization 标头。

smallrye.jwt.token.schemes

Bearer

包含备用单一或多个方案的逗号分隔列表,例如 DPoP

smallrye.jwt.token.kid

none

密钥标识符。如果设置了密钥标识符,则验证 JWK 密钥以及每个 JWT 令牌必须具有相匹配的 kid 标头。

smallrye.jwt.time-to-live

none

JWT 可签发的最长秒数。实际上,JWT 的过期日期与签发日期之间的差值不得超过此值。将此属性设置为非正值会放宽对令牌的有效“iat”(颁发时间) 声明的要求。

smallrye.jwt.require.named-principal

true

如果应用程序依赖于 java.security.Principal 返回名称,则令牌必须具有 upnpreferred_usernamesub 声明集。设置此属性会导致 SmallRye JWT 在应用程序代码无法可靠地处理非空 Principal 名称时引发异常。

smallrye.jwt.path.sub

none

包含主题名称的声明的路径。它从顶级 JSON 对象开始,可以包含多个片段,其中每个片段仅代表 JSON 对象名称,例如: realms/subject。如果令牌没有“sub”声明,但主题设置在其他声明中,则可以使用此属性。对具有命名空间限定声明使用双引号。

smallrye.jwt.claims.sub

none

当当前令牌没有可用的标准或自定义 sub 声明时,可以使用此属性设置默认子声明值。如果未设置 upnpreferred_usernamesub 声明,则可以有效地使用此属性自定义 java.security.Principal 名称。

smallrye.jwt.path.groups

none

包含组的声明的路径。它从顶级 JSON 对象开始,可以包含多个片段,其中每个片段仅代表 JSON 对象名称,例如: realm/groups。如果令牌没有“groups”声明,但组设置在其他声明中,则可以使用此属性。对具有命名空间限定声明使用双引号。

smallrye.jwt.groups-separator

space

用于拆分可能包含多个组值的字符串的分隔符。仅当 smallrye.jwt.path.groups 属性指向一个自定义声明且其值为字符串时,才会使用它。默认值是一个空格,因为一个标准的 OAuth2 scope 声明可能包含一个空格分隔序列。

smallrye.jwt.claims.groups

none

当当前令牌没有可用的标准或自定义组声明时,可以使用此属性设置默认组声明值。

smallrye.jwt.jwks.refresh-interval

60

JWK 缓存刷新间隔(分钟)。除非 mp.jwt.verify.publickey.location 指向基于 HTTP 或 HTTPS URL 的 JWK 集,并且 JWK HTTPS 端点未返回带有一个正 max-age 参数值的 HTTP Cache-Control 响应标头,否则将忽略此设置。

smallrye.jwt.jwks.forced-refresh-interval

30

强制 JWK 缓存刷新间隔(分钟),用于限制强制刷新尝试的频率,当令牌验证失败时可能会发生这种情况,因为缓存中没有 kid 属性与当前令牌的 kid 标头匹配的 JWK 密钥。除非 mp.jwt.verify.publickey.location 指向基于 HTTP 或 HTTPS URL 的 JWK 集,否则将忽略此设置。

smallrye.jwt.expiration.grace

0

过期宽限,单位:秒。默认情况下,如果当前时间不晚于 token 过期时间 1 分钟,则仍接受过期的 token。已弃用此属性。请改为使用 mp.jwt.verify.clock.skew

smallrye.jwt.verify.aud

none

token aud`声明可能包含的受众的逗号分隔列表。此属性已弃用 - 请使用 `mp.jwt.verify.audiences

smallrye.jwt.required.claims

none

token 必须包含的声明的逗号分隔列表。

smallrye.jwt.decrypt.key.location

none

Config 属性允许指定私有解密密钥的外部或内部位置。已弃用此属性。请改为使用 mp.jwt.decrypt.key.location

smallrye.jwt.decrypt.algorithm

RSA_OAEP

Decryption algorithm.

smallrye.jwt.decrypt.key

none

以字符串提供的解密密钥。

smallrye.jwt.token.decryption.kid

none

解密密钥标识符。如果设置它,则解密 JWK 密钥以及每枚 JWT 令牌都必须具有相匹配的 `kid`标头。

smallrye.jwt.client.tls.certificate.path

none

TLS 受信证书的路径,当需要通过 `HTTPS`获取密钥时可能需要配置。

smallrye.jwt.client.tls.trust-all

false

信任所有主机名。如果需要通过 HTTPS`获取密钥,且此属性设置为 `true,则默认情况下所有主机名都受信任。

smallrye.jwt.client.tls.hosts

none

受信任的主机名集。如果需要通过 HTTPS`获取密钥,且 `smallrye.jwt.client.tls.trust-all`设置为 `false,则此属性可用于配置受信任的主机名。

smallrye.jwt.http.proxy.host

none

HTTP proxy host.

smallrye.jwt.http.proxy.port

80

HTTP proxy port.

smallrye.jwt.keystore.type

JKS

如果 mp.jwt.verify.publickey.location`或 mp.jwt.decrypt.key.location 指向 KeyStore`文件,则可使用此属性自定义密钥库类型。如果没有设置,则系统将检查文件名以确定密钥库类型,然后默认为 `JKS

smallrye.jwt.keystore.provider

如果 `mp.jwt.verify.publickey.location`或 `mp.jwt.decrypt.key.location`指向 `KeyStore`文件,则可使用此属性自定义 `KeyStore`提供程序。

smallrye.jwt.keystore.password

密钥库密码。如果 mp.jwt.verify.publickey.location`或 `mp.jwt.decrypt.key.location,则必须设置此属性。

smallrye.jwt.keystore.verify.key.alias

必须设置此属性,以便标识公钥验证密钥,如果 `mp.jwt.verify.publickey.location`指向 `KeyStore`文件,则将从 `KeyStore`中提取此密钥。

smallrye.jwt.keystore.decrypt.key.alias

如果 `mp.jwt.decrypt.key.location`指向 `KeyStore`文件,则必须设置此属性,以便标识私有解密密钥。

smallrye.jwt.keystore.decrypt.key.password

如果 `mp.jwt.decrypt.key.location`指向 `KeyStore`文件时,`KeyStore`中私有解密密钥的密码与 `smallrye.jwt.keystore.password`不同,则可设置此属性。

smallrye.jwt.resolve-remote-keys-at-startup

false

将此属性设置为 true,可在应用程序启动时解析远程密钥。