The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

使用 JWT RBAC

本指南旨在阐述如何在你的Quarkus应用程序中使用 SmallRye JWT来验证 JSON Web Token,并将它们呈现为MicroProfile JWT 的 org.eclipse.microprofile.jwt.JsonWebToken ,然后使用不记名令牌验证(Bearer Token Authorization)和 基于角色的访问控制来保证对Quarkus HTTP端点的安全访问。

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

先决条件

完成这个指南,你需要:

  • 大概15分钟

  • 编辑器

  • JDK 17+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.9.6

  • 如果你愿意的话,还可以选择使用Quarkus CLI

  • 如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)

快速入门

解决方案

我们建议您按照下面几节的说明,一步一步地创建应用程序。不过,您可以直接跳到已完成的例子。

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

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

创建Maven项目

First, create a new project with the following command:

CLI
quarkus create app org.acme:security-jwt-quickstart \
    --extension='resteasy-reactive-jackson,smallrye-jwt,smallrye-jwt-build' \
    --no-code
cd security-jwt-quickstart

创建Grade项目,请添加 --gradle 或者 --gradle-kotlin-dsl 参数。

For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.8.3:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-jwt-quickstart \
    -Dextensions='resteasy-reactive-jackson,smallrye-jwt,smallrye-jwt-build' \
    -DnoCode
cd security-jwt-quickstart

创建Grade项目,请添加 -DbuildTool=gradle 或者 -DbuildTool=gradle-kotlin-dsl 参数。

For Windows users:

  • If using cmd, (don’t use backward slash \ and put everything on the same line)

  • If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=security-jwt-quickstart"

该命令生成Maven项目并导入 smallrye-jwt 扩展,其中包括MicroProfile JWT RBAC支持。

如果你已经配置了你的Quarkus项目,你可以通过在你的项目基础目录下运行以下命令,将 smallrye-jwt 扩展到你的项目。

CLI
quarkus extension add smallrye-jwt,smallrye-jwt-build
Maven
./mvnw quarkus:add-extension -Dextensions='smallrye-jwt,smallrye-jwt-build'
Gradle
./gradlew addExtension --extensions='smallrye-jwt,smallrye-jwt-build'

这将在你的build文件中添加以下内容:

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.Principal接口的扩展,提供了对与当前认证令牌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 在这里,我们通过检查请求用户/呼叫者 Principal 是否是null值,如果是则不安全。
5 这里我们检查Principal和JsonWebToken是否有相同的名字,因为JsonWebToken代表了当前的Principal。
6 这里我们得到了Principal的名字。
7 当我们建立的回复时使用调用者的名字,和 isSecure()getAuthenticationScheme() 这两个 SecurityContext 的状态,以及是否注入了非空的 JsonWebToken

运行应用程序

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

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

而你应该看到类似的输出:

quarkus:dev 输出
[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, resteasy-reactive, resteasy-reactive-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]

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

用curl命令访问/secured/permit-all
$ curl http://127.0.0.1:8080/secured/permit-all; echo
hello + anonymous, isHttps: false, authScheme: null, hasJWT: false

我们在请求中没有提供任何JWT,所以我们不会期望有任何安全状态被终端看到,而响应也与此一致:

  • 用户名称是匿名的

  • isHttps为false,因为没有使用https

  • authScheme为null

  • hasJWT为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 在这里,我们注入 JsonWebToken
2 这个新的endpoint将位于/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 在这里,我们以与 hello 方法相同的方式建立回复,但也通过直接调用注入的 JsonWebToken ,添加JWT birthdate 要求的值。

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

Your output should be as follows:

用curl 命令访问/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 Unauthorized错误。我们需要获得并传入一个有效的JWT来访问该端点。这有两个步骤,1)用如何验证JWT的信息配置我们的SmallRye JWT扩展;2)用适当的声明(claim)生成一个匹配的JWT。

配置SmallRye JWT扩展安全信息

创建一个 security-jwt-quickstart/src/main/resources/application.properties ,内容如下:

