跳到主要内容
Setting up auth with Fresh

Fresh 身份验证设置方法

身份验证和会话管理是构建现代 Web 应用程序最基本方面之一。这对于隐藏高级内容或创建仅限管理员访问的部分(如仪表板)是必要的。

Fresh 是一个边缘原生 Web 框架,它通过服务器端渲染和岛屿架构拥抱渐进增强,同时针对延迟和性能进行优化。因此,Fresh 应用程序往往能够获得更高的 Lighthouse 分数,并且可以在互联网带宽较低的地区正常运行。

这是一个将身份验证添加到您的 Fresh 应用程序的简单指南。请按照以下步骤操作,或在此处查看源代码

创建一个新的 Fresh 应用程序

首先,让我们创建一个新的 Fresh 应用程序。

$ deno run -A -r https://fresh.deno.dev my-auth-app

为了保持简单,让我们删除一些东西

$ rm -rf islands/Counter.tsx routes/api/joke.ts routes/\[name\].tsx

更新 index.tsx

首先,让我们更新我们的 import_map.json 以包含 std lib

{
  "imports": {
    "$fresh/": "https://deno.land/x/[email protected]/",
    "preact": "https://esm.sh/[email protected]",
    "preact/": "https://esm.sh/[email protected]/",
    "preact-render-to-string": "https://esm.sh/*[email protected]",
    "@preact/signals": "https://esm.sh/*@preact/[email protected]",
    "@preact/signals-core": "https://esm.sh/*@preact/[email protected]",
    "twind": "https://esm.sh/[email protected]",
    "twind/": "https://esm.sh/[email protected]/",
    "std/": "https://deno.land/[email protected]/"
  }
}

接下来,让我们更新 /routes/index.tsx 以显示您的登录状态。

我们将使用 cookies(借助 std/cookie 中的 getCookies)来检查用户的状态。

import type { Handlers, PageProps } from "$fresh/server.ts";
import { getCookies } from "std/http/cookie.ts";

interface Data {
  isAllowed: boolean;
}

export const handler: Handlers = {
  GET(req, ctx) {
    const cookies = getCookies(req.headers);
    return ctx.render!({ isAllowed: cookies.auth === "bar" });
  },
};

export default function Home({ data }: PageProps<Data>) {
  return (
    <div>
      <div>
        You currently {data.isAllowed ? "are" : "are not"} logged in.
      </div>
    </div>
  );
}

自定义处理程序只是检查 cookies 并设置 isAllowed。我们的默认组件将根据 isAllowed 的值显示不同的状态。目前,由于我们尚未添加任何登录或设置 cookie 的逻辑,因此它显示

Initial login screen

接下来,让我们创建一个名为 <Login> 的组件作为登录表单。

创建 <Login> 组件

我们可以在 index.tsx 中直接创建这个组件

function Login() {
  return (
    <form method="post" action="/api/login">
      <input type="text" name="username" />
      <input type="password" name="password" />
      <button type="submit">Submit</button>
    </form>
  );
}

此组件将 POST usernamepassword/api/login,我们稍后将定义它。请注意,该表单使用 multipart/form-data(而不是 json),它尽可能依赖于原生浏览器功能,并最大限度地减少对任何客户端 JavaScript 的需求。

接下来,让我们在 /api/login 端点创建实际的身份验证逻辑。

添加 loginlogout 路由

/routes/api/ 下,让我们创建 login.ts

$ touch /routes/api/login.ts

此端点的所有身份验证逻辑都将包含在自定义处理函数中。

为简单起见,我们的用户名和密码将硬编码为 “deno” 和 “land”。(在大多数生产环境中,您将使用身份验证策略、来自持久数据存储的令牌等。)

当向 /api/login 发出 POST 请求时,自定义处理函数将执行以下操作

  • req 请求参数中提取 usernamepassword
  • 对照我们硬编码的用户名和密码进行检查
  • auth cookie 设置为 bar(在生产环境中,这应该是每个会话的唯一值),maxAge 为 120(2 分钟后过期)
  • 并返回适当的 HTTP 响应 (HTTP 303 强制方法返回到 GET,防止奇怪的浏览器历史记录行为)

