ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Epic Stack 的 GitHub 第三方登录集成:基于 remix-auth 的 Connection 多 Provider 认证架构

Epic Stack 的 GitHub 第三方登录集成:基于 remix-auth 的 Connection 多 Provider 认证架构 Epic Stack 的 GitHub 第三方登录集成基于 remix-auth 的 Connection 多 Provider 认证架构【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack导读本文基于 Epic Stack 仓库中的架构决策文档 docs/decisions/030-github-auth.md完整讲解该项目如何以Connection连接数据模型为骨架、以remix-auth为认证引擎把 GitHub OAuth2 作为内置第三方登录实现并设计出可随时替换为任意 OAuth2 / OpenID Connect 提供方的可扩展架构。读完本文你将掌握为什么选择 Connection 模型而非硬编码 GitHub 用户、GitHub OAuth2 回调的完整状态机、无密码用户的注册/登录/解绑约束以及如何在未配置 GitHub 凭据时让应用依然正常运行。决策背景为什么需要连接而不是绑定许多应用都需要接入第三方身份提供方Identity Provider。Epic Stack 在设计之初就明确了一个目标支持连接connections作为内置能力而不是只为某一个平台写死一套流程。理由很直接不同用户群体依赖不同的提供方Google、GitHub、微软、企业自有 OIDC 等单靠一种登录方式无法覆盖所有场景只要把认证抽象成可插拔的策略就能低成本地在不同提供方之间切换符合 Epic Stack 的 Optimize for Adaptability面向可适应性优化 指导原则。在协议选型上决策文档给出了一个关键判断主流提供方普遍支持OAuth2越来越多提供方开始支持OpenID ConnectOIDC——它是 OAuth2 之上的一层标准用于以标准化方式获取用户信息GitHub 不支持 OpenID Connect但它是一个开发者向应用最常用的提供方之一。因此方案是采用remix-auth记录了一个边界划分账号密码登录流程由项目自己实现remix-auth 只保留给 GitHub 这类第三方认证使用这样既去掉了不必要的依赖代码又保留了 OAuth 策略的复用价值。核心数据模型Connection 表为了让一个用户能同时拥有多个第三方账号决策文档定义了一个全新的 Prisma 模型Connection。仓库中的实际实现位于 prisma/schema.prismamodel Connection { id String id default(cuid()) providerName String providerId String createdAt DateTime default(now()) updatedAt DateTime updatedAt user User relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String unique([providerName, providerId]) }与决策文档中的最初草案相比实际落地有一个值得注意的简化草案中的unique([providerId, userId])复合唯一约束在最终 schema 中被移除仅保留了unique([providerName, providerId])。这一改动意味着providerName providerId组合唯一从数据库层面保证同一个 GitHub 账号不会被重复绑定到两个 Epic Stack 用户这正是回调流程中各种已被占用分支判断的依据一个用户仍可通过userId外键关联多条 Connection实现一用户多提供方onDelete: Cascade保证删除用户时其全部连接记录随之清理避免孤儿数据。对应地User模型通过connections Connection[]建立反向关系并保留了可选的password关联密码与连接相互独立为纯第三方注册、无密码用户留出了空间。迁移 SQL 可在 prisma/migrations/20250221233640_init/migration.sql 中查看。Provider 抽象一个接口可插拔策略AuthProvider 接口第三方认证的核心抽象定义在 app/utils/providers/provider.ts它规定每个提供方必须实现三件事export interface AuthProvider { getAuthStrategy(): StrategyProviderUser, any | null handleMockAction(request: Request): Promisevoid resolveConnectionData( providerId: string, options?: { timings?: Timings }, ): Promise{ displayName: string link?: string | null } }getAuthStrategy()返回 remix-auth 的Strategy用于发起 OAuth 流程当环境变量缺失时返回null表示该提供方当前不可用handleMockAction()本地开发与测试时的模拟登录入口resolveConnectionData()给定providerId解析出该第三方账号的展示名与主页链接用于我的连接页面渲染。统一返回值类型ProviderUserid、email、username?、name?、imageUrl?保证了无论底层是 GitHub 还是未来接入的 Google上层逻辑拿到的都是同构的用户数据。Provider 注册表app/utils/connections.server.ts 维护了一个提供方注册表所有策略在这里集中装配export const providers: RecordProviderName, AuthProvider { github: new GitHubProvider(), }类型ProviderName由 app/utils/connections.tsx 中的 zod 枚举派生export const GITHUB_PROVIDER_NAME github export const providerNames [GITHUB_PROVIDER_NAME] as const export const ProviderNameSchema z.enum(providerNames) export const providerLabels: RecordProviderName, string { [GITHUB_PROVIDER_NAME]: GitHub, }新增一个提供方只需要三步实现AuthProvider、在providers注册表中加入实例、把名字加进providerNames枚举同时补上providerLabels/providerIcons等映射。这就是决策文档所说的让替换提供方变得容易的代码落地。GitHubProvider 实现OAuth2 策略与用户资料获取GitHub 的具体实现位于 app/utils/providers/github.server.ts。它通过环境变量控制可用性export class GitHubProvider implements AuthProvider { getAuthStrategy() { if ( !process.env.GITHUB_CLIENT_ID || !process.env.GITHUB_CLIENT_SECRET || !process.env.GITHUB_REDIRECT_URI ) { console.log( GitHub OAuth strategy not available because environment variables are not set, ) return null } return new GitHubStrategy({...}, async ({ tokens }) { ... }) } }需要配置的三个环境变量环境变量作用GITHUB_CLIENT_ID在 GitHub OAuth App 设置页面创建应用后获得的 Client IDGITHUB_CLIENT_SECRET同一应用的 Client SecretGITHUB_REDIRECT_URIOAuth 回调地址对应/auth/github/callback由于remix-auth-github不再在单次调用中同时返回用户与邮箱策略的回调函数需要分别调用两个 GitHub API并用 zod schema 校验响应GET https://api.github.com/user获取login用户名、id、name、avatar_urlGET https://api.github.com/user/emails获取邮箱列表从中选出primary: true的邮箱找不到则抛错Email not found。两次请求都携带Authorization: Bearer accessToken与X-GitHub-Api-Version: 2022-11-28请求头随后归一化为ProviderUser返回return { id: user.id, email, name: user.name, username: user.login, imageUrl: user.avatar_url, }resolveConnectionData 的缓存策略连接列表页需要为每个 Connection 展示第三方账号的显示名与链接GitHubProvider 通过cachified封装了对GET https://api.github.com/user/:providerId的请求app/utils/providers/github.server.tsconst result await cachified({ key: connection-data:github:${providerId}, cache, timings, ttl: 1000 * 60, // 新鲜期 1 分钟 swr: 1000 * 60 * 60 * 24 * 7, // 过期后最多再容忍 7 天旧数据 async getFreshValue(context) { ... }, checkValue: GitHubUserParseResult, })ttl: 60s、swr: 7 天的组合意味着正常读取几乎不会真正打到 GitHub API若解析失败则把metadata.ttl置 0 立即失效重试。解析失败时优雅降级为displayName: Unknown、link: null。登录与回调一次 OAuth 的全流程发起登录/auth/githubapp/routes/_auth/auth.$provider/index.ts 是发起登录的路由。GET直接重定向到/loginPOST则按ProviderNameSchema校验params.provider后调用authenticator.authenticate(providerName, request)由 remix-auth 引导用户跳转到 GitHub 授权页。跳转前会先把redirectTo来自表单隐藏字段或 Referrer 路由写入专门的 cookie供回调结束后恢复跳转。authenticator的装配在 app/utils/auth.server.tsexport const authenticator new AuthenticatorProviderUser() for (const [providerName, provider] of Object.entries(providers)) { const strategy provider.getAuthStrategy() if (strategy) { authenticator.use(strategy, providerName) } }注意只有当getAuthStrategy()返回非空即环境变量已配置时策略才会注册。这从机制上保证了未配置 GitHub 凭据时应用不会启动失败。回调状态机/auth/github/callback回调处理是整套设计中最复杂的部分位于 app/routes/_auth/auth.$provider/callback.ts。由于该 loader 会写入数据库它首先调用ensurePrimary()确保运行在主实例上避免写入只读副本随后用authenticator.authenticate换取 GitHub 返回的 profile并把成功/失败包装为带判别联合discriminated union的结果。认证失败直接redirectWithToast回/login并提示 Auth Failed。认证成功后根据当前登录状态与Connection 是否已存在回调进入一个完整的状态机用户当前状态Connection 状态处理分支已登录已存在且属于当前用户提示 Already Connected跳转连接页已登录已存在但属于他人提示该 GitHub 账号已绑定到其他账号跳转连接页已登录不存在创建 Connection绑定跳转连接页并提示 Connected未登录已存在直接创建新会话登录未登录不存在但邮箱匹配到现有用户为该用户创建 Connection 并创建会话邮箱匹配自动绑定未登录不存在且邮箱无匹配走新用户 onboarding流程makeSession是会话创建的统一出口调用handleNewSession前先写入prisma.session过期时间由getSessionExpirationDate()计算会话默认 30 天见 app/utils/auth.server.ts。新用户 onboarding对首次使用 GitHub 登录的新用户回调会创建一次 verify session写入onboardingEmailSessionKeyGitHub 提供的邮箱prefilledProfileKey预填资料用户名经normalizeUsername将非[a-zA-Z0-9_]字符替换为_并转小写、邮箱经normalizeEmail转小写规范化providerIdKeyGitHub 用户 id。然后跳转到/onboarding/github?redirectTo...。用户在该页完成资料确认后通过signupWithConnectionapp/utils/auth.server.ts一次性创建用户、Connection 与头像从imageUrl下载头像并上传到对象存储实现无密码注册。连接管理 UI绑定、解绑与最后一个连接保护app/routes/settings/profile/connections.tsx 实现了连接管理页对应决策文档中的适当的回调 URL 处理器和 UI。loader读取当前用户全部 Connection逐个调用resolveConnectionData解析展示名与链接同时通过userCanDeleteConnections计算是否允许删除绑定页面底部渲染ProviderConnectionFormapp/utils/connections.tsxPOST 到/auth/github走完整 OAuth 流程删除action 校验intent delete-connection后执行prisma.connection.delete。删除保护逻辑对应决策文档防止用户删除全部连接直到创建密码实现为async function userCanDeleteConnections(userId: string) { const user await prisma.user.findUnique({ select: { password: { select: { userId: true } }, _count: { select: { connections: true } }, }, where: { id: userId }, }) // user can delete their connections if they have a password if (user?.password) return true // users have to have more than one remaining connection to delete one return Boolean(user?._count.connections user?._count.connections 1) }即有密码的用户可以随意解绑无密码用户必须保留至少一个连接否则前端不渲染删除按钮后端 action 也会用 invariant 拦截。无密码用户仍可通过设置 → 创建密码/settings/profile/password_.create补设密码来解锁。无 GitHub 配置也能运行Mock 与降级设计决策文档的最后一个 consequence 是应用在未配置 GitHub 登录的情况下也必须正常启动。仓库通过三层机制落实策略按需注册getAuthStrategy()在环境变量缺失时返回nullauthenticator.use不会注册该策略登录页只展示账号密码方式Mock 流程GitHubProvider.handleMockActionapp/utils/providers/github.server.ts在GITHUB_CLIENT_ID以MOCK_开头或NODE_ENV test时生效直接以预设 code 构造回调请求重定向到/auth/github/callback。code 默认取常量MOCK_CODE_GITHUBMOCK_CODE_GITHUB_KODY见 app/utils/providers/constants.tse2e 测试可通过x-mock-code-github请求头注入自定义 codeMock 网络层测试环境下 tests/mocks/github.ts 会拦截 GitHub API 请求返回伪造数据配合MOCK_CODE_GITHUB完成从点击 GitHub 按钮到进入 onboarding的全链路模拟对应的端到端测试见 tests/e2e/onboarding.test.ts。这完整呼应了 Epic Stack 的 Minimize Setup Friction最小化配置摩擦 指导原则开发者在拿到项目后不需要先去 GitHub 注册 OAuth App 就能跑通登录链路。决策的后续影响与测试保障决策文档将这套架构引入后带来的主要后果在仓库中均能找到对应实现与测试无密码用户成为一等公民signupWithConnection允许不创建Password记录登录页的密码校验verifyUserPassword对无密码用户返回null只能通过连接登录回调状态机是受保护的契约上述 6 种分支在回调代码中逐一处理任何对流程的调整都可能破坏其中一种状态因此需要持续测试护航。相关测试分布在 app/routes/_auth/auth.$provider/callback.test.ts单元层与 tests/e2e/onboarding.test.ts端到端层可替换性providers注册表 ProviderNameSchema枚举的设计使社区或团队能够按需加入新的 OAuth2/OIDC 提供方同时保持上层登录、回调、连接管理逻辑零改动。总结Epic Stack 的 GitHub 认证并不是一段写死的 OAuth 代码而是一套完整的多提供方架构数据层Connection模型以unique([providerName, providerId])保证第三方账号全局唯一抽象层AuthProvider接口 providers注册表让添加/替换提供方成为低成本的增量修改流程层回调状态机覆盖登录、绑定、邮箱匹配、onboarding全部用户状态并在 UI 侧提供连接管理页面降级层环境变量缺失时优雅降级、Mock 机制保证本地开发与 CI 全链路可跑。无论你是想直接使用这套 GitHub 登录还是打算扩展自己的 OAuth2/OIDC 提供方都可以以 app/utils/providers/github.server.ts 为模板、以 app/routes/_auth/auth.$provider/callback.ts 为状态机参考在 Epic Stack 的框架内快速落地。【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表