github-actions[bot] b6e2e526d9 sync: Add files via upload
重大版本更新 (A@029ab8fcd9c4f587ce7a2394252868172d8e5d79)
2026-06-04 13:57:36 +00:00

223 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 用户认证
> 仅在用户明确要求登录功能时才实现认证。默认设计应支持匿名访问。
## 认证方法
### 用户名登录(默认方式)
**需要登录功能时**,优先采用用户名登录,自动生成 `{username}@meoo.local` 虚拟邮箱以兼容 Supabase 认证系统。
### 手机号登录
把手机号当作用户名(非短信验证码),同样自动生成 `{username}@meoo.local` 虚拟邮箱 + 密码登录。
### 邮箱登录
仅在用户明确要求使用真实邮箱时才使用的标准邮箱/密码认证。
## ❌ 不支持的登录方式
以下登录方式 **不支持**,如果用户要求请明确告知无法实现:
- **手机验证码登录(短信 OTP**:不支持短信验证码
- **第三方登录**不支持微信、QQ、支付宝、GitHub 等第三方登录
- **扫码登录**:不支持二维码扫码登录
- **指纹/面部识别**:不支持生物识别登录
**如果用户要求以上登录方式,请明确说明 Meoo Cloud 目前仅支持用户名/密码登录。**
## 实现要点
### 1. 存储完整会话对象
```typescript
const [session, setSession] = useState<Session | null>(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