• 首页

  • 写作

  • 文章归档

  • 照片

  • 友情链接

  • 旅行

  • 读书

  • 日志

  • 随记

  • 人文历史

  • linux

  • 前端
b l o g
b l o g

admin

lzp

03月
16
前端 | react ui

基于 cloudflare worker hono github 的登录系统

发表于 2025-03-16 • 字数统计 13289 • 被 6 人看爆

基于 cloudflare hono github 的登录系统

技术栈:

cloudflare worker
D1数据库
hono框架
github auth

参考:
https://zenn.dev/seachimes/articles/fe52c6d2fd35f6#db%E6%93%8D%E4%BD%9C%E3%81%AE%E9%96%A2%E6%95%B0%E3%82%92%E5%AE%9F%E8%A3%85

本地项目地址:

/Users/qy/Documents/git_rep/cloudflare_project/2025-03-05-hono-github-login-db/hono-auth-test-app-D1

github remote repo:

https://github.com/qyzhizi/hono-auth-test-app-D1

另一个参考:

https://zenn.dev/yu7400ki/articles/58091688063734#4.-d1-%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9

/Users/qy/Documents/git_rep/cloudflare_project/2025-02-22-github-clone-authjs-cloudflare/Authjs-cloudflare

next.js 不适合部署到 cloudflare,感觉总是会遇到各种兼容性的问题, 所以我切换到 hono, 框架更简洁,使用更简单

使用 hono 可以使用 包 hono@latest , 创建一个 cloudflare 的工程:

pnpm create hono@latest hono-auth-test-app

过程中 选择 cloudflare-workers 框架
安装依赖:

 pnpm i

运行:

pnpm dev

实际上启动的是: wrangler dev

测试:

curl -X GET http://localhost:8787/

编辑:wrangler.jsonc 添加: nodejs_compat
在本文中,我们将包含依赖于 Node.js API 的包。
如果你在默认状态下使用它,你会生气,所以启用 nodejs_compat 标志。
具体原因参考:https://developers.cloudflare.com/workers/runtime-apis/nodejs/

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "hono-auth-test-app",
  "main": "src/index.ts",
  "compatibility_date": "2025-03-05",
  "compatibility_flags": [
    "nodejs_compat"
  ]
}

DB环境构筑

博文中使用的是 本地 docker 部署的 pg, 这里我是用的是 cloudflare 平台的 D1 数据库,其实也是 sqlite 数据库
在本地调试时也可以通过 wrangler 虚拟出 worker 运行时与D1 数据库
本地创建D1 数据库

pnpm add drizzle-orm
pnpm add -D drizzle-kit

创建 src/infrastructure/db/schema.ts

import {
  sqliteTable,
  integer,
  text,
} from "drizzle-orm/sqlite-core";
import { sql } from "drizzle-orm"; // 正确导入 sql


export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: integer("created_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
});

