2.5. 编写应用程序
首先实施 ProtectedResource :
package org.acme.security.openid.connect.client;
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 io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/protected")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken principal;
@GET
@RolesAllowed("user")
@Produces("text/plain")
@Path("userName")
public Uni<String> userName() {
return Uni.createFrom().item(principal.getName());
}
@GET
@RolesAllowed("admin")
@Produces("text/plain")
@Path("adminName")
public Uni<String> adminName() {
return Uni.createFrom().item(principal.getName());
}
}
ProtectedResource 返回一个来自 userName () 和 adminName () 方法的名称。名称从当前的 JsonWebToken 中提取。
接下来,添加以下 REST 客户端:
-
RestClientWithOidcClientFilter,它使用quarkus-rest-client-oidc-filter扩展提供的 OIDC 客户端过滤器来获取和传播访问令牌。 -
RestClientWithTokenHeaderParam,它接受以编程方式创建的 OidcClient 作为 HTTPAuthorization标头值而获取的令牌。 -
RestClientWithTokenPropagationFilter,它使用quarkus-rest-client-oidc-token-propagation扩展提供的 OIDC 令牌传播过滤器来获取和传播访问令牌。
添加 RestClientWithOidcClientFilter REST 客户端:
package org.acme.security.openid.connect.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface RestClientWithOidcClientFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
- 1
- 在 REST 客户端中注册 OIDC 客户端过滤器,以获取和传播令牌。
添加 RestClientWithTokenHeaderParam REST 客户端:
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@RegisterRestClient
@Path("/")
public interface RestClientWithTokenHeaderParam {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName(@HeaderParam("Authorization") String authorization);
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName(@HeaderParam("Authorization") String authorization);
}
添加 RestClientWithTokenPropagationFilter REST 客户端:
package org.acme.security.openid.connect.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@AccessToken
@Path("/")
public interface RestClientWithTokenPropagationFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
- 1
- 使用 REST 客户端注册 OIDC 令牌传播过滤器,以传播传入的已存在的令牌。
不要在同一 REST 客户端中使用 RestClientWithOidcClientFilter 和 RestClientWithTokenPropagationFilter 接口,因为它们可能会发生冲突,从而导致问题。例如,OIDC 客户端过滤器可以从 OIDC 令牌传播过滤器覆盖令牌,或者传播过滤器在 none 可用时尝试传播令牌,则希望 OIDC 客户端过滤器获取新令牌。
另外,添加 OidcClientCreator 以编程方式创建 OIDC 客户端。OidcClientCreator 支持 RestClientWithTokenHeaderParam REST 客户端调用:
package org.acme.security.openid.connect.client;
import java.util.Map;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.runtime.OidcClientConfig;
import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
@ApplicationScoped
public class OidcClientCreator {
@Inject
OidcClients oidcClients;
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String oidcProviderAddress;
private volatile OidcClient oidcClient;
public void startup(@Observes StartupEvent event) {
createOidcClient().subscribe().with(client -> {oidcClient = client;});
}
public OidcClient getOidcClient() {
return oidcClient;
}
private Uni<OidcClient> createOidcClient() {
OidcClientConfig cfg = OidcClientConfig
.authServerUrl(oidcProviderAddress)
.id("myclient")
.clientId("backend-service")
.credentials("secret")
.grant(Type.PASSWORD)
.grantOptions("password", Map.of("username", "alice", "password", "alice"))
.build();
return oidcClients.newClient(cfg);
}
}
- 1
OidcClients可用于检索已初始化的、名为 OIDC 客户端并按需创建新的 OIDC 客户端。
现在,通过添加 FrontendResource 来完成创建应用程序:
package org.acme.security.openid.connect.client;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.client.runtime.TokensHelper;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.smallrye.mutiny.Uni;
@Path("/frontend")
public class FrontendResource {
@Inject
@RestClient
RestClientWithOidcClientFilter restClientWithOidcClientFilter;
@Inject
@RestClient
RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter;
@Inject
OidcClientCreator oidcClientCreator;
TokensHelper tokenHelper = new TokensHelper();
@Inject
@RestClient
RestClientWithTokenHeaderParam restClientWithTokenHeaderParam;
@GET
@Path("user-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getUserNameWithOidcClientToken() {
return restClientWithOidcClientFilter.getUserName();
}
@GET
@Path("admin-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithOidcClientToken() {
return restClientWithOidcClientFilter.getAdminName();
}
@GET
@Path("user-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getUserNameWithPropagatedToken() {
return restClientWithTokenPropagationFilter.getUserName();
}
@GET
@Path("admin-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithPropagatedToken() {
return restClientWithTokenPropagationFilter.getAdminName();
}
@GET
@Path("user-name-with-oidc-client-token-header-param")
@Produces("text/plain")
public Uni<String> getUserNameWithOidcClientTokenHeaderParam() {
return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
.transformToUni(tokens -> restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()));
}
@GET
@Path("admin-name-with-oidc-client-token-header-param")
@Produces("text/plain")
public Uni<String> getAdminNameWithOidcClientTokenHeaderParam() {
return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
.transformToUni(tokens -> restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()));
}
@GET
@Path("user-name-with-oidc-client-token-header-param-blocking")
@Produces("text/plain")
public String getUserNameWithOidcClientTokenHeaderParamBlocking() {
Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely();
return restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()).await().indefinitely();
}
@GET
@Path("admin-name-with-oidc-client-token-header-param-blocking")
@Produces("text/plain")
public String getAdminNameWithOidcClientTokenHeaderParamBlocking() {
Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely();
return restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()).await().indefinitely();
}
}
- 1 5 6
FrontendResource使用带有 OIDC 客户端过滤器注入的RestClientWithOidcClientFilterREST 客户端来获取并传播访问令牌到ProtectedResource(当/frontend/user-name-with-oidc-client-token或/frontend/admin-name-with-oidc-client-token)。- 2 7 8
FrontendResource使用注入的RestClientWithTokenPropagationFilterREST 客户端以及 OIDC 令牌传播过滤器,以便在/frontend/user-name-with-propagated-token或/frontend/admin-name-with-propagated-token被调用时将当前的传入访问令牌传播到ProtectedResource。- 4 9 10
FrontendResource使用以编程方式创建的 OIDC 客户端来获取和传播访问令牌到ProtectedResource,方法是将其直接传递给注入的RestClientWithTokenHeaderParamREST 客户端方法作为 HTTPAuthorization标头值,即/frontend/user-name-with-oidc-client-token-header-param或/frontend/admin-name-with-oidc-client-token-header-param。- 11 12
- 有时,在向令牌传播令牌之前,可能需要以阻止方式获取令牌。本例演示了如何在这样的情形中获取令牌。
- 3
- 当 OIDC 客户端直接使用时,
io.quarkus.oidc.client.runtime.TokensHelper是一个非常有用的工具,没有 OIDC 客户端过滤器。要使用TokensHelper,请将 OIDC Client 传递给它以获取令牌,并且TokensHelper获取令牌,并在需要时以线程安全的方式刷新它们。
最后,添加 Jakarta REST ExceptionMapper :
package org.acme.security.openid.connect.client;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.jboss.resteasy.reactive.ClientWebApplicationException;
@Provider
public class FrontendExceptionMapper implements ExceptionMapper<ClientWebApplicationException> {
@Override
public Response toResponse(ClientWebApplicationException t) {
return Response.status(t.getResponse().getStatus()).build();
}
}
此例外映射程序仅用于在测试期间验证 ProtectedResource 是否在令牌没有预期的角色时返回 403。如果没有此映射程序,Quarkus REST (以前称为 RESTEasy Reactive)可以正确地将来自 REST 客户端调用的异常转换为 500,以避免泄漏下游资源(如 ProtectedResource )的信息。但是,在测试中,无法断言 500 是由授权异常导致的,而不是一些内部错误。