TokenSecuredResource的application.properties
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, 添加公钥.
2 我们将发行者issuer设置为URL字符串 https://example.com/issuer .
3 我们将公钥作为一种资源纳入本地可执行文件。

添加公钥

这个 JWT规范 定义了人们可以使用的JWTs的各种安全级别。MicroProfile JWT RBAC规范要求JWTs用RSA-256签名算法进行签名。这反过来又需要一个RSA公钥对。在REST终端服务器端,你需要配置RSA公钥的位置,用来验证与请求一起发送的JWT。之前配置的 mp.jwt.verify.publickey.location=publicKey.pem ,意思是公钥要能在classpath上作为 publicKey.pem 文件能找到。为了达到这个目的,请将以下内容复制到 security-jwt-quickstart/src/main/resources/publicKey.pem 文件中。

RSA公钥PEM格式内容
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----

生成一个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

GenerateToken主驱动类
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 声明(claim)是JWT的发行者。这需要与服务器端的 mp.jwt.verify.issuer 符合。一遍使令牌被接受为有效的。
2 这个 upn 声明(claim)被MicroProfile JWT RBAC规范定义为首选声明,用做为被container security APIs所能见的 Principal
3 这个 group 声明(claim)提供了与JWT bearer相关的group和最高级别的role。
4 这个 birthday 声明(claim),它可以被认为是一个敏感的声明,所以你可能要考虑对这些声明进行加密, 见用SmallRye JWT生成JWT令牌

注意为了使这段代码工作,我们需要RSA私钥的内容,它与我们在TokenSecuredResource应用程序中的公钥相对应。将以下PEM内容放入 security-jwt-quickstart/src/test/resources/privateKey.pem

RSA私钥PEM格式的内容
-----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 属性来指向这个私人签名钥匙。

用OpenSSL生成密钥

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

用于生成密钥的openssl命令
openssl genrsa -out rsaPrivateKey.pem 2048
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem

生成私钥后需要一个额外的步骤,以便将其转换为PKCS#8格式。

openssl转换私钥的命令
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem

你可以使用生成的这对密钥,而不是本快速入门中使用的密钥。

现在我们可以生成一个JWT,然后与 TokenSecuredResource 端点一起使用。要做到这一点,运行以下命令:

JWT生成输出样本
$ 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签名。

最后,安全访问/secured/roles-allowed

现在让我们用它来向/secured/roles-allowed端点发出一个安全请求。确保你的Quarkus服务器仍然运行在开发模式下,然后运行以下命令,确保使用你在上一步中生成的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命令带上所需的JWT来访问/secured/roles-allowed
$ 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)的认证方案

  • 一个非空的JsonWebToken

  • birthdate声明(claim)的值

使用JsonWebToken和声明(claim)注入

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中的声明(claims)的方法。它为MicroProfile JWT RBAC规范所要求的共同声明(common claims)以及JWT中可能存在的任何的声明提供访问器。

所有的JWT声明(claims)也可以被注入。让我们用另一个端点/secured/roles-allowed-admin来扩展我们的 TokenSecuredResource ,它使用注入的 birthdate (而不是从 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 的声明(claim)作为 String 注入 - 这就是为什么现在需要 @RequestScoped 范围。
3 在这里,我们使用注入的 birthday 声明(claim)来建立最终的回复。

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

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

打包并运行应用程序

像往常一样,该应用程序可以用以下方式打包:

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

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

Runner jar的例子
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO  [io.quarkus] (main) Quarkus 3.8.3 started in 0.796s. Listening on: http://[::]:8080
2019-03-28 14:27:48,841 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, resteasy-reactive-jackson, security, smallrye-jwt]

