Using JWT RBAC

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

This guide explains how your Quarkus application can utilize SmallRye JWT to verify JSON Web Tokens, represent them as MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken and provide secured access to the Quarkus HTTP endpoints using Bearer Token Authorization and Role-Based Access Control.

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

Quarkus OpenID Connect quarkus-oidc extension also supports Bearer Token Authorization and uses smallrye-jwt to represent the bearer tokens as JsonWebToken. For more information, read the OIDC Bearer token authentication guide. OpenID Connect extension has to be used if the Quarkus application needs to authenticate the users using OIDC Authorization Code Flow. For more information, see OIDC code flow mechanism for protecting web applications

Prerequisites

Unresolved directive in security-jwt.adoc - include::{includes}/prerequisites.adoc[]

Quickstart

Solution

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

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can skip 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].

解决方案内置于 security-jwt-quickstart directory 中。

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

Creating the Maven project

首先,使用以下命令创建新项目:

First, create a new project with the following command:

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

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

This command generates the Maven project and imports the smallrye-jwt extension, which includes the MicroProfile JWT RBAC support.

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

If you already have your Quarkus project configured, you can add the smallrye-jwt extension to your project by running the following command in your project base directory:

Unresolved directive in security-jwt.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-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 端点:

Create a REST endpoint in src/main/java/org/acme/security/jwt/TokenSecuredResource.java with the following content:

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 Here we inject the JsonWebToken interface, an extension of the java.security.Principal interface that provides access to the claims associated with the current authenticated token.
2 @PermitAll is a Jakarta common security annotation that indicates that the given endpoint is accessible by any caller, authenticated or not.
3 Here we inject the Jakarta REST SecurityContext to inspect the security state of the call and use a getResponseString() function to populate a response string.
4 Here we check if the call is insecure by checking the request user/caller Principal against null.
5 Here we check that the Principal and JsonWebToken have the same name since JsonWebToken does represent the current Principal.
6 Here we get the Principal name.
7 The reply we build up makes use of the caller name, the isSecure() and getAuthenticationScheme() states of the request SecurityContext, and whether a non-null JsonWebToken was injected.

Run the application

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

Now we are ready to run our application. Use:

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

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

and you should see output similar to:

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 这样的命令行工具访问它:

Now that the REST endpoint is running, we can access it using a command line tool like 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,因此我们不会期望端点看到任何安全状态,而响应与此一致:

We have not provided any JWT in our request, so we would not expect that there is any security state seen by the endpoint, and the response is consistent with that:

  • username is anonymous

  • isHttps is false as https is not used

  • authScheme is null

  • hasJWT is false

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

Use Ctrl-C to stop the Quarkus server.

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

So now let’s actually secure something. Take a look at the new endpoint method helloRolesAllowed in the following:

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 This new endpoint will be located at /secured/roles-allowed
3 @RolesAllowed is a Jakarta common security annotation that indicates that the given endpoint is accessible by a caller if they have either a "User" or "Admin" role assigned.
4 Here we build the reply the same way as in the hello method but also add a value of the JWT birthdate claim by directly calling the injected JsonWebToken.

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

After you make this addition to your TokenSecuredResource, rerun the ./mvnw compile quarkus:dev command, and then try curl -v [role="bare"]http://127.0.0.1:8080/secured/roles-allowed; echo to attempt to access the new endpoint.

您的输出应如下所示:

Your output should be as follows:

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。

Excellent, we have not provided any JWT in the request, so we should not be able to access the endpoint, and we were not. Instead, we received an HTTP 401 Unauthorized error. We need to obtain and pass in a valid JWT to access that endpoint. There are two steps to this, 1) configuring our SmallRye JWT extension with information on how to validate a JWT, and 2) generating a matching JWT with the appropriate claims.

Configuring the SmallRye JWT Extension Security Information

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

Create a security-jwt-quickstart/src/main/resources/application.properties with the following content:

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 We are setting public key location to point to a classpath publicKey.pem location. We will add this key in part B, Adding a Public Key.
2 We are setting the issuer to the URL string https://example.com/issuer.
3 We are including the public key as a resource in the native executable.

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 文件中。

