构建一个带身份验证的 Deno 应用
Node.js 的创建者 Ryan Dahl 创建了一个用于设计 Web 应用程序的新框架。他回过头来,利用在最初编写 Node 时还不可用的新技术,纠正了事后发现的一些错误。这就是 Deno(发音为 DEH-no),一个用 TypeScript 编写的 “类似 Node 的” Web 应用的框架。在本文中,我将引导你创建一个带有身份验证的基本 Web 应用。
要点
- 创建你的 Deno 应用
- 用 Deno 构建真实的 Web 应用
- 为你的 Deno 应用添加功能
- 用 Okta 添加身份验证
- 运行 Deno 程序
你几乎可以在 Deno 网站上找到所需的所有信息,以及有关当前可用于 Deno 的所有第三方库的信息。当前框架最大的缺点应该是:它只是在 2020 年 5 月 13 日发行了1.0版,因此即使有很多基本库,也没有 Node 的库那么多。不过对于那些精通 Node 的人,向 Deno 过渡应该很容易。
你可以在 https://deno.land/#installation 中找到安装说明。
创建你的 Deno 应用
我找不到任何基本的脚手架库,所以只能从一个空文件夹开始。在程序的根文件夹中,创建一个名为 index.ts
的文件,这将作为你 Deno 程序的起点。我们将会使用 Opine,它是 Deno 的 Express 克隆版本,可简化构建和路由。
与 Deno 不同的是,没有用于引入第三方库的包管理器。你可以通过使用库的完整 URL 来完成此操作。在 index.ts
文件顶部执行此操作,然后设置一个基本的 Web 应用程序。
import { opine } from 'https://deno.land/x/opine@0.12.0/mod.ts';const app = opine();
app.get('/', (req, res) => {
res.send('Deno Sample');
});
app.listen(3000);
console.log('running on port 3000');
然后,在终端下切换到程序文件夹中,并输入以下内容来运行这个非常基本的程序:
deno run -A index.ts
-A
是用于开发目的的快捷选项。在默认情况下,Deno 完全处于锁定状态,所以需要把参数传递给 run 命令以允许访问,例如 --allow-net
允许联网, --allow-read
允许程序从文件系统读取。这里的 -A
允许所有内容,从而有效地禁用了所有安全性。当你运行这个程序然后转到 http://localhost:3000
时,空白页上将会出现 Deno Sample 字样。
用 Deno 构建真实的 Web 应用
虽然这是一个良好的开端,但并没有太大用处。你还需要添加一些“真实”的功能,这些功能比“真实世界”要多一些,接下来修改 index.ts
文件,使其内容为:
import { opine, serveStatic } from 'https://deno.land/x/opine@0.12.0/mod.ts';import { renderFileToString } from 'https://deno.land/x/dejs@0.7.0/mod.ts';
import { join, dirname } from 'https://deno.land/x/opine@main/deps.ts';
import { ensureAuthenticated } from './middleware/authmiddleware.ts';
import users from './controllers/usercontroller.ts';
import auth from './controllers/authcontroller.ts';
const app = opine();
const __dirname = dirname(import.meta.url);
app.engine('.html', renderFileToString);
app.use(serveStatic(join(__dirname, 'public')));
app.set('view engine', 'html');
app.get('/', (req, res) => {
res.render('index', { title: 'Deno Sample' });
});
app.use('/users', ensureAuthenticated, users);
app.use('/auth', auth)
app.listen(3000);
console.log('running on port 3000');
你会注意到很多的 import
语句,这些语句引入了一些第三方库。在这里,我用的是 dejs,这是 Deno 的 EJS 端口。我还引入了 Opine 库中的一些用于处理目录名称的类。我在后面将会介绍本地导入的这三个文件。现在你只需要知道导入了它们。
opine()
实例化下面的代码行创建对本地目录的引用。下面的三行代码将视图引擎设置为 DEJS,用来处理类似 HTML 的文件,这很像 EJS 对 Node 的处理方式。下一部分已稍作更改以渲染这些 HTML 模板文件,并且最后两行代码引入了一些外部路由。需要注意的一件事是 /users
路由具有 ensureAuthenticated()
中间件功能。这将迫使用户先登录,然后才能访问该页面。
为你的 Deno 应用添加功能
接下来创建一些在上面代码所缺失的部分。从路由开始。在程序的根目录中创建一个名为 controllers
的文件夹。然后在该文件夹内添加一个 usercontroller.ts
文件,内容如下:
import { Router } from 'https://deno.land/x/opine@0.12.0/mod.ts';const users = new Router();
// users routes
users.get('/me', (req, res) => {
res.render('users/me', { title: 'My Profile', user: res.app.locals.user });
});
export default users;
这是一个简单的路由文件。它从 Opine 获取路由,并创建一个新实例来挂起路由。然后有代码为 /me
添加路由以在 users/me
中渲染 HTML 视图。render()
调用还将标题和登录用户传递到页面。该页面将受到保护,以便始终有用户可以访问。
接下来,创建一些点击路由时能够显示的视图。在根文件夹中,添加一个 views
文件夹。在其中创建一个 shared
文件夹和一个 users
文件夹。在 shared
文件夹中,创建一个 header.html
和 footer.html
文件。在 users
文件夹中添加 me.html
文件。最后,在 views
文件夹本身中创建一个 index.html
文件。
这些是非常简单的方法,但是它演示了如何创建可被其他视图重用的视图。在 shared/header.html
文件中添加以下内容:
<!DOCTYPE html><html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><%= title %></title>
</head>
<body>
这将输出 HTML 页面的顶部,并将标题注入页面。接下来,将以下内容添加到 shared/footer.html
文件中:
</body></html>
现在你可以在 index.html
文件中使用这些局部变量:
<%- await include('views/shared/header.html', { title }); %><a href="https://segmentfault.com/users/me">My Profile</a>
<%- await include('views/shared/footer.html'); %>
这包括页脚和页眉部分的内容,并向个人资料页面添加了链接。 users/me.html
文件的内容是:
<%- await include('views/shared/header.html', { title }); %><h1>My Profile</h1>
<ul>
<% for(var p in user){ %>
<li><span><%= p %>: </span><%= user[p] %></li>
<% } %>
</ul>
<%- await include('views/shared/footer.html'); %>
同样,此页面包含页眉和页脚,并循环遍历 user
对象的属性。当然这不是一个漂亮的个人资料页面,但是它能够使你知道身份验证步骤是否全部有效。
用 Okta 添加身份验证
如果你还没有Okta帐户,可以在此处获得免费的开发人员帐户。登录 Okta 后进入仪表板。你需要创建一个 Okta 应用,以利用 Okta 作为项目的身份提供者。
单击菜单中的 Applications,然后单击 Add Application。这将带你进入应用程序向导。选择 Web 作为你的平台,然后单击 Next。下一页是 Application Settings 页面。为你的应用程序命名(我命名为 DenoExample)。将所有 URL 更改为使用端口 3000
而不是 8080
,然后将 Login Redirect URIs 更改为 http://localhost:3000/auth/callback
。最后,单击 Done 在 Okta 中创建应用程序。
进入新创建的应用程序页面后,确保你位于 General Settings 选项卡上并滚动到底部,直到看到 Client Credentials 部分。我们先暂时使用这些值,所以不要关闭这个窗口。
回到你的应用程序中,在程序的根目录中创建一个名为 .env
的新文件。该文件的内容将是:
issuer=https://{yourOktaOrgUrl}/oauth2/defaultclientId={yourClientID}
clientSecret={yourClientSecret}
redirectUrl=http://localhost:3000/auth/callback
state=SuPeR-lOnG-sEcReT
从 Okta 应用程序的 Client Credentials 部分复制客户端 ID 和客户端密钥。然后返回到信息中心,从菜单下方的右侧复制你的 Okta org URL。
现在你可以开始用 Okta 进行身份验证了。不幸的是你必须手动创建它。不过这是一个很棒的练习,可以帮助你了解 OAuth 和 OIDC 的工作方式。在程序的根文件夹中,创建一个名为 middleware
的新文件夹,并添加一个名为 authmiddleware.ts
的文件。然后添加以下内容:
import { config } from 'https://deno.land/x/dotenv/mod.ts';export const ensureAuthenticated = async (req:any, res:any, next:any) => {
const user = req.app.locals.user;
if(!user){
const reqUrl = req.originalUrl;
const {issuer, clientId, redirectUrl, state} = config();
const authUrl = `${issuer}/v1/authorize?client_id=${clientId}&response_type=code&scope=openid%20email%20profile&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}:${reqUrl}`;
res.location(authUrl).sendStatus(302);
}
next();
}
首先,导入一个用于读取 .env
文件的库 dotenv。然后实现 ensureAuthenticated()
中间件,该中间件将启动身份验证过程的第一步。它首先检用户是否登录。如果已登录,则它只调用 next()
,因为无事可做。
如果没有当前登录的用户,它将从 .env
文件构建一个由 Issuer,clientId,redirectUrl 和 state 属性组成的 URL。它调用发行者 URL 的 /v1/authorize
端点。然后重定向到该 URL。这是 Okta 托管的登录页面。有点像当你重定向到 Google 并用其作为身份提供者登录的机制。登录完成后将要调用的 URL 是 .env
文件中的 URL http://localhost:3000/auth/callback
。我还标记了用户重定向到 state
查询参数时要使用的原始 URL。一旦他们登录,这将会很容易把他们直接引导回去。
接下来,你将需要实现 auth/callback
路由来处理登录页面的结果,并交换将从 Okta 收到的授权代码。在 controllers
文件夹中创建一个名为 authcontroller.ts
的文件,其内容如下:
import { Router } from 'https://deno.land/x/opine@0.12.0/mod.ts';import { config } from "https://deno.land/x/dotenv/mod.ts";
const auth = new Router();
// users routes
auth.get('/callback', async (req, res) => {
const { issuer, clientId, clientSecret, redirectUrl, state } = config();
if (req.query.state.split(':')[0] !== state) {
res.send('State code does not match.').sendStatus(400);
}
const tokenUrl: string = `${issuer}/v1/token`;
const code: string = req.query.code;
const headers = new Headers();
headers.append('Accept', 'application/json');
headers.append('Authorization', `Basic ${btoa(clientId + ':' + clientSecret)}`);
headers.append('Content-Type', 'application/x-www-form-urlencoded');
const response = await fetch(tokenUrl, {
method: 'POST',
headers: headers,
body: `grant_type=authorization_code&redirect_uri=${encodeURIComponent(redirectUrl)}&code=${code}`
});
const data = await response.json();
if (response.status !== 200) {
res.send(data);
}
const user = parseJwt(data.id_token);
req.app.locals.user = user;
req.app.locals.isAuthenticated = true;
res.location(req.query.state.split(':')_[_1] || '/').sendStatus(302);
});
function parseJwt (token:string) {
const base64Url = token.split('.')_[_1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
};
export default auth;
实际上,这里发生的事比你想象的要少得多。首先从 Opine 引入 Router
,然后再次读取 .env
文件。接着他们像在 usercontroller.ts
文件中一样实例化路由器。接下来是解构 config 对象,能够更易于使用它的值。接下来,我检查了状态查询参数以确保其匹配。这有助于确保 Okta 是发送授权码的人。然后用 req.query.code
从查询字符串中提取授权码。
接下来是对 token 端点的调用。你将在 POST
请求中将授权码发送给 Okta,以交换 ID Token。因此,这里我为请求构建了一些标头。最重要的是 Authorization
标头,其值为 Basic {yourClientId}:{yourClientSecret}
,客户端 ID 和密码是 base64 编码的。然后,使用这些标头和带有 authorization_code
的 grant_type
(与以前相同的重定向 URL)的主体,以及带有我刚从 Okta 收到的授权代码的 Token 端点,对 Token 端点进行 POST
调用。
fetch()
调用返回一个用 then()
函数解析的 promise。我得到 response
对象的JSON值,为了确保调用成功,用下面的 parseJwt()
函数解析 id_token
值并将其粘贴到名为 user
的局部变量中。最后在重定向到身份验证之前,将用户发送到他们最初请求的 URL。
运行 Deno 程序
现在用以下命令从终端再次运行该程序:
deno run -A index.ts
一旦运行,你将能够单击主页上的配置文件链接,并将其重定向到 Okta 的托管登录页面。登录后,将会直接回到个人资料页面,你会看到 ID Token 的属性显示在列表中。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
以上是 构建一个带身份验证的 Deno 应用 的全部内容, 来源链接: utcz.com/a/38490.html