你也可以用以下方法生成本地可执行文件:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native
Native Executable的例子
[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, resteasy-reactive, resteasy-reactive-jackson, security, 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 JsonWebTokens 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.

参考指南

支持的注射范围

这里 @ApplicationScoped , @Singleton@RequestScoped 外层Bean注入作用域在注入 org.eclipse.microprofile.jwt.JsonWebToken 时都是被支持的, @RequestScoped 作用域对 JsonWebToken 强制执行,以确保当前令牌被呈现。

然而,当单个令牌要求被注入为简单的类型,例如 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 来访问各个声明(claims),在这种情况下,设置 @RequestScoped 是没有必要的。

请参阅 MP JWT CDI注入要求 以了解更多细节。

支持的公钥格式

公钥可以采用以下任何一种格式,按优先顺序排列:

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

  • JSON网络密钥(JWK)

  • JSON网络密钥集(JWKS)

  • JSON Web Key (JWK) Base64 URL 编码

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

处理验证密钥

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

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

如果你需要使用对称密匙验证令牌签名,那么必须使用 JSON Web Key (JWK)或 JSON Web Key Set (JWK Set)格式来呈现这个密匙,例如:

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

这个密匙JWK也需要用 smallrye.jwt.verify.key.locationsmallrye.jwt.verify.algorithm 应该设置为 HS256 / HS384 / HS512

用JWTParser解析和验证JsonWebToken

如果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();
        }
    }
}

Please also see the How to Add SmallRye JWT directly section about using JWTParser without the HTTP support provided by quarkus-smallrye-jwt.

令牌解密

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.

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.

请参阅 使用SmallRye JWT生成JWT令牌 ,了解如何快速生成加密的或内部签名然后加密的令牌。

定制工厂

io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory 默认用于解析和验证JWT令牌,并将其转换为 JsonWebToken。它使用 MP JWT smallrye-jwt 这些在 Configuration 中列出的属性来验证和定制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 extension uses SmallRye JWT library which is currently not reactive.

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

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.

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

令牌传播

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

测试

Wiremock

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

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

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

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}

本地公钥

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 注解

添加以下依赖关系:

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 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.

如何检查日志中的错误

请启用 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

主动认证

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

请注意,如果没有进行令牌验证,你就不能在公共方法中使用注入的 JsonWebToken

如何直接添加SmallRye JWT

To parse and verify JsonWebToken with JWTParser, 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.

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生产者,包括如下:

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

配置参考

Quarkus配置

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

类型

默认

The MP-JWT configuration object

Environment variable: QUARKUS_SMALLRYE_JWT_ENABLED

Show more

boolean

true

The name of the java.security.Provider that supports SHA256withRSA signatures

Environment variable: QUARKUS_SMALLRYE_JWT_RSA_SIG_PROVIDER

Show more

string

SunRsaSign

Enable this property if fetching the remote keys can be a time-consuming operation. Do not enable it if you use the local keys.

Environment variable: QUARKUS_SMALLRYE_JWT_BLOCKING_AUTHENTICATION

Show more

boolean

false

Always create HTTP 401 challenge, even for requests containing no authentication credentials. JWT authentication mechanism will return HTTP 401 when an authentication challenge is required. However if it is used alongside one of the interactive authentication mechanisms then returning HTTP 401 to the users accessing the application from a browser may not be desired. If you prefer you can request that JWT authentication mechanism does not create a challenge in such cases by setting this property to 'true'.

Environment variable: QUARKUS_SMALLRYE_JWT_SILENT

Show more

boolean

false

MicroProfile JWT配置

Property Name 默认 描述

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 支持的公钥格式 section.

mp.jwt.verify.publickey.location

none

配置属性允许指定公钥的外部或内部位置。该值可以是一个相对路径或一个URL。如果该值指向一个基于HTTPS的JWK集,那么为了让它在本地模式下工作, quarkus.ssl.native 属性也必须被设置为 true ,更多细节请参见 使用SSL与本地可执行程序

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

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

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

mp.jwt.token.cookie

none

包含令牌的cookie的名称。只有当 mp.jwt.token.header 被设置为 Cookie ,该属性才会有效。

额外的SmallRye JWT配置

SmallRye JWT提供了更多的属性,可以用来定制令牌的处理:

Property Name 默认 描述

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_KEY , PEM_CERTIFICATE , JWKJWK_BASE64URL ,以优化验证密钥的加载方式。

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

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