The JWT specification defines various levels of security of JWTs that one can use. The MicroProfile JWT RBAC specification requires that JWTs that are signed with the RSA-256 signature algorithm. This in turn requires an RSA public key pair. On the REST endpoint server side, you need to configure the location of the RSA public key to use to verify the JWT sent along with requests. The mp.jwt.verify.publickey.location=publicKey.pem setting configured previously expects that the public key is available on the classpath as publicKey.pem. To accomplish this, copy the following content to a security-jwt-quickstart/src/main/resources/publicKey.pem file.

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

Often one obtains a JWT from an identity manager like Keycloak, but for this quickstart we will generate our own using the JWT generation API provided by smallrye-jwt. For more information, see Generate JWT tokens with SmallRye JWT.

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

Take the code from the following listing and place into 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 The iss claim is the issuer of the JWT. This needs to match the server side mp.jwt.verify.issuer. in order for the token to be accepted as valid.
2 The upn claim is defined by the MicroProfile JWT RBAC spec as preferred claim to use for the Principal seen via the container security APIs.
3 The group claim provides the groups and top-level roles associated with the JWT bearer.
4 The birthday claim. It can be considered to be a sensitive claim, so you may want to consider encrypting the claims, see Generate JWT tokens with SmallRye JWT.

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

Note for this code to work we need the content of the RSA private key that corresponds to the public key we have in the TokenSecuredResource application. Take the following PEM content and place it into 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 属性指向此私有签名密钥。

We will use a smallrye.jwt.sign.key.location property to point to this private signing key.

Generating Keys with OpenSSL

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

It is also possible to generate a public and private key pair using the OpenSSL command line tool.

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

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

An additional step is needed for generating the private key for converting it into the PKCS#8 format.

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

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

You can use the generated pair of keys instead of the keys used in this quickstart.

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

Now we can generate a JWT to use with TokenSecuredResource endpoint. To do this, run the following command:

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 签名。

The JWT string is the Base64 URL encoded string that has 3 parts separated by '.' characters. First part - JWT headers, second part - JWT claims, third part - JWT signature.

Finally, Secured Access to /secured/roles-allowed

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

Now let’s use this to make a secured request to the /secured/roles-allowed endpoint. Make sure you have the Quarkus server still running in dev mode, and then run the following command, making sure to use your version of the generated JWT from the previous step:

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

成功!我们现在拥有:

Success! We now have:

  • a non-anonymous caller name of jdoe@quarkus.io

  • an authentication scheme of 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 接口。

Now that we can generate a JWT to access our secured REST endpoints, let’s see what more we can do with the JsonWebToken interface and the JWT claims. The org.eclipse.microprofile.jwt.JsonWebToken interface extends the java.security.Principal interface, and is in fact the type of the object that is returned by the jakarta.ws.rs.core.SecurityContext#getUserPrincipal() call we used previously. This means that code that does not use CDI but does have access to the REST container SecurityContext can get hold of the caller JsonWebToken interface by casting the SecurityContext#getUserPrincipal().

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

The JsonWebToken interface defines methods for accessing claims in the underlying JWT. It provides accessors for common claims that are required by the MicroProfile JWT RBAC specification as well as arbitrary claims that may exist in the JWT.

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

All the JWT claims can also be injected. Let’s expand our TokenSecuredResource with another endpoint /secured/roles-allowed-admin which uses the injected birthdate claim (as opposed to getting it from 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 Here we inject the JsonWebToken.
2 Here we inject the birthday claim as String - this is why the @RequestScoped scope is now required.
3 Here we use the injected birthday claim to build the final reply.

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

Now generate the token again and run:

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

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

As usual, the application can be packaged using:

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

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

And executed using 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]

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

You can also generate the native executable with:

Unresolved directive in security-jwt.adoc - include::{includes}/devtools/build-native.adoc[]

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 扩展功能的更多信息。

The solution repository located in the security-jwt-quickstart directory contains all the versions we have worked through in this quickstart guide as well as some additional endpoints that illustrate subresources with injection of 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 directory to learn more about the SmallRye JWT extension features.

Reference Guide

