# 用户认证 > 仅在用户明确要求登录功能时才实现认证。默认设计应支持匿名访问。 ## 认证方法 ### 用户名登录(默认方式) **需要登录功能时**,优先采用用户名登录,自动生成 `{username}@meoo.local` 虚拟邮箱以兼容 Supabase 认证系统。 ### 手机号登录 把手机号当作用户名(非短信验证码),同样自动生成 `{username}@meoo.local` 虚拟邮箱 + 密码登录。 ### 邮箱登录 仅在用户明确要求使用真实邮箱时才使用的标准邮箱/密码认证。 ## ❌ 不支持的登录方式 以下登录方式 **不支持**,如果用户要求请明确告知无法实现: - **手机验证码登录(短信 OTP)**:不支持短信验证码 - **第三方登录**:不支持微信、QQ、支付宝、GitHub 等第三方登录 - **扫码登录**:不支持二维码扫码登录 - **指纹/面部识别**:不支持生物识别登录 **如果用户要求以上登录方式,请明确说明 Meoo Cloud 目前仅支持用户名/密码登录。** ## 实现要点 ### 1. 存储完整会话对象 ```typescript const [session, setSession] = useState(null); ``` ### 2. 用户名认证示例 **实现登录注册时必须使用此方式**,除非用户明确要求使用真实邮箱: ```typescript // 注册 await supabase.auth.signUp({ email: `${username}@meoo.local`, password, options: { data: { username } } }); // 登录 await supabase.auth.signInWithPassword({ email: `${username}@meoo.local`, password, }); ``` ### 3. 认证状态监听 ```typescript // ✅ 正确:同步状态更新 + 在 effect cleanup 里 unsubscribe useEffect(() => { const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { setSession(session); setUser(session?.user ?? null); // 延迟任何 Supabase 调用 if (session?.user) { setTimeout(() => { fetchUserProfile(session.user.id); }, 0); } }); return () => subscription.unsubscribe(); }, []); // ❌ 错误:会导致死锁 supabase.auth.onAuthStateChange(async (event, session) => { const profile = await supabase.from('profiles').select()... // 死锁 }); ``` ## 用户数据表设计 ### 用户配置文件表 **重要**:只有当用户明确要求注册和登录功能时,才创建 `public.profiles` 表与 `auth.users` 表关联。 ```sql CREATE TABLE public.profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, username TEXT UNIQUE NOT NULL, avatar_url TEXT, created_at TIMESTAMP DEFAULT NOW() ); ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; -- 用户只能查看和修改自己的配置文件 CREATE POLICY users_select_own_profile ON public.profiles FOR SELECT USING (auth.uid() = id); CREATE POLICY users_update_own_profile ON public.profiles FOR UPDATE USING (auth.uid() = id) WITH CHECK (auth.uid() = id); ``` ### 自动创建配置文件触发器 设置触发器在用户注册时自动创建配置文件(依赖 `SECURITY DEFINER` 绕过 RLS 写入 profiles): ```sql -- 插入 public.profiles 的函数 CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ BEGIN INSERT INTO public.profiles (id, username, avatar_url) VALUES ( NEW.id, NEW.raw_user_meta_data ->> 'username', NEW.raw_user_meta_data ->> 'avatar_url' ); RETURN NEW; END; $$; -- 创建触发器(DROP IF EXISTS 保证重复执行幂等) DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); ``` > 不要忘记 `SECURITY DEFINER`! ### 业务表与用户关联 如果需要在业务表中引用用户信息,请引用 profiles 表的 `id`: ```sql CREATE TABLE public.user_posts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ALTER TABLE public.user_posts ENABLE ROW LEVEL SECURITY; -- 用户只能查看和操作自己的帖子 CREATE POLICY users_select_own_posts ON public.user_posts FOR SELECT USING (user_id = auth.uid()); CREATE POLICY users_insert_own_posts ON public.user_posts FOR INSERT WITH CHECK (user_id = auth.uid()); -- UPDATE 必须同时给 USING 和 WITH CHECK,否则用户能把别人的帖子改到自己名下后再修改 CREATE POLICY users_update_own_posts ON public.user_posts FOR UPDATE USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); ``` ### 角色和权限设计 角色**必须**存储在独立表中,不要在 profiles 表中存储角色: ```sql CREATE TYPE public.app_role AS ENUM ('admin', 'moderator', 'user'); CREATE TABLE public.user_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, role app_role NOT NULL, UNIQUE (user_id, role) ); ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY; -- 用户能读到自己的角色 CREATE POLICY users_select_own_roles ON public.user_roles FOR SELECT USING (user_id = auth.uid()); -- 辅助函数检查用户角色 CREATE OR REPLACE FUNCTION public.has_role(_user_id UUID, _role app_role) RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS ( SELECT 1 FROM public.user_roles WHERE user_id = _user_id AND role = _role ) $$; ``` ## 创建测试用户 ```sql INSERT INTO auth.users ( instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at, confirmation_token, recovery_token, email_change_token_new, email_change, raw_app_meta_data, raw_user_meta_data, is_super_admin ) VALUES ( '00000000-0000-0000-0000-000000000000', gen_random_uuid(), 'authenticated', 'authenticated', 'testuser@meoo.local', crypt('password123', gen_salt('bf')), NOW(), NOW(), NOW(), '', '', '', '', '{"provider":"email","providers":["email"]}'::jsonb, '{"username":"testuser"}'::jsonb, false ); ``` 必需字段:`instance_id`、`::jsonb` 类型转换、空字符串令牌(非 NULL)