// 每日前端夜话 第374篇
// 正文共:4400 字
// 预计阅读时间:10 分钟
Node.js 的创建者 Ryan Dahl 创建了一个用于设计 Web 应用程序的新框架。他回过头来,利用在最初编写 Node 时还不可用的新技术,纠正了事后发现的一些错误。这就是 Deno[1](发音为 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[2],它是 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');
然后,在终端下切换到程序文件夹中,并输入以下内容来运行这个非常基本的程序:
代码语言:javascript复制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[3],这是 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="/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><strong><%= p %>: </strong><%= user[p] %></li>
<% } %>
</ul>
<%- await include('views/shared/footer.html'); %>
同样,此页面包含页眉和页脚,并循环遍历 user
对象的属性。当然这不是一个漂亮的个人资料页面,但是它能够使你知道身份验证步骤是否全部有效。
用 Okta 添加身份验证
如果你还没有Okta帐户,可以在此处获得免费的开发人员帐户[4]。登录 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/default
clientId={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 email profile&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}:${reqUrl}`;
res.location(authUrl).sendStatus(302);
}
next();
}
首先,导入一个用于读取 .env
文件的库 dotenv[5]。然后实现 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 程序
现在用以下命令从终端再次运行该程序:
代码语言:javascript复制deno run -A index.ts
一旦运行,你将能够单击主页上的配置文件链接,并将其重定向到 Okta 的托管登录页面。登录后,将会直接回到个人资料页面,你会看到 ID Token 的属性显示在列表中。
作者:Lee Brandt 翻译:疯狂的技术宅 原文:https://scotch.io/tutorials/build-your-first-deno-app-with-authentication