Supported Injection Scopes

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

@ApplicationScoped, @Singleton and @RequestScoped outer bean injection scopes are all supported when an org.eclipse.microprofile.jwt.JsonWebToken is injected, with the @RequestScoped scoping for JsonWebToken enforced to ensure the current token is represented.

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

However, @RequestScoped must be used when the individual token claims are injected as simple types such as String, for example:

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 不是必需的。

Note you can also use the injected JsonWebToken to access the individual claims in which case setting @RequestScoped is not necessary.

Please see MP JWT CDI Injection Requirements for more details.

Supported Public Key Formats

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

Public Keys may be formatted in any of the following formats, specified in order of precedence:

  • Public Key Cryptography Standards #8 (PKCS#8) PEM

  • JSON Web Key (JWK)

  • JSON Web Key Set (JWKS)

  • JSON Web Key (JWK) Base64 URL encoded

  • JSON Web Key Set (JWKS) Base64 URL encoded

Dealing with the verification keys

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

If you need to verify the token signature using the asymmetric RSA or Elliptic Curve (EC) key then use the mp.jwt.verify.publickey.location property to refer to the local or remote verification key.

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

Use mp.jwt.verify.publickey.algorithm to customize the verification algorithm (default is RS256), for example, set it to ES256 when working with the EC keys.

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

If you need to verify the token signature using the symmetric secret key then either a JSON Web Key (JWK) or JSON Web Key Set (JWK Set) format must be used to represent this secret key, for example:

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

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

This secret key JWK will also need to be referred to with smallrye.jwt.verify.key.location. smallrye.jwt.verify.algorithm should be set to HS256/HS384/HS512.

Parse and Verify JsonWebToken with JWTParser

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

If the JWT token can not be injected, for example, if it is embedded in the service request payload or the service endpoint acquires it out of band, then one can use 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

You can also use it to customize the way the token is verified or decrypted. For example, one can supply a local 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

Please also see the add-smallrye-jwt section about using JWTParser without the HTTP support provided by quarkus-smallrye-jwt.

Token Decryption

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

If your application needs to accept the tokens with the encrypted claims or the encrypted inner-signed claims, all you have to do is set smallrye.jwt.decrypt.key.location pointing to the decryption key.

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

If this is the only key property that is set, the incoming token is expected to contain the encrypted claims only. If either mp.jwt.verify.publickey or mp.jwt.verify.publickey.location verification properties are also set then the incoming token is expected to contain the encrypted inner-signed token.

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

See Generate JWT tokens with SmallRye JWT and learn how to generate the encrypted or inner-signed and then encrypted tokens fast.

Custom Factories

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

io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory is used by default to parse and verify JWT tokens and convert them to JsonWebToken principals. It uses MP JWT and smallrye-jwt properties listed in the Configuration section to verify and customize JWT tokens.

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

If you need to provide your own factory, for example, to avoid verifying the tokens again which have already been verified by the firewall, then you can either use a ServiceLoader mechanism by providing a META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory resource or simply have an Alternative CDI bean implementation like this one:

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-smallrye-jwt extension uses SmallRye JWT library which is currently not reactive.

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

What it means from the perspective of quarkus-smallrye-jwt which operates as part of the reactive Quarkus security architecture, is that an IO thread entering the SmallRye JWT verification or decryption code might block in one of the following cases:

  • Default key resolver refreshes JsonWebKey set containing the keys which involves a remote call to the OIDC endpoint

  • Custom key resolver such as AWS Application Load Balancer (ALB) key resolver, resolves the keys against the AWS ALB key endpoint using the current token’s key identifier header value

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

In such cases, if the connections are slow, for example, it may take more than 3 seconds to get a response from the key endpoint, the current event loop thread will most likely block.

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

To prevent it, set quarkus.smallrye-jwt.blocking-authentication=true.

Token Propagation

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

Please see the Token Propagation section about the Bearer access token propagation to the downstream services.

Testing

Wiremock

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

If you configure mp.jwt.verify.publickey.location to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the OpenID Connect Bearer Token Integration testing Wiremock section but only change the application.properties to use MP JWT configuration properties instead:

