什么是营销型的网站做论坛网站4g空间够不够用
什么是营销型的网站,做论坛网站4g空间够不够用,手机网站设计公司优选亿企邦,南京外贸网站建设公司排名可插拔认证架构#xff1a;Supabase Auth 与「一键切换 NextAuth」的抽象层 本文是「不爽有解」技术博客连载的第五篇#xff0c;介绍为何要做认证抽象、统一类型与 AuthProvider 接口、Supabase 实现与切换方式、服务端与客户端的协作#xff0c;以及匿名身份#xff08;指…可插拔认证架构Supabase Auth 与「一键切换 NextAuth」的抽象层本文是「不爽有解」技术博客连载的第五篇介绍为何要做认证抽象、统一类型与 AuthProvider 接口、Supabase 实现与切换方式、服务端与客户端的协作以及匿名身份指纹在投票与限流中的用法。不爽有解https://bushuangyoujie.cn — 基于真实痛点的独立开发者工具发现与推荐平台。一、为何要做认证抽象产品需要同时支持匿名与登录两种形态未登录可浏览、投票、搜索登录后可投稿痛点/工具、评论、个人中心。若把「当前用户」和「登录/登出」直接写在 Supabase Auth 的调用里将来若要迁到 NextAuth、Clerk或本地/CI 里用 Mock 做单测或 Storybook就要改遍所有业务代码。认证抽象的目标是业务只依赖「当前用户、登录、登出、会话」等统一接口不关心底层是 Supabase 还是 NextAuth换 Provider 时只需实现同一套接口并切换注入业务层几乎不动。此外匿名身份如浏览器指纹需要和登录用户一起纳入「统一身份」投票防刷、限流、以及「提交者可见自己的待审核内容」等都依赖「能区分是谁」的标识。抽象层里用UserIdentityauthenticated | anonymous id/identifier统一表达API 用getUserIdentity(request)拿到「登录用户或匿名指纹」再决定是否允许写、以及如何限流。二、统一类型与 AuthProvider 接口2.1 通用类型lib/auth/types.ts所有与「用户、会话、登录选项」相关的类型都收口在 types 里不依赖具体 ProviderSupabase/NextAuth 等便于各实现做映射如 Supabase User → AuthUser。// lib/auth/types.ts节选exportinterfaceAuthUser{id:string;email?:string;emailVerified?:boolean;name?:string;avatarUrl?:string;metadata?:Recordstring,any;}exportinterfaceAuthSession{accessToken:string;refreshToken?:string;expiresAt?:number;user:AuthUser;}exportinterfaceSignInOptions{email:string;password:string;}exportinterfaceSignUpOptions{email:string;password:string;metadata?:Recordstring,any;}exportinterfaceOAuthOptions{provider:github|google|apple|string;redirectTo?:string;}/** 统一身份登录用户或匿名指纹 */exportinterfaceUserIdentity{type:authenticated|anonymous;id:string;// user_id 或 fingerprintidentifier:string;// email 或 fingerprint}UserIdentity用于 API 与限流同一套逻辑里先看「是否登录」是则用user.id否则用匿名 id指纹便于投票去重、限流 key 的组成。2.2 AuthProvider 接口lib/auth/provider.ts所有认证实现必须实现AuthProvider这样业务代码只依赖「获取当前用户、会话、登录、登出、监听状态变更」等不依赖具体 SDK。// lib/auth/provider.ts节选exportinterfaceAuthProvider{getCurrentUser():PromiseAuthUser|null;getSession():PromiseAuthSession|null;signIn(options:SignInOptions):PromiseAuthSession;signUp(options:SignUpOptions):PromiseAuthSession|null;signInWithOAuth(options:OAuthOptions):Promise{url:string};signOut():Promisevoid;onAuthStateChange(callback:AuthStateChangeCallback):()void;resetPassword(email:string):Promisevoid;resendSignupConfirmation(email:string):Promisevoid;updatePassword(newPassword:string):Promisevoid;verifyEmail(token:string):Promisevoid;}业务侧通过auth.getCurrentUser()、auth.signIn()等调用见下节这些方法内部委托给当前注入的 Provider因此换 Provider 只需换注入不改业务调用方。三、Supabase 实现与切换方式3.1 SupabaseAuthProviderlib/auth/supabase-provider.tsSupabase 实现类内部持有一个 Supabase 客户端浏览器端用createBrowserClient把 Supabase 的 User/Session 映射成统一的AuthUser/AuthSession并实现接口里所有方法。// 映射 Supabase User → AuthUserfunctionmapSupabaseUserToAuthUser(supabaseUser:any):AuthUser|null{if(!supabaseUser)returnnull;return{id:supabaseUser.id,email:supabaseUser.email,emailVerified:supabaseUser.email_confirmed_at!null,name:supabaseUser.user_metadata?.name||supabaseUser.user_metadata?.full_name,avatarUrl:supabaseUser.user_metadata?.avatar_url,metadata:supabaseUser.user_metadata||{},};}exportclassSupabaseAuthProviderimplementsAuthProvider{privatesupabase:SupabaseClient;constructor(supabaseClient?:SupabaseClient){this.supabasesupabaseClient??createBrowserClient(supabaseUrl,supabaseAnonKey);}asyncgetCurrentUser():PromiseAuthUser|null{const{data:{user}}awaitthis.supabase.auth.getUser();returnmapSupabaseUserToAuthUser(user);}asyncsignIn(options:SignInOptions):PromiseAuthSession{const{data,error}awaitthis.supabase.auth.signInWithPassword({email:options.email,password:options.password,});if(error)thrownewError(error.message);// ... 映射 session 并返回}onAuthStateChange(callback:AuthStateChangeCallback):()void{const{data}this.supabase.auth.onAuthStateChange((event,session){constmappedEvent/* SIGNED_IN | SIGNED_OUT | ... */;callback(mappedEvent,session?mapSupabaseSessionToAuthSession(session,session.user):null);});returndata.subscription.unsubscribe;}// ... signUp、signInWithOAuth、signOut、resetPassword 等}要点所有对 Supabase Auth 的调用都封装在 Provider 内对外只暴露AuthUser/AuthSession业务不接触supabase.auth。3.2 默认注入与切换lib/auth/index.ts默认使用 Supabase 实现通过setAuthProvider(provider)可切换为 Mock 或 NextAuth 实现如单测、Storybook 或未来迁移。// lib/auth/index.tsimport{SupabaseAuthProvider}from./supabase-provider;importtype{AuthProvider}from./provider;letauthProvider:AuthProvidernewSupabaseAuthProvider();exportfunctionsetAuthProvider(provider:AuthProvider):void{authProviderprovider;}exportfunctiongetAuthProvider():AuthProvider{returnauthProvider;}exportconstauth{getCurrentUser:()authProvider.getCurrentUser(),getSession:()authProvider.getSession(),signIn:(options)authProvider.signIn(options),signUp:(options)authProvider.signUp(options),signInWithOAuth:(options)authProvider.signInWithOAuth(options),signOut:()authProvider.signOut(),onAuthStateChange:(cb)authProvider.onAuthStateChange(cb),resetPassword:(email)authProvider.resetPassword(email),resendSignupConfirmation:(email)authProvider.resendSignupConfirmation(email),updatePassword:(p)authProvider.updatePassword(p),verifyEmail:(token)authProvider.verifyEmail(token),};业务代码统一用auth.getCurrentUser()、auth.signIn()等测试或 Storybook 里在入口处setAuthProvider(new MockAuthProvider())即可切到 Mock无需改页面组件。3.3 NextAuth 骨架lib/auth/nextauth-provider.ts项目中保留了一个 NextAuth 的骨架实现NextAuthAuthProvider各方法暂时throw new NotImplementedError(...)并注释说明「安装 next-auth、配置 route、实现各方法后在 index 里 setAuthProvider(new NextAuthAuthProvider()) 即可切换」。这样未来迁移时只需实现该类并切换注入业务对auth.*的调用保持不变。四、服务端与客户端的协作4.1 服务端getCurrentUser、requireAuth、getUserIdentity服务端不能直接用「浏览器里的 Supabase 客户端」读 session而是要在 API 或 Server Component 里从当前请求的 Cookie中解析出用户。项目里在lib/auth/server.ts中实现getCurrentUser()用supabase/ssr的createServerClient传入 Next 的cookies()再调用supabase.auth.getUser()把得到的 Supabase User 映射成AuthUser返回未登录返回 null。requireAuth()内部调 getCurrentUser()若为 null 则throw new Error(未登录请先登录)用于投稿、个人中心等必须登录的 API。getUserIdentity(request?)先尝试 getCurrentUser()若已登录则返回{ type: authenticated, id: user.id, identifier: user.email || user.id }否则从 request 的 headerx-fingerprint或 cookiefingerprint取匿名标识返回{ type: anonymous, id: fingerprint, identifier: fingerprint }。投票、限流、以及「提交者身份」都依赖这套统一身份。当前 server.ts 内部仍直接使用 Supabase SSR 与 Cookie而不是通过getAuthProvider().getCurrentUser()原因是服务端 getCurrentUser 需要「从 request 读 Cookie」而 AuthProvider 接口是面向客户端的无参方法。若未来切换 NextAuth可增加「服务端适配」例如在 server 里根据 NODE_ENV 或配置选择「从 Cookie 读 Supabase session」还是「从 Cookie 读 NextAuth session」或为 Provider 增加getCurrentUserFromRequest(request)的扩展接口。目前保持「客户端走 Provider、服务端走 server 工具函数」的划分已能满足「业务统一用 getCurrentUser/requireAuth/getUserIdentity、底层可逐步替换」的目标。4.2 超管与 requireSuperAdmin部分 API如后台审核需要「仅超管可访问」。在 server 里用环境变量SUPER_ADMIN_EMAILS逗号分隔邮箱维护超管列表提供isSuperAdmin(user)和requireSuperAdmin()后者先 requireAuth()再校验邮箱是否在列表中否则抛错。这样权限判断仍集中在 auth 层业务 API 只需调requireSuperAdmin()。4.3 客户端useAuth Hook客户端用useAuth()lib/auth/client.ts拿到当前用户、会话、loading、isAuthenticated。内部通过auth.getCurrentUser()、auth.getSession()和auth.onAuthStateChange()与当前注入的 Provider 通信因此换 Provider 后组件无需改代码。// 使用示例 const { user, loading, isAuthenticated } useAuth(); if (loading) return Spinner /; if (!isAuthenticated) return LoginPrompt /; return div欢迎, {user?.email}/div;服务端与客户端水合首屏若由服务端渲染服务端可先 getCurrentUser() 得到用户再通过 layout 或 props 把「是否登录」传给客户端客户端 useAuth 初始化时会再拉一次与服务端结果一致因为 Cookie 同源。若首屏不依赖「当前用户」的 HTML 内容也可仅客户端 useAuth避免服务端为每个请求都调一次 getCurrentUser。五、匿名身份指纹5.1 用途投票、限流需要「区分不同访客」同一用户或同一设备在时间窗口内只能投一票、或限制提交/投票频率。登录用户用user.id未登录用户用浏览器指纹作为匿名 id与 IP 一起组成限流 key并在投票表里存 fingerprint 做唯一约束如 problem_id fingerprint 24 小时内唯一。5.2 前端生成与存储lib/fingerprint.ts// lib/fingerprint.ts节选exportasyncfunctiongetFingerprint():Promisestring{if(typeofwindowundefined)returnserver-side;constnavnavigatorasNavigator{deviceMemory?:number};constfeatures[navigator.userAgent,navigator.language,screen.widthxscreen.height,newDate().getTimezoneOffset(),navigator.platform,navigator.hardwareConcurrency??0,nav.deviceMemory??0,].join(|);lethash0;for(leti0;ifeatures.length;i){hash(hash5)-hashfeatures.charCodeAt(i);hashhashhash;}conststorageKeybrowser_fingerprint;letfingerprintlocalStorage.getItem(storageKey);if(!fingerprint){fingerprintfp_${Math.abs(hash)}_${Date.now()};localStorage.setItem(storageKey,fingerprint);}returnfingerprint;}用 UA、语言、分辨率、时区、平台等组合成简单哈希再与时间戳拼成唯一串首次生成后写入 localStorage后续复用避免同一设备每次刷新都变。注意这只是「防刷用的匿名标识」不保证全局唯一、也不做高精度设备识别。5.3 请求中携带指纹投票、提交推荐等接口需要服务端知道「当前匿名用户是谁」。前端在请求 body 里带fingerprint或部分接口用 headerx-fingerprint例如投票时先getFingerprint()再fetch(..., { body: JSON.stringify({ fingerprint }) })。服务端在 API 里通过getUserIdentity(request)得到身份若已登录则 identity 为 authenticated投票记录可记 user_id若未登录则用 body/header 里的 fingerprint 或 identity.id从 cookie 回退作为匿名 id用于唯一约束与限流。5.4 限流 keyIP fingerprintlib/rate-limit.ts中getRateLimitIdentifier(request, bodyFingerprint)将x-forwarded-for或 x-real-ip与 body 里的 fingerprint 拼成 key例如ip:fingerprint再对「提交」「投票」分别做滑动窗口限流。这样同一 IP 下多设备仍有不同 fingerprint限流更细同时避免未带 fingerprint 时 key 过于粗糙。六、Mock Provider 在单测与 Storybook 中的用法MockAuthProvider实现 AuthProvider 接口用内存存储「当前用户」和「会话」并维护一个简单账号表如 testexample.com / password123。signIn 时校验邮箱密码通过则设置 currentUser/currentSession 并触发 onAuthStateChangegetCurrentUser/getSession 直接返回内存值。单测或 Storybook 入口处import{setAuthProvider}from/lib/auth;import{MockAuthProvider}from/lib/auth/mock-provider;setAuthProvider(newMockAuthProvider());之后所有依赖auth.*或useAuth()的组件与 API 测试都走 Mock无需真实 Supabase也可在 Mock 里预置「已登录」状态方便做权限相关用例。「不爽有解」未登录可投票、登录可投稿且未来若要换 Auth 只需换 Provider 并适配服务端读 session 的方式业务代码几乎不动。下一篇将写API 设计、校验与限流Zod、统一响应、ilike 转义与限流 key。