使用 OAuth2 RBAC
本指南解释了您的 Quarkus 应用程序如何利用 OAuth2 令牌来提供对 Jakarta REST(以前称为 JAX-RS)端点的安全访问。
OAuth2 是一个授权框架,它使应用程序能够代表用户获取对 HTTP 资源的访问权限。它可以通过委托给外部服务器(身份验证服务器)用户身份验证并为身份验证上下文提供令牌来实现基于令牌的应用程序身份验证机制。
此扩展提供了使用不透明 Bearer Token 的轻量级支持,并通过调用自省端点来验证它们。
如果 OAuth2 身份验证服务器提供 JWT Bearer Token,请考虑使用 OIDC Bearer token 身份验证 或 SmallRye JWT 扩展。如果 Quarkus 应用程序需要使用 OIDC 授权码流对用户进行身份验证,则必须使用 OpenID Connect 扩展。有关更多信息,请参阅 保护 Web 应用程序的 OIDC 代码流机制 指南。
解决方案
我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。
克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载存档。
该解决方案位于 security-oauth2-quickstart
目录中。它还包含一个非常简单的 UI 来使用此处创建的 Jakarta REST 资源。
创建 Maven 项目
首先,我们需要一个新项目。使用以下命令创建一个新项目
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数用双引号括起来,例如"-DprojectArtifactId=security-oauth2-quickstart"
此命令生成一个项目并导入 elytron-security-oauth2
扩展,其中包括 OAuth2 不透明令牌支持。
如果您不想使用 Maven 插件,只需在构建文件中包含依赖项即可
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-oauth2</artifactId>
</dependency>
implementation("io.quarkus:quarkus-elytron-security-oauth2")
检查 Jakarta REST 资源
创建具有以下内容的 src/main/java/org/acme/security/oauth2/TokenSecuredResource.java
文件
package org.acme.security.oauth2;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/secured")
public class TokenSecuredResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
这是一个基本的 REST 端点,没有任何 Elytron Security OAuth2 特定功能,所以让我们添加一些。
我们将使用 JSR 250 通用安全注解,它们在 使用安全 指南中进行了描述。
package org.acme.security.oauth2;
import java.security.Principal;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {
@GET()
@Path("permit-all")
@PermitAll (1)
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) { (2)
Principal caller = ctx.getUserPrincipal(); (3)
String name = caller == null ? "anonymous" : caller.getName();
String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
return helloReply; (4)
}
}
1 | @PermitAll 指示任何调用者(无论是否经过身份验证)都可以访问给定的端点。 |
2 | 在这里,我们注入 Jakarta REST SecurityContext 来检查调用的安全状态。 |
3 | 在这里,我们获取当前请求用户/调用者 Principal 。对于不安全的调用,这将为 null,因此我们通过检查 caller 是否为 null 来构建用户名。 |
4 | 我们构建的回复使用了调用者名称、请求 SecurityContext 的 isSecure() 和 getAuthenticationScheme() 状态。 |
设置 application.properties
您需要使用以下最小属性配置您的应用程序
quarkus.oauth2.client-id=client_id
quarkus.oauth2.client-secret=secret
quarkus.oauth2.introspection-url=http://oauth-server/introspect
您需要指定身份验证服务器的自省 URL 以及您的应用程序将用于向身份验证服务器进行身份验证的 client-id
/ client-secret
。
然后,扩展将使用此信息来验证令牌并恢复与之关联的信息。
有关所有配置属性,请参阅本指南末尾的 配置参考 部分。
运行应用程序
现在我们准备好运行我们的应用程序了。使用
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
现在 REST 端点正在运行,我们可以使用像 curl 这样的命令行工具访问它
$ curl http://127.0.0.1:8080/secured/permit-all; echo
hello + anonymous, isSecure: false, authScheme: null
我们没有在请求中提供任何令牌,因此我们不希望端点看到任何安全状态,并且响应与此一致
-
用户名是 anonymous
-
isSecure
为 false,因为未使用 https -
authScheme
为 null
保护端点
现在让我们实际保护一些东西。查看以下新的端点方法 helloRolesAllowed
package org.acme.security.oauth2;
import java.security.Principal;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {
@GET()
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
Principal caller = ctx.getUserPrincipal();
String name = caller == null ? "anonymous" : caller.getName();
String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
return helloReply;
}
@GET()
@Path("roles-allowed") (1)
@RolesAllowed({"Echoer", "Subscriber"}) (2)
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowed(@Context SecurityContext ctx) {
Principal caller = ctx.getUserPrincipal();
String name = caller == null ? "anonymous" : caller.getName();
String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
return helloReply;
}
}
1 | 这个新端点将位于 /secured/roles-allowed |
2 | @RolesAllowed 指示如果调用者被分配了“Echoer”或“Subscriber”角色,则可以访问给定的端点。 |
在您将此添加到您的 TokenSecuredResource
后,尝试 curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
以尝试访问新端点。您的输出应该是
$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /secured/roles-allowed HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Connection: keep-alive
< Content-Type: text/html;charset=UTF-8
< Content-Length: 14
< Date: Sun, 03 Mar 2019 16:32:34 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Not authorized
太棒了,我们没有在请求中提供任何 OAuth2 令牌,所以我们不应该能够访问该端点,而我们也没有。相反,我们收到了 HTTP 401 Unauthorized 错误。我们需要获取并传入有效的 OAuth2 令牌才能访问该端点。这需要两个步骤,1) 使用有关如何验证令牌的信息配置我们的 Elytron Security OAuth2 扩展,以及 2) 生成具有适当声明的匹配令牌。
生成令牌
您需要使用令牌端点从标准 OAuth2 身份验证服务器(例如 Keycloak)获取令牌。
您可以在下面找到 client_credential
流的此类调用的 curl 示例
curl -X POST "http://oauth-server/token?grant_type=client_credentials" \
-H "Accept: application/json" -H "Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ="
它应该像这样响应……
{"access_token":"60acf56d-9daf-49ba-b3be-7a423d9c7288","token_type":"bearer","expires_in":1799,"scope":"READER"}
角色映射
角色是从自省端点响应的声明之一映射的。默认情况下,它是 scope
声明。通过使用空格分隔符拆分声明来获取角色。如果声明是一个数组,则不进行拆分,角色从数组中获取。
您可以使用 quarkus.oauth2.role-claim
属性自定义用于角色的声明的名称。
打包并运行应用程序
与往常一样,可以使用以下命令打包应用程序
quarkus build
./mvnw install
./gradlew build
并使用 java -jar target/quarkus-app/quarkus-run.jar
执行
[INFO] Scanning for projects...
...
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO [io.quarkus] (main) Quarkus 3.24.4 started in 0.796s. Listening on: http://[::]:8080
2019-03-28 14:27:48,841 INFO [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]
您还可以使用以下命令生成原生可执行文件
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
[INFO] Scanning for projects...
...
[security-oauth2-quickstart-runner:25602] universe: 493.17 ms
[security-oauth2-quickstart-runner:25602] (parse): 660.41 ms
[security-oauth2-quickstart-runner:25602] (inline): 1,431.10 ms
[security-oauth2-quickstart-runner:25602] (compile): 7,301.78 ms
[security-oauth2-quickstart-runner:25602] compile: 10,542.16 ms
[security-oauth2-quickstart-runner:25602] image: 2,797.62 ms
[security-oauth2-quickstart-runner:25602] write: 988.24 ms
[security-oauth2-quickstart-runner:25602] [total]: 43,778.16 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 51.500 s
[INFO] Finished at: 2019-06-28T14:30:56-07:00
[INFO] ------------------------------------------------------------------------
$ ./target/security-oauth2-quickstart-runner
2019-03-28 14:31:37,315 INFO [io.quarkus] (main) Quarkus 0.20.0 started in 0.006s. Listening on: http://[::]:8080
2019-03-28 14:31:37,316 INFO [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]
集成测试
如果您不想为集成测试使用真实的 OAuth2 授权服务器,则可以使用 基于属性的安全 扩展进行测试,或使用 Wiremock 模拟授权服务器。
首先,需要将 Wiremock 添加为测试依赖项。对于 Maven 项目,这将像这样发生
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> (1)
</dependency>
1 | 使用正确的 Wiremock 版本。所有可用版本都可以在 这里 找到。 |
testImplementation("org.wiremock:wiremock:${wiremock.version}") (1)
1 | 使用正确的 Wiremock 版本。所有可用版本都可以在 这里 找到。 |
在 Quarkus 测试中,当需要在运行 Quarkus 测试之前启动某些服务时,我们利用 @io.quarkus.test.common.QuarkusTestResource
注解来指定一个 io.quarkus.test.common.QuarkusTestResourceLifecycleManager
,它可以启动服务并提供 Quarkus 将使用的配置值。
有关 |
让我们创建一个名为 MockAuthorizationServerTestResource
的 QuarkusTestResourceLifecycleManager
实现,如下所示
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import java.util.Collections;
import java.util.Map;
public class MockAuthorizationServerTestResource implements QuarkusTestResourceLifecycleManager { (1)
private WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer();
wireMockServer.start(); (2)
// define the mock for the introspect endpoint
WireMock.stubFor(WireMock.post("/introspect").willReturn(WireMock.aResponse() (3)
.withBody(
"{\"active\":true,\"scope\":\"Echoer\",\"username\":null,\"iat\":1562315654,\"exp\":1562317454,\"expires_in\":1458,\"client_id\":\"my_client_id\"}")));
return Collections.singletonMap("quarkus.oauth2.introspection-url", wireMockServer.baseUrl() + "/introspect"); (4)
}
@Override
public void stop() {
if (null != wireMockServer) {
wireMockServer.stop(); (5)
}
}
}
1 | start 方法在任何测试运行之前由 Quarkus 调用,并返回一个配置属性 Map ,该配置属性在测试执行期间适用。 |
2 | 启动 Wiremock。 |
3 | 配置 Wiremock 以通过返回 OAuth2 自省响应来存根对 /introspect 的调用。您需要自定义此行以返回您的应用程序所需的内容(至少 scope 属性,因为角色是从 scope 派生的)。 |
4 | 由于 start 方法返回适用于测试的配置,因此我们设置 quarkus.oauth2.introspection-url 属性,该属性控制 OAuth2 扩展使用的自省端点的 URL。 |
5 | 当所有测试完成后,关闭 Wiremock。 |
您的测试类需要使用 @QuarkusTestResource(MockAuthorizationServerTestResource.class)
进行注解,以使用此 QuarkusTestResourceLifecycleManager
。
下面是一个使用 MockAuthorizationServerTestResource
的测试示例。
@QuarkusTest
@QuarkusTestResource(MockAuthorizationServerTestResource.class) (1)
class TokenSecuredResourceTest {
// use whatever token you want as the mock OAuth server will accept all tokens
private static final String BEARER_TOKEN = "337aab0f-b547-489b-9dbd-a54dc7bdf20d"; (2)
@Test
void testPermitAll() {
RestAssured.given()
.when()
.header("Authorization", "Bearer " + BEARER_TOKEN) (3)
.get("/secured/permit-all")
.then()
.statusCode(200)
.body(containsString("hello"));
}
@Test
void testRolesAllowed() {
RestAssured.given()
.when()
.header("Authorization", "Bearer " + BEARER_TOKEN)
.get("/secured/roles-allowed")
.then()
.statusCode(200)
.body(containsString("hello"));
}
}
1 | 将先前创建的 MockAuthorizationServerTestResource 用作 Quarkus 测试资源。 |
2 | 定义您想要的任何令牌,它不会被 OAuth2 模拟授权服务器验证。 |
3 | 在 Authorization 标头中使用此令牌以触发 OAuth2 身份验证。 |
|
配置参考
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
确定是否启用 OAuth2 扩展。如果您包含 环境变量: 显示更多 |
布尔值 |
|
字符串 |
|
|
字符串 |
||
用于验证令牌的 OAuth2 客户端密钥。如果启用了扩展,则为必需。 环境变量: 显示更多 |
字符串 |
|
用于验证令牌并收集身份验证声明的 OAuth2 自省端点 URL。如果启用了扩展,则为必需。 环境变量: 显示更多 |
字符串 |
|
OAuth2 服务器证书文件。警告:本机模式不支持此功能,其中证书必须包含在本机映像生成期间使用的信任存储中,请参阅 将 SSL 与本机可执行文件一起使用。 环境变量: 显示更多 |
字符串 |
|
令牌自省的客户端连接超时。如果未设置,则为无限。 环境变量: 显示更多 |
||
关于 Duration 格式
要写入 duration 值,请使用标准 您还可以使用简化的格式,以数字开头
在其他情况下,简化格式将被转换为
|