# 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 配置属性:

If you work with Keycloak and configure mp.jwt.verify.publickey.location to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the OpenID Connect Bearer Token Integration testing Keycloak section but only change the application.properties to use MP JWT configuration properties instead:

# 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(发布者)声明被设置为领域端点地址。

Note that the tokens issued by Keycloak have an iss (issuer) claim set to the realm endpoint address.

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

If your Quarkus application is running in a docker container, it may share a network interface with a Keycloak docker container launched by DevServices for Keycloak, with the Quarkus application and Keycloak communicating with each other via an internal shared docker network.

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

In such cases, use the following configuration instead:

# 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 配置属性:

You can use the same approach as described in the OpenID Connect Bearer Token Integration testing Local Public Key section but only change the application.properties to use MP JWT configuration properties instead:

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

添加以下依赖项:

Add the following dependency:

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

并编写以下测试代码:

and write a test code like this one:

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 类可能如下所示:

where ProtectedResource class may look like this:

@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 注释为可选项,可用于设置其他令牌声明。

Note that @TestSecurity annotation must always be used and its user property is returned as JsonWebToken.getName() and roles property - as JsonWebToken.getGroups(). @JwtSecurity annotation is optional and can be used to set the additional token claims.

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

@TestSecurity and @JwtSecurity can be combined in a meta-annotation, as follows:

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

    }

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

This is particularly useful if the same set of security settings needs to be used in multiple test methods.

How to check the errors in the logs

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

Please enable io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator TRACE level logging to see more details about the token verification or decryption errors:

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

If you’d like to skip the token verification when the public endpoint methods are invoked, disable the proactive authentication.

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

Note that you can’t access the injected JsonWebToken in the public methods if the token verification has not been done.

How to Add SmallRye JWT directly

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

To jwt-parser, use smallrye-jwt instead of quarkus-smallrye-jwt directly for the following situations:

  • You work with Quarkus extensions that do not support HTTP, such as Quarkus GRPC.

  • You provide an extension-specific HTTP, the support of which conflicts with the support of those offered by quarkus-smallrye-jwt and Vert.x HTTP, such as Quarkus AWS Lambda.

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

Start with adding the smallrye-jwt dependency:

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

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

and update application.properties to get all the CDI producers provided by smallrye-jwt included as follows:

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

Configuration Reference

Quarkus configuration

Unresolved directive in security-jwt.adoc - include::{generated-dir}/config/quarkus-smallrye-jwt.adoc[]

MicroProfile JWT configuration

Property Name Default Description

mp.jwt.verify.publickey

none

The mp.jwt.verify.publickey config property allows the Public Key text itself to be supplied as a string. The Public Key will be parsed from the supplied string in the order defined in the Supported Public Key Formats section.

mp.jwt.verify.publickey.location

none

Config property allows for an external or internal location of Public Key to be specified. The value may be a relative path or a URL. If the value points to an HTTPS based JWK set then, for it to work in native mode, the quarkus.ssl.native property must also be set to true, see Using SSL With Native Executables for more details.

mp.jwt.verify.publickey.algorithm

RS256

Signature algorithm. Set it to ES256 to support the Elliptic Curve signature algorithm.

mp.jwt.decrypt.key.location

none

Config property allows for an external or internal location of Private Decryption Key to be specified.

mp.jwt.verify.issuer

none

Config property specifies the value of the iss (issuer) claim of the JWT that the server will accept as valid.

mp.jwt.verify.audiences

none

Comma separated list of the audiences that a token aud claim may contain.

mp.jwt.verify.clock.skew

60

Clock skew in seconds used during the token expiration and age verification. An expired token is accepted if the current time is within the number of seconds specified by this property after the token expiration time. The default value is 60 seconds.

mp.jwt.verify.token.age

none

Number of seconds that must not elapse since the token iat (issued at) time.

mp.jwt.token.header

Authorization

Set this property if another header such as Cookie is used to pass the token.

mp.jwt.token.cookie

none

Name of the cookie containing a token. This property will be effective only if mp.jwt.token.header is set to Cookie.

Additional SmallRye JWT configuration

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

