求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
相关产品推荐
相关产品推荐