这是代码

import { Handlers } from "$fresh/server.ts";
import { setCookie } from "std/http/cookie.ts";

export const handler: Handlers = {
  async POST(req) {
    const url = new URL(req.url);
    const form = await req.formData();
    if (form.get("username") === "deno" && form.get("password") === "land") {
      const headers = new Headers();
      setCookie(headers, {
        name: "auth",
        value: "bar", // this should be a unique value for each session
        maxAge: 120,
        sameSite: "Lax", // this is important to prevent CSRF attacks
        domain: url.hostname,
        path: "/",
        secure: true,
      });

      headers.set("location", "/");
      return new Response(null, {
        status: 303, // "See Other"
        headers,
      });
    } else {
      return new Response(null, {
        status: 403,
      });
    }
  },
};

让我们也创建一个用于注销的端点:/routes/logout.ts

$ touch routes/logout.ts

注销逻辑将删除登录时设置的 cookie,并将用户重定向到根页面

import { Handlers } from "$fresh/server.ts";
import { deleteCookie } from "std/http/cookie.ts";

export const handler: Handlers = {
  GET(req) {
    const url = new URL(req.url);
    const headers = new Headers(req.headers);
    deleteCookie(headers, "auth", { path: "/", domain: url.hostname });

    headers.set("location", "/");
    return new Response(null, {
      status: 302,
      headers,
    });
  },
};

现在,让我们通过返回到 /routes/index.tsx 并添加我们的登录和注销组件,将所有内容联系起来。

<Login> 和注销添加到 index

在我们的 routes/index.tsx 页面的 <Home> 组件中,让我们添加 <Login> 组件,以及一个注销按钮(它向 /logout 发送请求)

// routes/index.tsx

export default function Home({ data }: PageProps<Data>) {
  return (
    <div>
      <div>
        You currently {data.isAllowed ? "are" : "are not"} logged in.
      </div>
      {!data.isAllowed ? <Login /> : <a href="/logout">Logout</a>}
    </div>
  );
}

检查 localhost,现在我们有了可以工作的登录和注销按钮

Logging in and logging out

不错!

处理未登录用户

许多付费墙网站会在用户未登录时自动重定向用户。

我们可以通过在自定义处理程序中添加重定向逻辑来添加此功能

import type { Handlers } from "$fresh/server.ts";
import { getCookies } from "std/http/cookie.ts";

export default function Home() {
  return (
    <div>
      Here is some secret
    </div>
  );
}

export const handler: Handlers = {
  GET(req, ctx) {
    const cookies = getCookies(req.headers);
    if (cookies.auth === "bar") {
      return ctx.render!();
    } else {
      const url = new URL(req.url);
      url.pathname = "/";
      return Response.redirect(url);
    }
  },
};

Logging in or getting redirected

成功!

或者,如果您不想重定向用户,而只想在用户通过身份验证时显示秘密内容

import type { Handlers, PageProps } from "$fresh/server.ts";
import { getCookies } from "std/http/cookie.ts";

interface Data {
  isAllowed: boolean;
}

export default function Home({ data }: PageProps<Data>) {
  return (
    <div>
      {data.isAllowed ? "Here is some secret" : "You are not allowed here"}
    </div>
  );
}

export const handler: Handlers<Data> = {
  GET(req, ctx) {
    const cookies = getCookies(req.headers);

    return ctx.render!({ isAllowed: cookies.auth === "bar" });
  },
};

下一步是什么

这是一个将身份验证添加到您的 Fresh 应用程序的简要指南。我们计划在以后的文章中探讨许多使其更适合生产环境的方法

  • /routes/api/login.ts 中添加更强大的身份验证策略
  • 使用唯一值使 cookie 和会话管理更加安全
  • 使用持久性数据存储(如MongoDB 用于用户帐户或 Redis 用于会话管理)

我们希望这篇文章对您有所帮助!

遇到困难?在 Twitter我们的 Discord 上提出关于 Fresh 和 Deno 的问题。