SmallRye JWT provides more properties which can be used to customize the token processing:

Property Name Default Description

smallrye.jwt.verify.key.location

NONE

Location of the verification key which can point to both public and secret keys. Secret keys can only be in the JWK format. Note that 'mp.jwt.verify.publickey.location' will be ignored if this property is set.

smallrye.jwt.verify.algorithm

Signature algorithm. This property should only be used for setting a required symmetric algorithm such as HS256. It is deprecated for setting asymmetric algorithms such as ES256 - use 'mp.jwt.verify.publickey.algorithm' instead.

smallrye.jwt.verify.key-format

ANY

Set this property to a specific key format such as PEM_KEY, PEM_CERTIFICATE, JWK or JWK_BASE64URL to optimize the way the verification key is loaded.

smallrye.jwt.verify.key-provider

DEFAULT

By default, PEM, JWK or JWK key sets can be read from the local file system or fetched from URIs as required by MicroProfile JWT specification. Set this property to AWS_ALB to support an AWS Application Load Balancer verification key resolution.

smallrye.jwt.verify.relax-key-validation

false

Relax the validation of the verification keys, setting this property to true will allow public RSA keys with the length less than 2048 bit.

smallrye.jwt.verify.certificate-thumbprint

false

If this property is enabled then a signed token must contain either 'x5t' or 'x5t#S256' X509Certificate thumbprint headers. Verification keys can only be in JWK or PEM Certificate key formats in this case. JWK keys must have a 'x5c' (Base64-encoded X509Certificate) property set.

smallrye.jwt.token.header

Authorization

Set this property if another header such as Cookie is used to pass the token. This property is deprecated - use 'mp.jwt.token.header'.

smallrye.jwt.key-cache-size

100

Key cache size. Use this property, as well as smallrye.jwt.key-cache-time-to-live, to control the key cache when a key provider such as AWS_ALB is configured with smallrye.jwt.verify.key-provider=AWS_ALB for resolving the keys dynamically.

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

10

Key cache entry time-to-live in minutes. Use this property, as well as smallrye.jwt.key-cache-size, to control the key cache when a key provider such as AWS_ALB is configured with smallrye.jwt.verify.key-provider=AWS_ALB for resolving the keys dynamically.

smallrye.jwt.token.cookie

none

Name of the cookie containing a token. This property will be effective only if smallrye.jwt.token.header is set to Cookie. This property is deprecated - use mp.jwt.token.cookie.

smallrye.jwt.always-check-authorization

false

Set this property to true for Authorization header be checked even if the smallrye.jwt.token.header is set to Cookie but no cookie with a smallrye.jwt.token.cookie name exists.

smallrye.jwt.token.schemes

Bearer

Comma-separated list containing an alternative single or multiple schemes, for example, DPoP.

smallrye.jwt.token.kid

none

Key identifier. If it is set then the verification JWK key as well every JWT token must have a matching kid header.

smallrye.jwt.time-to-live

none

The maximum number of seconds that a JWT may be issued for use. Effectively, the difference between the expiration date of the JWT and the issued at date must not exceed this value. Setting this property to a non-positive value relaxes the requirement for the token to have a valid 'iat' (issued at) claim.

smallrye.jwt.require.named-principal

true

If an application relies on java.security.Principal returning a name then a token must have a upn or preferred_username or sub claim set. Setting this property will result in SmallRye JWT throwing an exception if none of these claims is available for the application code to reliably deal with a non-null Principal name.

smallrye.jwt.path.sub

none

Path to the claim containing the subject name. It starts from the top level JSON object and can contain multiple segments where each segment represents a JSON object name only, example: realms/subject. This property can be used if a token has no 'sub' claim but has the subject set in a different claim. Use double quotes with the namespace qualified claims.

smallrye.jwt.claims.sub

none

This property can be used to set a default sub claim value when the current token has no standard or custom sub claim available. Effectively this property can be used to customize java.security.Principal name if no upn or preferred_username or sub claim is set.

smallrye.jwt.path.groups

none

Path to the claim containing the groups. It starts from the top level JSON object and can contain multiple segments where each segment represents a JSON object name only, example: realm/groups. This property can be used if a token has no 'groups' claim but has the groups set in a different claim. Use double quotes with the namespace qualified claims.