export const userAuths = sqliteTable("user_auths", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  userId: integer("user_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  providerType: integer("provider_type").notNull(),
  providerUserId: text("provider_user_id").notNull(),
  createdAt: integer("created_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
});

export const tables = {
  users,
  userAuths,
};

drizzle.config.ts 的設定

import { defineConfig } from "drizzle-kit";

export default defineConfig({
    out: "./drizzle/migrations",
    schema: "./src/infrastructure/db/schema.ts",
    dialect: "sqlite",
    driver: "d1-http",
});

本地创建数据库迁移:

pnpm drizzle-kit generate --config=drizzle-dev.config.ts
├── [  56]  README.md
├── [  96]  drizzle
│   └── [ 128]  migrations
│       ├── [ 734]  0000_awesome_wither.sql
│       └── [ 128]  meta
│           ├── [3.4K]  0000_snapshot.json
│           └── [ 204]  _journal.json
├── [ 206]  drizzle-dev.config.ts

配置 D1 数据库

"d1_databases": [
  {
    "binding": "DB",
    "database_name": "hono_db1",
    "database_id": "df0c8a98-2a17-4977-b9d9-34d795e90fea",
    "migrations_dir": "drizzle/migrations"
  }
]

binding: 绑定类型
database_name: 数据库名字
database_id 是远程 cloudflare D1 数据库的 id, 目前应该可以先不管
migrations_dir 是数据库迁移数据所在的目录

创建 D1 数据库:

npx wrangler d1 migrations apply hono_db1 --local

如果要创建 cloudflare 远程数据库可以使用:

npx wrangler d1 migrations apply hono_db1 --remote

hono_db1 是 数据库的名字

检查D1 数据库是否已经创建:

npx wrangler d1 execute hono_db1 --command="select * from user_auths;" --local

如果里面有数据,就是下面这个样子的,如果没有数据,那么会显示为空,但不会报错,目前是还没有数据的

 ⛅️ wrangler 3.112.0 (update available 3.114.0)
---------------------------------------------------------

🌀 Executing on local database hono_db1 (df0c8a98-2a17-4977-b9d9-34d795e90fea) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 1 command executed successfully.
┌────┬─────────┬───────────────┬──────────────────┬─────────────────────┬─────────────────────┐
│ id │ user_id │ provider_type │ provider_user_id │ created_at          │ updated_at          │
├────┼─────────┼───────────────┼──────────────────┼─────────────────────┼─────────────────────┤
│ 1  │ 1       │ 0             │ 25292861         │ 2025-03-08 09:45:42 │ 2025-03-08 09:45:42 │
└────┴─────────┴───────────────┴──────────────────┴─────────────────────┴─────────────────────┘

创建远程数据库,如果后面要部署到远程,需要这一步:

如果未登录 cloudflare,需要登录先

npx wrangler d1 migrations apply hono_db1 --remote

.dev.vars 用于处理本地环境中的 secret 信息

因此可以这样添加:

ACCESS_TOKEN_SECRET='access-token-secret'
REFRESH_TOKEN_SECRET='refresh-token-secret'
ACCESS_TOKEN_EXPIRY="300"
REFRESH_TOKEN_EXPIRY="604800"

GITHUB_ID=Ov23l******C
GITHUB_SECRET=5f1*****a

这里的 GITHUB_ID GITHUB_SECRET 后面会提到,是 github auth 认证所创建的应用信息
ACCESS_TOKEN_SECRET REFRESH_TOKEN_SECRET 是用于生成 jwt 所需的 密钥

可以使用下面的命令生成 32位的随机密码:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"      

创建文件:src/types/provider.ts

export const Provider = {
    GitHub: 0,
    Google: 1,
};

export type ProviderType = (typeof Provider)[keyof typeof Provider];

src/infrastructure/user.ts

这个文件是的 findOrCreateUser() 函数是按照认证成功时调用而实现的。
在中间件中调用
从表中通过电子邮件地址获取用户信息
用户信息存在且未关联认证信息时,向 user_auths 表添加认证信息并返回用户信息
用户信息存在且与认证信息关联时返回用户信息
上述情况不适用时,请创建新条目

import { tables, userAuths, users } from "./db/schema";
import { findFirstUserAuth } from "./userAuth";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import type { Context } from "hono";
import { ProviderType } from "../types/provider";

export interface User {
    id: number;
    name: string;
    email: string;
}

export const findOrCreateUser = async (
    c: Context,
    name: string,
    email: string,
    providerType: ProviderType,
    providerUserId: string,
): Promise<User> => {
    const db = drizzle(c.env.DB, { schema: tables });

    let user = await db.query.users.findFirst({
        where: (users, { eq }) => eq(users.email, email),
    });

    if (user) {
        const userAuth = await findFirstUserAuth(
            c,
            user.id,
            providerType,
            providerUserId,
        );
        if (!userAuth) {
            await db.insert(userAuths).values({
                userId: user.id,
                providerType,
                providerUserId,
            });
        }
        return user;
    }

    const newUser = await db.insert(users).values({ name, email }).returning();

    try {
        if (newUser.length > 0) {
          await db.insert(userAuths).values({
            userId: newUser[0].id,
            providerType,
            providerUserId,
          });
          return newUser[0];
        }
      } catch (error) {
        console.error("Failed to insert into userAuths, rolling back user insertion", error);
        await db.delete(users).where(eq(users.id, newUser[0].id));
      }
    
    throw new Error("User creation failed");
};

export const findManyUsers = async (c: Context): Promise<User[]> => {
    const db = drizzle(c.env.DB, { schema: tables });
    return await db.query.users.findMany();
};

src/infrastructure/userAuth.ts

主要是一个函数:findFirstUserAuth 用于查找第一个用户凭据

注意 const db = drizzle(c.env.DB, { schema: tables }); 这种数据库初始化方式,这里使用的是 drizzle ORM

import { drizzle } from "drizzle-orm/d1"; drizzle-orm

import { and, eq } from "drizzle-orm";
import { tables, userAuths } from "./db/schema";
import { drizzle } from "drizzle-orm/d1";
import type { Context } from "hono";
import { ProviderType } from "../types/provider";

export interface UserAuth {
	id: number;
	userId: number;
	providerType: ProviderType;
	providerUserId: string;
	createdAt: Date;
	updatedAt: Date;
}

export const findFirstUserAuth = async (
	c: Context,
    userId: number,
	providerType: ProviderType,
	providerUserId: string,
): Promise<UserAuth | undefined> => {
	const db = drizzle(c.env.DB, { schema: tables });
	const result = await db.query.userAuths.findFirst({
		where: and(
        	eq(userAuths.userId, userId),
			eq(userAuths.providerType, providerType),
			eq(userAuths.providerUserId, providerUserId),
		),
	});
	return result;
};

中间件实现

这里将执行令牌验证处理和 GitHub 认证的实现

实现令牌验证处理

实现验证访问令牌和刷新令牌的中间件,并保护所有endpoint。
环境变量设置
添加签名用的私钥和有效期到环境变量文件中。
.dev.vars

ACCESS_TOKEN_SECRET='access-token-secret'
REFRESH_TOKEN_SECRET='refresh-token-secret'
ACCESS_TOKEN_EXPIRY="300"
REFRESH_TOKEN_EXPIRY="604800"

以下图示进行实现

使用了 Hono 的助手来验证令牌、签名和设置 Cookie。

两者都很简单易用。
https://hono.dev/docs/helpers/jwt
https://hono.dev/docs/helpers/cookie

/auth/* 计划为每个提供商创建认证用端点,所以先跳过查看路径。

src/middleware/auth/index.ts

import type { Hono } from "hono";
import { getCookie, setCookie } from "hono/cookie";
import { createMiddleware } from "hono/factory";
import { sign, verify } from "hono/jwt";
import type { JWTPayload } from "hono/utils/jwt/types";
import { useGitHubAuth } from "./github";

export const configAuthMiddleware = (app: Hono) => {
    useGitHubAuth(app);
	app.use(authMiddleware); // エンドポイント全体に適用
};

export const authMiddleware = createMiddleware(async (c, next) => {
	if (c.req.path.startsWith("/auth/")) {
		return next();
	}

	let accessToken = getCookie(c, "access_token");
	if (!accessToken) {
		return c.text("Unauthorized: no access token", 401);
	}

	let accessTokenPayload: JWTPayload;
	try {
		accessTokenPayload = await verify(accessToken, c.env.ACCESS_TOKEN_SECRET);
	} catch (err) {
		const refreshToken = getCookie(c, "refresh_token");
		if (!refreshToken) {
			return c.text("Unauthorized: no refresh token", 401);
		}
		let refreshTokenPayload: JWTPayload;
		try {
			refreshTokenPayload = await verify(refreshToken, c.env.REFRESH_TOKEN_SECRET);
		} catch (err2) {
			return c.text("Unauthorized: invalid refresh token", 401);
		}

		accessTokenPayload = {
			...refreshTokenPayload,
			exp:
				Math.floor(Date.now() / 1000) + parseInt(c.env.ACCESS_TOKEN_EXPIRY, 10),
		};
		accessToken = await sign(accessTokenPayload, c.env.ACCESS_TOKEN_SECRET);
	}

	setCookie(c, "access_token", accessToken, {
		path: "/",
		httpOnly: true,
		secure: true,
		sameSite: "Strict",
	});

	return next();
});

在 src 直下的 index.ts 中调用刚才定义的函数。

src/index.tsx

import { Hono } from "hono";
import type { FC } from 'hono/jsx'
import { configAuthMiddleware } from "./middleware/auth";

const Layout: FC = (props) => {
    return (
      <html>
        <body>{props.children}</body>
      </html>
    )
}

const Top: FC<{ messages: string[] }> = (props: {
    messages: string[]
  }) => { 
    return (
      <Layout>
        <h1>Hello Hono!</h1>
        <ul>
          {props.messages.map((message) => {
            return <li>{message}!!</li>
          })}
        </ul>
      </Layout>
    )
  }

const app = new Hono();

configAuthMiddleware(app); // 追加

// app.get("/", (c) => {
//     return c.text("Hello Hono!");
// });

app.get('/', (c) => {
  const messages = ['Good Morning', 'Good Evening', 'Good Night']
  return c.html(<Top messages={messages} />)
})

export default app;

启动服务器进行确认。

而不是返回 Hello Hono! ,而是返回 401 错误表示成功。

> curl -X GET http://localhost:8787/
Unauthorized: no access token

GitHub 认证的实现

以下流程进行实现。
GitHub 认证
认证成功后,使用认证信息进行用户确认或注册
返回令牌

OAuth 应用注册

在实现之前,请先进行 OAuth Apps 的注册。
打开 GitHub,从设置 > 开发者设置中添加 OAuth 应用。
本次为了在本地环境中进行验证,按照以下方式设置并添加。

如果可以添加的话,Client ID 和 Client secrets 会显示出来,然后将其添加到环境变量中。
.dev.vars

GITHUB_ID=your-github-client-id
GITHUB_SECRET=your-github-client-secret

GitHub 认证的实现将使用 Hono 的 oauth-providers 包。

https://github.com/honojs/middleware/tree/main/packages/oauth-providers

pnpm i @hono/oauth-providers

以下为 GitHub 认证用端点的实现。

当您访问 /auth/github 端点时,您会被重定向到 GitHub 身份验证页面。
认证成功后,将被重定向到回调 URL,因此可以使用认证信息进行用户搜索、注册和发行令牌。

src/middleware/auth/github.ts

import { Context, Hono } from "hono";
import { githubAuth } from "@hono/oauth-providers/github";
import { Provider } from "../../types/provider";
import { findOrCreateUser } from "../../infrastructure/user";
import { sign } from "hono/jwt";
import { setCookie } from "hono/cookie";

export const useGitHubAuth = (app: Hono) => {
	app.use("/auth/github", (c: Context, next) => {
		return githubAuth({
			client_id: c.env.GITHUB_ID,
			client_secret: c.env.GITHUB_SECRET,
			scope: ["read:user", "user", "user:email"],
			oauthApp: true,
		})(c, next);
	});

	app.get("/auth/github", async (c: Context) => {
		const userData = c.get("user-github");

		if (!userData) {
			return c.text("GitHub authentication failed", 400);
		}
		if (!userData.id || !userData.name || !userData.email) {
			return c.text("Required information could not be retrieved", 400);
		}

		const user = await findOrCreateUser(
			c,
			userData.name,
			userData.email,
			Provider.GitHub,
			userData.id.toString(),
		);

		const accessTokenPayload = {
			sub: user.id.toString(),
			name: user.name,
			exp:
				Math.floor(Date.now() / 1000) + parseInt(c.env.ACCESS_TOKEN_EXPIRY, 10),
		};
		const refreshTokenPayload = {
			sub: user.id.toString(),
			name: user.name,
			exp:
				Math.floor(Date.now() / 1000) +
				parseInt(c.env.REFRESH_TOKEN_EXPIRY, 10),
		};

		const accessToken = await sign(
			accessTokenPayload,
			c.env.ACCESS_TOKEN_SECRET,
		);
		const refreshToken = await sign(
			refreshTokenPayload,
			c.env.REFRESH_TOKEN_SECRET,
		);

		setCookie(c, "access_token", accessToken, {
			path: "/",
			httpOnly: true,
			secure: true,
			sameSite: "Strict",
		});
		setCookie(c, "refresh_token", refreshToken, {
			path: "/",
			httpOnly: true,
			secure: true,
			sameSite: "Strict",
		});

		return c.redirect("/");
	});
};

为使 GitHub 认证的端点可用,将之前实现的函数添加到 auth/index.ts 的 configAuthMiddleware() 函数中。

import { useGitHubAuth } from "./github";

export const configAuthMiddleware = (app: Hono) => {
	useGitHubAuth(app);
	// useGoogleAuth(app);
	app.use(authMiddleware);
};

动作确认

GitHub 认证用端点访问时会跳转到 GitHub 认证页面,因此允许。
允许后将被重定向到路由
确认 Cookie 中已设置令牌。
由于重定向时没有执行令牌验证,因此需要重新调用端点。(需调查..)

401 而不是显示 Hello Hono!🥳

获取用户信息

最后,我尝试获取数据库中注册的用户信息。
为 src/index.ts 添加新的端点。

import { findManyUsers } from "./infrastructure/user";

app.get("/users", async (c) => {
	const users = await findManyUsers(c);
	return c.json({ users: users });
});

调用/users 获取用户信息成功🙌

结束

最近想尝试的技术终于有机会接触到了。非常满意。
Hono 拥有丰富的第三方中间件和嵌入式中间件,还有简单易用的助手,感觉开发体验非常好😸

以上即为全部。
本篇文章如有任何参考价值,不胜荣幸。
感谢您阅读至此!

参考

https://zenn.dev/seachimes/articles/fe52c6d2fd35f6#%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E5%AE%9F%E8%A3%85

分享到:
cloudflare pages 实现 hono jsx dom github 登录认证
饮食之线粒体
  • 文章目录
  • 站点概览
admin

! lzp

hello

Github Twitter QQ Email Telegram RSS
看爆 Top5
  • 历史与人文 视频链接 189次看爆
  • 2022日志随笔 175次看爆
  • 我的青海湖骑行 164次看爆
  • 读书随笔 124次看爆
  • rs2 设置教程 97次看爆

站点已萌萌哒运行 00 天 00 小时 00 分 00 秒(●'◡'●)ノ♥

Copyright © 2025 admin

由 Halo 强力驱动 · Theme by Sagiri · 站点地图