You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求Remixjs集成Passportjs的实现示例,替代remix-auth

Remix.js 结合 Passport.js 实现 Discord 认证示例

先解决你用 remix-auth-discord 无法获取 guilds 的问题

你之前的代码里,guilds 并不在 profile 对象中——Discord 的用户信息接口不会返回服务器列表,需要用拿到的 accessToken 单独调用 /users/@me/guilds 接口获取。如果想继续用 remix-auth,可以修改回调函数:

async (accessToken) => {
  // 调用Discord API获取服务器列表
  const guildsRes = await fetch("https://discord.com/api/users/@me/guilds", {
    headers: { Authorization: `Bearer ${accessToken.accessToken}` }
  });
  const guilds = await guildsRes.json();

  const user = {
    id: accessToken.profile.id,
    username: accessToken.profile.__json.username,
    email: accessToken.profile.emails?.[0].value,
    guilds: guilds
  };

  return user; // 返回用户对象,authenticator会自动存入session
}

以下是 Remix.js 结合 Passport.js 的完整实现

1. 安装依赖

npm install passport passport-discord @types/passport @types/passport-discord

2. 配置 Session 服务

// services/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node";

export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "_session",
    sameSite: "lax",
    path: "/",
    httpOnly: true,
    secrets: ["你的加密密钥"],
    secure: process.env.NODE_ENV === "production",
    maxAge: 60 * 60 * 24 * 7, // 会话有效期1周
  },
});

export async function getSession(request: Request) {
  const cookie = request.headers.get("Cookie");
  return sessionStorage.getSession(cookie);
}

export async function commitSession(session: any) {
  return sessionStorage.commitSession(session);
}

export async function destroySession(session: any) {
  return sessionStorage.destroySession(session);
}

3. 配置 Passport Discord 策略

// auth/passport.server.ts
import passport from "passport";
import { Strategy as DiscordStrategy } from "passport-discord";
import type { Profile } from "passport-discord";

// 序列化用户到Session
passport.serializeUser((user, done) => {
  done(null, user);
});

// 从Session反序列化用户
passport.deserializeUser((user: any, done) => {
  done(null, user);
});

// 配置Discord认证策略
passport.use(
  new DiscordStrategy(
    {
      clientID: process.env.DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
      callbackURL: process.env.DISCORD_CALLBACK_URL!,
      scope: ["identify", "email", "guilds"],
    },
    async (accessToken, refreshToken, profile: Profile, done) => {
      // 获取用户加入的服务器列表
      const guildsRes = await fetch("https://discord.com/api/users/@me/guilds", {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
      const guilds = await guildsRes.json();

      // 构造需要存储的用户信息
      const user = {
        id: profile.id,
        username: profile.username,
        email: profile.emails?.[0].value,
        avatar: profile.photos?.[0].value,
        guilds: guilds,
        accessToken,
      };

      done(null, user);
    }
  )
);

export default passport;

4. 适配 Passport 到 Remix 请求模型

// auth/passport-adapter.server.ts
import passport from "./passport.server";
import { getSession, commitSession } from "../services/session.server";
import type { Request } from "@remix-run/node";

export async function passportAuthenticate(
  request: Request,
  strategy: string,
  options: any = {}
) {
  const session = await getSession(request);

  // 模拟Passport需要的Express请求对象
  const req: any = {
    headers: request.headers,
    query: new URL(request.url).searchParams,
    body: await request.formData(),
    session: session.data,
    login: (user: any, done: any) => {
      session.set("user", user);
      done(null);
    },
  };

  return new Promise((resolve, reject) => {
    passport.authenticate(strategy, options, (err: any, user: any) => {
      if (err) return reject(err);
      if (!user) return reject(new Error("认证失败"));

      req.login(user, async (loginErr: any) => {
        if (loginErr) return reject(loginErr);
        const cookie = await commitSession(session);
        resolve({ user, cookie });
      });
    })(req, {}, (next: any) => next());
  });
}

5. 路由实现

登录页(routes/auth.tsx)
import { Form } from "@remix-run/react";

export default function AuthPage() {
  return (
    <div className="flex items-center justify-center h-screen">
      <Form action="/auth/discord" method="post">
        <button
          type="submit"
          className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
        >
          用Discord登录
        </button>
      </Form>
    </div>
  );
}
发起Discord认证(routes/auth/discord.tsx)
import type { ActionFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import passport from "~/auth/passport.server";
import { getSession } from "~/services/session.server";

export const action: ActionFunction = async ({ request }) => {
  const session = await getSession(request);
  const req: any = {
    headers: request.headers,
    session: session.data,
  };

  return new Promise((resolve) => {
    passport.authenticate("discord")(req, {}, (result: any) => {
      resolve(redirect(result.headers.location));
    });
  });
};
认证回调(routes/auth/discord/callback.tsx)
import type { LoaderFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { passportAuthenticate } from "~/auth/passport-adapter.server";

export const loader: LoaderFunction = async ({ request }) => {
  try {
    const { cookie } = await passportAuthenticate(request, "discord");
    return redirect("/dashboard", {
      headers: { "Set-Cookie": cookie },
    });
  } catch (err) {
    console.error("认证失败:", err);
    return redirect("/auth");
  }
};
仪表盘(routes/dashboard.tsx)
import type { LoaderFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getSession } from "~/services/session.server";

export const loader: LoaderFunction = async ({ request }) => {
  const session = await getSession(request);
  const user = session.get("user");

  if (!user) return redirect("/auth");
  return json({ user });
};

export default function DashboardPage() {
  const { user } = useLoaderData<typeof loader>();

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">仪表盘</h1>
      <div className="mb-4">
        <p>欢迎回来,{user.username}!</p>
        <p>邮箱:{user.email}</p>
      </div>
      <h2 className="text-xl font-semibold mb-2">你加入的服务器:</h2>
      <ul className="list-disc pl-6">
        {user.guilds.map((guild: any) => (
          <li key={guild.id}>{guild.name}</li>
        ))}
      </ul>
    </div>
  );
}

6. 环境变量配置

在项目根目录创建 .env 文件:

DISCORD_CLIENT_ID=你的Discord客户端ID
DISCORD_CLIENT_SECRET=你的Discord客户端密钥
DISCORD_CALLBACK_URL=http://localhost:3000/auth/discord/callback

然后在 remix.config.js 中暴露这些变量:

module.exports = {
  // ...其他配置
  serverEnvironmentVariables: [
    "DISCORD_CLIENT_ID",
    "DISCORD_CLIENT_SECRET",
    "DISCORD_CALLBACK_URL",
  ],
};

内容的提问来源于stack exchange,提问作者Xolo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 12:50:21