如何使用外部 REST API 服务器(基于 Vert.x/Kotlin)和 Keycloak 实现 Nuxt.js/Vue.js OAuth2 身份验证 🐬
介绍
通过 OAuth2 进行身份验证
👾 Nuxt.js 设置
🚀 SirixDB HTTP 服务器:基于 Vert.x 的 REST API
💚 设置 Keycloak
结论
一个可嵌入的、双时态的、仅追加的数据库系统和事件存储系统
SirixDB Web 前端 - 一个演进式、版本化、时间化的 NoSQL 文档存储库
介绍
身份验证很复杂。因此,最好将身份验证委托给专门的软件。我们选择使用 Keycloak。
我们希望 为 SirixDB (一个时态文档存储系统)构建一个基于 Nuxt.js 的 前端 ,以便高效地存储和查询数据快照。HTTP 服务器提供非阻塞、异步的 REST API。我们决定使用 Kotlin(大量使用协程)和 Vert.x 来实现 API 服务器。
通过 OAuth2 进行身份验证
OAuth2 定义了几种所谓的流程。对于基于浏览器的应用程序, 授权码流程 是最佳且最安全的流程,我们将使用此流程。
💚 使用 Nuxt.js 实现 OAuth2 授权码流程
我们有一个工作流,其中只有 SirixDB HTTP 服务器直接与 Keycloak 交互(除了重定向到 Node.js 服务器)。因此,我们的前端只需要知道 SirixDB HTTP 服务器的两个路由: GET /user/authorize和 POST /token。
一般来说,我们的工作流程如下:
身份验证中间件控制用户是否应该 /login首先被重定向到登录路由。
该 /login路由包含一个简单的按钮,用于向 SirixDB HTTP 服务器发出请求。Nuxt.js 会生成一个唯一的、无法猜测的 stateURL ,并将其 作为 URL 参数 redirect_uri传递给该路由。 GET /user/authorize
HTTP 服务器重定向到 Keycloak 的登录页面,并发送这两个参数。
用户正确填写凭据后,Keycloak 会将浏览器重定向到给定的 redirect_url,该 URL 首先由 Nuxt.js 发送(并发送到 SirixDB HTTP 服务器)。
在基于 Node.js 的服务器上,基于 Nuxt.js 的前端通过 Keycloak 的重定向 URL 来访问回调路由。
Nuxt.js 随后提取 URL 参数 code并检查该 state参数的有效性。
接下来,Nuxt.js 会向 SirixDB HTTP 服务器上的端点发送一个 POSTHTTP 请求,并再次传递 参数 ,该参数指向相同的回调路由。此外,它还会发送一个 我们设置为代码的 JWT 访问令牌,以便 Nuxt.js 能够识别该访问令牌。 /tokencoderedirect_uriresponse_type
SirixDB HTTP 服务器随后将给定的代码与 Keycloak 生成的 JWT 访问令牌交换,并将其包含在 HTTP 响应中发送给基于 Nuxt.js 的前端。
请注意,如果我们处于通用模式(非单页应用模式),则可以简化此工作流程。正如我们稍后将看到的,Nuxt.js 中的 Node.js 服务器也可以直接与 Keycloak 通信。在这种设置下,SirixDB HTTP 服务器将仅根据已颁发的 JWT 令牌检查其路由的授权。这样一来,前端无需了解 Keycloak 的具体情况,也无需知道主机/端口和端点详细信息。此外,我们将看到 Nuxt.js 本身并不支持 Keycloak。
👾 Nuxt.js 设置
在 Nuxt.js 配置文件中, nuxt.config.js我们需要添加以下模块:
['@nuxtjs/axios', { baseURL: 'https://localhost:9443' }], '@nuxtjs/auth', '@nuxtjs/proxy'
Enter fullscreen mode
Exit fullscreen mode
然后我们再补充:
axios: {
baseURL: 'https://localhost:9443',
browserBaseURL: 'https://localhost:9443',
proxyHeaders: true,
proxy: true,
},
auth: {
strategies: {
keycloak: {
_scheme: 'oauth2',
authorization_endpoint: 'https://localhost:9443/user/authorize',
userinfo_endpoint: false,
access_type: 'offline',
access_token_endpoint: 'https://localhost:9443/token',
response_type: 'code',
token_type: 'Bearer',
token_key: 'access_token',
},
},
redirect: {
login: '/login',
callback: '/callback',
home: '/'
},
},
router: {
middleware: ['auth']
}
Enter fullscreen mode
Exit fullscreen mode
https://localhost:9443这是 SirixDB HTTP 服务器正在监听的主机/端口。
默认情况下,我们的 Nuxt.js 配置会在所有路由上启用身份验证中间件。如果用户未通过身份验证,则会执行第一步,Nuxt.js 的身份验证模块会将用户重定向到相应的 GET /login路由。
我们将定义一个简单的 login页面:
<template>
<div>
<h3> Login</h3>
<el-button type= "primary" @ click= "login()" > Login via Keycloak</el-button>
</div>
</template>
<script lang= "ts" >
import { Component , Prop , Vue } from " vue-property-decorator " ;
@ Component
export default class Login extends Vue {
private login (): void {
this . $auth . loginWith ( ' keycloak ' )
}
}
</script>
<style lang= "scss" >
</style>
Enter fullscreen mode
Exit fullscreen mode
为了定义要使用的正确 TypeScript 类型, this.$auth我们需要添加
"typings" : "types/index.d.ts" ,
"files" : [ "types/*.d.ts" ]
Enter fullscreen mode
Exit fullscreen mode
添加到 package.json文件中。此外,我们将创建 types目录并添加 index.d.ts 文件。
在 Nuxt.js 应用程序的插件文件夹中,我们将添加一个文件来扩展 axios 客户端:
export default function ({ $axios , redirect }) {
$axios . defaults . httpsAgent = new https . Agent ({ rejectUnauthorized : false })
$axios . onRequest ( config => {
config . headers . common [ ' Origin ' ] = ' http://localhost:3005 ' ;
config . headers . common [ ' Content-Type ' ] = ' application/json ' ;
config . headers . common [ ' Accept ' ] = ' application/json ' ;
config . headers . put [ ' Origin ' ] = ' http://localhost:3005 ' ;
config . headers . put [ ' Content-Type ' ] = ' application/json ' ;
config . headers . put [ ' Accept ' ] = ' application/json ' ;
config . headers . post [ ' Origin ' ] = ' http://localhost:3005 ' ;
config . headers . post [ ' Content-Type ' ] = ' application/json ' ;
config . headers . post [ ' Accept ' ] = ' application/json ' ;
config . headers . del [ ' Origin ' ] = ' http://localhost:3005 ' ;
config . headers . del [ ' Content-Type ' ] = ' application/json ' ;
config . headers . del [ ' Accept ' ] = ' application/json ' ;
});
$axios . onError ( error => {
const code = parseInt ( error . response && error . response . status );
if ( code === 401 ) {
redirect ( ' https://localhost:9443/user/authorize ' );
}
});
}
Enter fullscreen mode
Exit fullscreen mode
现在我们已经完成了 Nuxt.js 部分。接下来,我们将研究 SirixDB HTTP 服务器。
🚀 SirixDB HTTP 服务器:基于 Vert.x 的 REST API
我们需要设置 OAuth2 登录路由以及所有其他与 OAuth2 配置相关的内容。
但首先,我们将为 OAuth2 身份验证码流程添加 CORS 处理程序:
if ( oauth2Config . flow == OAuth2FlowType . AUTH_CODE ) {
val allowedHeaders = HashSet < String >()
allowedHeaders . add ( "x-requested-with" )
allowedHeaders . add ( "Access-Control-Allow-Origin" )
allowedHeaders . add ( "origin" )
allowedHeaders . add ( "Content-Type" )
allowedHeaders . add ( "accept" )
allowedHeaders . add ( "X-PINGARUNER" )
allowedHeaders . add ( "Authorization" )
val allowedMethods = HashSet < HttpMethod >()
allowedMethods . add ( HttpMethod . GET )
allowedMethods . add ( HttpMethod . POST )
allowedMethods . add ( HttpMethod . OPTIONS )
allowedMethods . add ( HttpMethod . DELETE )
allowedMethods . add ( HttpMethod . PATCH )
allowedMethods . add ( HttpMethod . PUT )
this . route (). handler ( CorsHandler . create ( "*" )
. allowedHeaders ( allowedHeaders )
. allowedMethods ( allowedMethods ))
}
Enter fullscreen mode
Exit fullscreen mode
OAuth2 配置通过以下方式读取:
val oauth2Config = oAuth2ClientOptionsOf ()
. setFlow ( OAuth2FlowType . valueOf ( config . getString ( "oAuthFlowType" , "PASSWORD" )))
. setSite ( config . getString ( "keycloak.url" ))
. setClientID ( "sirix" )
. setClientSecret ( config . getString ( "client.secret" ))
. setTokenPath ( config . getString ( "token.path" , "/token" ))
. setAuthorizationPath ( config . getString ( "auth.path" , "/user/authorize" ))
val keycloak = KeycloakAuth . discoverAwait (
vertx , oauth2Config
)
Enter fullscreen mode
Exit fullscreen mode
配置文件内容如下:
{
"https.port" : 9443 ,
"keycloak.url" : "http://localhost:8080/auth/realms/sirixdb" ,
"auth.path" : "http://localhost:8080/auth/realms/sirixdb/protocol/openid-connect/auth" ,
"token.path" : "/token" ,
"client.secret" : "2e54cfdf-909b-47ca-b385-4c44886f04f0" ,
"oAuthFlowType" : "AUTH_CODE" ,
"redirect.uri" : "http://localhost:3005/callback"
}
Enter fullscreen mode
Exit fullscreen mode
请注意,通常情况下,Nuxt.js 会指定重定向 URI,在这种情况下,SirixDB HTTP 服务器会从 URL 查询参数中读取该 URI。
HTTP 服务器使用以下扩展函数来提供协程处理程序,而挂起函数则在 Vert.x 事件循环中运行:
/**
* An extension method for simplifying coroutines usage with Vert.x Web routers.
*/
private fun Route . coroutineHandler ( fn : suspend ( RoutingContext ) -> Unit ): Route {
return handler { ctx ->
launch ( ctx . vertx (). dispatcher ()) {
try {
fn ( ctx )
} catch ( e : Exception ) {
ctx . fail ( e )
}
}
}
}
Enter fullscreen mode
Exit fullscreen mode
路径 GET /user/authorize(步骤 2)。浏览器将重定向到 Keycloak 登录页面。
get ( "/user/authorize" ). coroutineHandler { rc ->
if ( oauth2Config . flow != OAuth2FlowType . AUTH_CODE ) {
rc . response (). statusCode = HttpStatus . SC_BAD_REQUEST
} else {
val redirectUri =
rc . queryParam ( "redirect_uri" ). getOrElse ( 0 ) { config . getString ( "redirect.uri" ) }
val state = rc . queryParam ( "state" ). getOrElse ( 0 ) { java . util . UUID . randomUUID (). toString () }
val authorizationUri = keycloak . authorizeURL (
JsonObject ()
. put ( "redirect_uri" , redirectUri )
. put ( "state" , state )
)
rc . response (). putHeader ( "Location" , authorizationUri )
. setStatusCode ( HttpStatus . SC_MOVED_TEMPORARILY )
. end ()
}
}
Enter fullscreen mode
Exit fullscreen mode
提供凭据后,浏览器会被重定向到 redirect_uri(即 /callback 路由),并带有给定的状态(该状态最初由 Nuxt.js 生成)。然后,Nuxt.js 的身份验证模块会从 URL 查询参数中提取状态码 state和 code响应类型。如果状态与生成的状态相同,则会继续 POST 请求,并将重定向到 redirect_uri 和响应类型作为表单参数存储起来。
路线 POST /token(步骤 7):
post ( "/token" ). handler ( BodyHandler . create ()). coroutineHandler { rc ->
try {
val dataToAuthenticate : JsonObject =
when ( rc . request (). getHeader ( HttpHeaders . CONTENT_TYPE )) {
"application/json" -> rc . bodyAsJson
"application/x-www-form-urlencoded" -> formToJson ( rc )
else -> rc . bodyAsJson
}
val user = keycloak . authenticateAwait ( dataToAuthenticate )
rc . response (). end ( user . principal (). toString ())
} catch ( e : DecodeException ) {
rc . fail (
HttpStatusException (
HttpResponseStatus . INTERNAL_SERVER_ERROR . code (),
"\"application/json\" and \"application/x-www-form-urlencoded\" are supported Content-Types." +
"If none is specified it's tried to parse as JSON"
)
)
}
}
private fun formToJson ( rc : RoutingContext ): JsonObject {
val formAttributes = rc . request (). formAttributes ()
val code =
formAttributes . get ( "code" )
val redirectUri =
formAttributes . get ( "redirect_uri" )
val responseType =
formAttributes . get ( "response_type" )
return JsonObject ()
. put ( "code" , code )
. put ( "redirect_uri" , redirectUri )
. put ( "response_type" , responseType )
}
Enter fullscreen mode
Exit fullscreen mode
SirixDB HTTP 服务器从 Keycloak 获取 JWT 令牌并将其发送回前端。
之后,Nuxt.js 会将令牌存储在其会话、存储等位置。
最后,Axios 需要将每个 API 请求的令牌作为 Bearer 令牌发送到 Authorization-Header 中。我们可以通过以下方式检索令牌 this.$auth.getToken('keycloak'):
请注意,Nuxt.js/Node.js 可以直接与 Keycloak 交互,而无需使用 SirixDB HTTP 服务器进行间接交互,SirixDB HTTP 服务器只需验证 JWT 令牌即可。
在这种情况下, nuxt.config.jsKeycloak 认证对象如下所示:
keycloak: {
_scheme: 'oauth2',
authorization_endpoint: 'http://localhost:8080/auth/realms/sirixdb/protocol/openid-connect/auth',
userinfo_endpoint: false,
access_type: 'offline',
access_token_endpoint: 'http://localhost:8080/auth/realms/sirixdb/protocol/openid-connect/token',
response_type: 'code',
token_type: 'Bearer',
token_key: 'access_token',
client_secret: '2e54cfdf-909b-47ca-b385-4c44886f04f0',
client_id: 'sirix'
}
Enter fullscreen mode
Exit fullscreen mode
在这种情况下,我们需要将以下内容添加 http://localhost:3005到 Keycloak 中允许的 Web 源中,我们将在下一节中看到。
然而,我无法使其正常工作,因为Nuxt.js 的 身份验证模块 不知何故没有将 client_secret 发送到 Keycloak token端点:
错误:"unauthorized_client" 错误描述:"请求中未提供客户端密钥"
💚 设置 Keycloak
您可以按照这篇优秀的教程 中的说明配置 Keycloak 。以下是对 SirixDB 的简要概述(您可以使用 SirixDB 的 docker-compose 文件跳过某些部分)。不过,它应该与其他项目的 Keycloak 配置几乎相同。
简而言之 :
打开浏览器。网址: http://localhost:8080 使用用户名 admin和密码登录 admin以访问 Keycloaks Web 配置界面
创建一个名为的新领域 sirixdb
前往“客户”=>“帐户”
将客户端 ID 更改为 sirix
请确保访问类型设置为“机密”。
转到“凭据”选项卡
将客户端密钥添加到 SirixDB HTTP 服务器配置文件(如上所示)中。将值更改 client.secret为 Keycloak 设置的任何值。
必须在设置选项卡中启用标准流程。
将有效的重定向 URI 设置为 http://localhost:3005/* 或端口 3000 或 Nuxt.js 应用程序运行的任何位置
请确保设置正确的值,以 Web Origins允许来自这些域的 CORS。
结论
让所有组件协同工作带来了一些麻烦。一个简化的办法是让 Nuxt.js 先完成所有身份验证,然后让外部 API 服务器检查令牌。
如果您觉得这篇文章对您有帮助,或者我把整个授权过程描述得太复杂了,请告诉我。
关于 SirixDB 和 前端, 我非常希望得到一些意见甚至贡献,那将是莫大的荣幸 :-) 我是一名后端工程师,目前正在利用业余时间学习 Nuxt.js/Vue.js、TypeScript 和 D3,以用于这个项目。这是一个全新的项目,所以我们可以使用 Vue.js 的组合式 API 等。🐣
如果你喜欢这个项目,不妨在推特之类的社交媒体上分享一下,让更多人知道!🙈
在GitHub 上为 SirixDB 和 GitHub SirixDB Web Frontend 项目 做贡献 💚
SirixDB 是一个可嵌入的、双时态的、仅追加的数据库系统和事件存储系统,用于存储不可变的轻量级快照。它保留了每个资源的完整历史记录。每次提交都通过结构共享存储一个空间高效的快照。它采用日志结构,并且永远不会覆盖数据。SirixDB 使用了一种新颖的页面级版本控制方法。
基于 Nuxt.js/Vue.js、D3.js 和 Typescript 的 SirixDB Web 前端
此致敬礼, 约翰内斯
文章来源:https://dev.to/johanneslichtenberger/how-to-implement-nuxt-js-vue-js-oauth2-authentication-with-an-external-rest-api-server-based-on-vert-x-kotlin-and-keycloak-3c1h