跳到主要内容
Deno 2 终于来了 🎉️
了解更多
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 库

{
  "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 以显示您的登录状态。

我们将使用 cookie(借助于来自 std/cookiegetCookies)来检查用户的状态。

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>
  );
}

自定义处理程序只是检查 cookie 并设置 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(在生产环境中,这应该是一个每个会话唯一的 value)并设置 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 的问题。