smallrye.jwt.verify.certificate-thumbprint

false

如果此属性被启用,那么签名的令牌必须包含 x5tx5t#S256 X509Certificate的thumbprint headers。在这种情况下,验证密钥只能是JWK或PEM证书密钥格式。JWK密钥必须有一个 x5c(Base64编码的X509Certificate)属性设置。

smallrye.jwt.token.header

Authorization

如果使用另一个header,如 Cookie ,来传递令牌,则设置此属性。此属性已被废弃—请使用 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

如果将此属性设置为 trueAuthorization header也会被检查,即使 smallrye.jwt.token.header 被设置为 Cookie ,但又不存在名称为 smallrye.jwt.token.cookie 的cookie。

smallrye.jwt.token.schemes

Bearer

逗号分隔的列表,其中包含一个可替代的单一或多个schemes,例如, DPoP

smallrye.jwt.token.kid

none

密钥标识符。如果它被设置,那么验证JWK密钥以及每个JWT标记必须有一个匹配的 kid header。

smallrye.jwt.time-to-live

none

JWT可能被发布使用的最大秒数。实际上,JWT的到期日和签发日期之间的差异不得超过这个值。将此属性设置为非正值,可以取消对令牌具有有效的 iat(签发日期)声明的要求。

smallrye.jwt.require.named-principal

true

如果一个应用程序依赖于 java.security.Principal ,返回一个名称,那么一个标记必须有一个 upnpreferred_usernamesub 的声明集。如果应用程序代码没有这些要求来可靠地处理非空的 Principal 名称,设置此属性将导致SmallRye JWT抛出一个异常。

smallrye.jwt.path.sub

none

包含subject名称的声明的路径。它从顶级的JSON对象开始,可以包含多个段,每个段只代表一个JSON对象名称,例如: realms/subject 。如果一个标记没有 sub 声明,但在一个不同的声明里设置了subject,则可以使用这个属性。在命名空间限定的声明上使用双引号。

smallrye.jwt.claims.sub

none

当当前令牌没有可用的标准或自定义 sub 声明时,此属性可用于设置默认的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 响应header。

smallrye.jwt.jwks.forced-refresh-interval

30

强制刷新JWK缓存的时间间隔,以分钟为单位,用于限制强制刷新尝试的频率,这可能发生在令牌验证失败时,因为缓存中没有能与当前令牌带 kid 属性的JWK密钥,相匹配的 kid header。除非 mp.jwt.verify.publickey.location 指向基于HTTP或HTTPS URL的JWK集,否则它将被忽略。

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

逗号分隔的列表,列出了令牌 aud 声明可能包含的受众。该属性已被废弃—请使用 mp.jwt.verify.audiences

smallrye.jwt.required.claims

none

一个token必须包含逗号分隔的声明(claims)列表。

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

解密算法。

smallrye.jwt.decrypt.key

none

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

smallrye.jwt.token.decryption.kid

none

解密密钥标识符。如果它被设置,那么解密JWK密钥以及每个JWT标记必须有一个匹配的 kid header。

smallrye.jwt.client.tls.certificate.path

none

如果需要通过 HTTPS 获取密钥,则需要配置 TLS 信任证书的路径。

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代理主机。

smallrye.jwt.http.proxy.port

80

HTTP代理端口。

smallrye.jwt.keystore.type

JKS

如果 mp.jwt.verify.publickey.locationmp.jwt.decrypt.key.location 指向一个 KeyStore 文件,这个属性可以用来定制一个密钥库类型。如果没有设置,那么文件名将被检查以确定密钥库类型,默认为 JKS

smallrye.jwt.keystore.provider

如果 mp.jwt.verify.publickey.locationmp.jwt.decrypt.key.location 指向一个 KeyStore 文件,该属性可用于定制一个 KeyStore 提供者。

smallrye.jwt.keystore.password

Keystore密码。如果有 mp.jwt.verify.publickey.locationmp.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 不同,则可以设置此属性。

Related content