我目前正在从事一个个人项目,该项目涉及一个 Quarkus REST API 作为后端,Keycloak 作为 OpenId Connect Provider 和一个 Vue 应用程序作为前端。我只是不知道如何使这三个组件在用户身份验证中很好地协同工作,同时保持适当的安全性。
根据OAuth 2.0 for Browser-Based Apps的 v8 草案,SPA 不应该是保留访问和(可能)刷新令牌的地方,因为在这种情况下它们很难安全地存储。这意味着,后端必须充当依赖方,启动 OIDC 授权代码流。然后我会在我的 SPA 中保留一个会话 cookie(我不想这样做,以保持 API 无状态),或者将令牌存储在 Secure、SameSite、HttpOnly cookie 中。这种方法是我想要完成的,到目前为止收效甚微。
原型实现
我的 Quarkus 应用程序使用该quarkus-oicd
扩展。我理解它的方式,我必须将以下配置添加到 Quarkus' application.properties
:
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=myClientId
quarkus.oidc.credentials.secret=********
quarkus.oidc.auth-server-url=http://127.0.0.1:8082/auth/realms/myRealm
这application-type=web-app
告诉 Quarkus 它负责启动授权代码流。另一种选择是service
,在这种情况下,Quarkus 仅验证客户端发送到 API 的不记名令牌。
该 API 在端口 8081 上运行,仅公开一个示例资源:
@Path("/hello")
@Authenticated
public class ReactiveGreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello RESTEasy Reactive";
}
}
这个简单的 Vue 组件是一个概念验证:
// FetchComponent.vue
<template>
<div>
<button v-on:click="fetchFromBackend">Fetch</button>
<p><b>Output:</b>{{ message }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data() {
return {
message: "Click the button to fetch.",
};
},
methods: {
fetchFromBackend(): void {
this.message = "Waiting...";
fetch("http://localhost:8081/hello", {
credentials: "include",
})
.then((resp) => {
console.log(resp);
if (resp.redirected) {
window.location.assign(resp.url);
} else {
return resp.text().then((text) => (this.message = text));
}
})
.catch((reason) => (this.message = "Caught error: " + reason));
},
},
});
</script>
<style></style>
期望的结果
我原以为没有有效令牌的后端调用会被重定向到 Keycloak 的身份验证页面。然后,用户将输入他们的凭据,登录并使用身份验证代码重定向回 SPA。它再次调用 API。在反向通道上,Quarkus 将身份验证代码与令牌交换,并以 cookie 的形式将其转发给客户端。此 cookie 将用于对用户进行任何进一步的 API 调用的身份验证。
实际结果
当后端在没有有效令牌的情况下被调用时,它会按预期重定向到 Keycloak 的登录页面。但是,显然,根本没有办法从 JS 导航到重定向的 URL。提取规范指出:“重定向(其状态或内部响应(如果有)状态为重定向状态的响应)不会暴露给 API。暴露重定向可能会泄漏通过跨站点脚本攻击无法获得的信息。” 获取请求中的redirect: 'manual'
选项有点像红鲱鱼。它不符合人们的预期,当然也不允许我访问重定向 URL。
相反,浏览器透明地遵循重定向并尝试获取登录 URL。那根本行不通。它会导致 CORS 错误,因为 Keycloak 没有设置相关的标头(我想它不应该,因为这不是它应该如何工作的)。
我不知道如何从这里开始,但我认为答案对于更有经验的人来说是非常明显的。
作为结束语,我想补充一点,这种架构并不是一个非常明智的决策制定过程的结果。我选择它主要是因为:
- Quarkus:Java 目前是我工作的主要语言
- Keycloak:我想尝试一下适当的外部化 IAM 和 SSO 有一段时间了。这似乎是一个很好的机会。
- Vue:我想要一些东西来训练我的 JS 技能,并且在我的简历上看起来不错。当前的任何一批热门 SPA 框架都符合要求。
因此,任何类似于“这是一个糟糕的设置,只是不要这样做,而是尝试 X”的答案肯定也是受欢迎的,尽管我仍然希望以自豪的方式解决这个难题。