Remix v2 + Cloudflare Pages 集成 Github 登录

Remix v2 + Cloudflare Pages 集成 Github 登录_第1张图片

Remix Auth 特性

  • 完整的服务器端身份验证
  • 完整的 TypeScript 支持
  • 基于策略的身份验证
  • 轻松处理成功和失败
  • 实施自定义策略
  • 支持持久会话

文章目录

  • Remix Auth 特性
  • 安装依赖
  • 封装服务
  • 登录及回调
  • 登出/注销
  • TypeScript 类型
  • FAQ

安装依赖

npm i --save remix-auth remix-auth-github

需要用到这两个包。然后创建 auth 相关的文件,参考以下结构:

├── app
│   ├── entry.client.tsx
│   ├── entry.server.tsx
│   ├── root.tsx
│   └── routes
│       ├── _index.tsx
│       ├── auth.github.callback.ts
│       ├── auth.github.ts
│       └── private.tsx
└── tsconfig.json

routes 目录中创建 auth.github.tsauth.github.callback.ts 两个核心文件。

封装服务

由于 Cloudflare Pages 无法使用 process.env 所以采用了一种很骚的操作来实现:

// auth.server.ts
import { createCookieSessionStorage } from '@remix-run/cloudflare';
import { Authenticator } from 'remix-auth';
import type { SessionStorage } from '@remix-run/cloudflare';
import { GitHubStrategy } from 'remix-auth-github';
import { z } from 'zod';
import type { Env } from '../env';

const UserSchema = z.object({
  username: z.string(),
  displayName: z.string(),
  email: z.string().email().nullable(),
  avatar: z.string().url(),
  githubId: z.string().min(1),
  isSponsor: z.boolean()
});

const SessionSchema = z.object({
  user: UserSchema.optional(),
  strategy: z.string().optional(),
  'oauth2:state': z.string().uuid().optional(),
  'auth:error': z.object({ message: z.string() }).optional()
});

export type User = z.infer<typeof UserSchema>;

export type Session = z.infer<typeof SessionSchema>;

export interface IAuthService {
  readonly authenticator: Authenticator<User>;
  readonly sessionStorage: TypedSessionStorage<typeof SessionSchema>;
}

export class AuthService implements IAuthService {
  #sessionStorage: SessionStorage<typeof SessionSchema>;
  #authenticator: Authenticator<User>;

  constructor(env: Env, hostname: string) {
    let sessionStorage = createCookieSessionStorage({
      cookie: {
        name: 'sid',
        httpOnly: true,
        secure: env.CF_PAGES === 'production',
        sameSite: 'lax',
        path: '/',
        secrets: [env.COOKIE_SESSION_SECRET]
      }
    });

    this.#sessionStorage = sessionStorage;
    this.#authenticator = new Authenticator<User>(this.#sessionStorage as unknown as SessionStorage, {
      throwOnError: true
    });

    let callbackURL = new URL(env.GITHUB_CALLBACK_URL);
    callbackURL.hostname = hostname;

    this.#authenticator.use(
      new GitHubStrategy(
        {
          clientID: env.GITHUB_ID,
          clientSecret: env.GITHUB_SECRET,
          callbackURL: callbackURL.toString()
        },
        async ({ profile }) => {
          return {
            displayName: profile._json.name,
            username: profile._json.login,
            email: profile._json.email ?? profile.emails?.at(0) ?? null,
            avatar: profile._json.avatar_url,
            githubId: profile._json.node_id
            // isSponsor: await gh.isSponsoringMe(profile._json.node_id)
          };
        }
      )
    );
  }

  get authenticator() {
    return this.#authenticator;
  }

  get sessionStorage() {
    return this.#sessionStorage;
  }
}

其中还用到了 zod 来定义类型,这个部分是可以忽略不用的。

如果感兴趣的话,可以访问 Zod 文档学习: https://zod.dev/

然后找到根目录下的 server.ts 进行改造,主要改造的方法为 getLoadContext 部分,在其中将 services 作为依赖进行注入:

import { logDevReady } from '@remix-run/cloudflare';
import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages';
import * as build from '@remix-run/dev/server-build';
import { AuthService } from '~/server/services/auth';
import { EnvSchema } from './env';

if (process.env.NODE_ENV === 'development') {
  logDevReady(build);
}

export const onRequest = createPagesFunctionHandler({
  build,
  getLoadContext: (ctx) => {
    const env = EnvSchema.parse(ctx.env);
    const { hostname } = new URL(ctx.request.url);

    const auth = new AuthService(env, hostname);
    const services: RemixServer.Services = {
      auth
    };
    return { env, services };
  },
  mode: build.mode
});

登录及回调

这部分可以参考 remix-auth 的文档: https://github.com/sergiodxa/remix-auth

// auth.github.ts
import type { ActionFunction } from '@remix-run/cloudflare';

export const action: ActionFunction = async ({ request, context }) => {
  return await context.services.auth.authenticator.authenticate('github', request, {
    successRedirect: '/private',
    failureRedirect: '/'
  });
};

如果想要用 Get 请求进行登录, action 改为 loader 即可。

// auth.github.callback.ts
import type { LoaderFunction } from '@remix-run/cloudflare';

export const loader: LoaderFunction = async ({ request, context }) => {
  return await context.services.auth.authenticator.authenticate('github', request, {
    successRedirect: '/private',
    failureRedirect: '/'
  });
};

这里通过 context 将服务传递进来,避免反复使用 env 环境变量进行初始化。

然后写一个路由测试登录结果:

import type { ActionFunction, LoaderFunction } from '@remix-run/cloudflare';
import { json } from '@remix-run/cloudflare';
import { Form, useLoaderData } from '@remix-run/react';

export const action: ActionFunction = async ({ request }) => {
  await auth.logout(request, { redirectTo: '/' });
};

export const loader: LoaderFunction = async ({ request, context }) => {
  const profile = await context.services.auth.authenticator.isAuthenticated(request, {
    failureRedirect: '/'
  });

  return json({ profile });
};

export default function Screen() {
  const { profile } = useLoaderData<typeof loader>();
  return (
    <>
      <Form method='post'>
        <button>Log Out</button>
      </Form>

      <hr />

      <pre>
        <code>{JSON.stringify(profile, null, 2)}</code>
      </pre>
    </>
  );
}

登出/注销

可以参考以下代码,新建一个路由实现:

export async function action({ request }: ActionArgs) {
  await authenticator.logout(request, { redirectTo: "/login" });
};

TypeScript 类型

如果需要通过类型提示的话,添加一个 .d.ts 文件,或者在 root.tsx 中添加类型声明:

import type { Env } from './env';
import type { IAuthService } from './services/auth';

declare global {
  namespace RemixServer {
    export interface Services {
      auth: IAuthService;
    }
  }
}

declare module '@remix-run/cloudflare' {
  interface AppLoadContext {
    env: Env;
    DB: D1Database;
    services: RemixServer.Services;
  }
}

参考这个, Env 是你自己所需要的环境变量的类型定义。

完成。完整的示例代码在: https://github.com/willin/remix-cloudflare-pages-demo/tree/c8c350ce954d14cdc68f1f9cd11cecea00600483

FAQ

注意:v2 版本之后,不可以使用 remix-utils。存在兼容性问题。

你可能感兴趣的:(github)