smallrye.jwt.groups-separator

space

Separator for splitting a string which may contain multiple group values. It will only be used if the smallrye.jwt.path.groups property points to a custom claim whose value is a string. The default value is a single space because a standard OAuth2 scope claim may contain a space separated sequence.

smallrye.jwt.claims.groups

none

This property can be used to set a default groups claim value when the current token has no standard or custom groups claim available.

smallrye.jwt.jwks.refresh-interval

60

JWK cache refresh interval in minutes. It will be ignored unless the mp.jwt.verify.publickey.location points to the HTTP or HTTPS URL based JWK set and no HTTP Cache-Control response header with a positive max-age parameter value is returned from a JWK HTTPS endpoint.

smallrye.jwt.jwks.forced-refresh-interval

30

Forced JWK cache refresh interval in minutes which is used to restrict the frequency of the forced refresh attempts which may happen when the token verification fails due to the cache having no JWK key with a kid property matching the current token’s kid header. It will be ignored unless the mp.jwt.verify.publickey.location points to the HTTP or HTTPS URL based JWK set.

smallrye.jwt.expiration.grace

0

Expiration grace in seconds. By default an expired token will still be accepted if the current time is no more than 1 min after the token expiry time. This property is deprecated. Use mp.jwt.verify.clock.skew instead.

smallrye.jwt.verify.aud

none

Comma separated list of the audiences that a token aud claim may contain. This property is deprecated - use mp.jwt.verify.audiences.

smallrye.jwt.required.claims

none

Comma separated list of the claims that a token must contain.

smallrye.jwt.decrypt.key.location

none

Config property allows for an external or internal location of Private Decryption Key to be specified. This property is deprecated - use mp.jwt.decrypt.key.location.

smallrye.jwt.decrypt.algorithm

RSA_OAEP

Decryption algorithm.

smallrye.jwt.decrypt.key

none

Decryption key supplied as a string.

smallrye.jwt.token.decryption.kid

none

Decryption Key identifier. If it is set then the decryption JWK key as well every JWT token must have a matching kid header.

smallrye.jwt.client.tls.certificate.path

none

Path to TLS trusted certificate which may need to be configured if the keys have to be fetched over HTTPS.

smallrye.jwt.client.tls.trust-all

false

Trust all the hostnames. If the keys have to be fetched over HTTPS and this property is set to true then all the hostnames are trusted by default.

smallrye.jwt.client.tls.hosts

none

Set of trusted hostnames. If the keys have to be fetched over HTTPS and smallrye.jwt.client.tls.trust-all is set to false then this property can be used to configure the trusted hostnames.

smallrye.jwt.http.proxy.host

none

HTTP proxy host.

smallrye.jwt.http.proxy.port

80

HTTP proxy port.

smallrye.jwt.keystore.type

JKS

This property can be used to customize a keystore type if either mp.jwt.verify.publickey.location or mp.jwt.decrypt.key.location` points to a KeyStore file. If it is not set then the file name will be checked to determine the keystore type before defaulting to JKS.

smallrye.jwt.keystore.provider

This property can be used to customize a KeyStore provider if mp.jwt.verify.publickey.location or mp.jwt.decrypt.key.location points to a KeyStore file.

smallrye.jwt.keystore.password

Keystore password. If mp.jwt.verify.publickey.location or mp.jwt.decrypt.key.location then this property has be set.

smallrye.jwt.keystore.verify.key.alias

This property has to be set to identify a public verification key which will be extracted from KeyStore from a matching certificate if mp.jwt.verify.publickey.location points to a KeyStore file.

smallrye.jwt.keystore.decrypt.key.alias

This property has to be set to identify a private decryption key if mp.jwt.decrypt.key.location points to a KeyStore file.

smallrye.jwt.keystore.decrypt.key.password

This property may be set if a private decryption key’s password in KeyStore is different to smallrye.jwt.keystore.password when mp.jwt.decrypt.key.location points to a KeyStore file.

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

false

Set this property to true to resolve the remote keys at the application startup.