Build Your First Deno App with Authentication

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.htmlfooter.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帐户,可以在此处获得免费的开发人员帐户。登录 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%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_codegrant_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 的属性显示在列表中。