康心伴Logo
康心伴WellAlly
移动开发

使用 Supabase RLS 构建安全的 React Native 可穿戴应用

5 分钟阅读

使用 Supabase RLS 构建安全的 React Native 可穿戴应用

概述

健康数据包含敏感个人信息,必须严格保护。Supabase 的行级安全(Row-Level Security, RLS)提供数据库级别的访问控制,确保用户只能访问自己的数据。本文将介绍如何在 React Native 可穿戴应用中实现完整的安全方案。

安全架构设计

多层安全策略

code
┌─────────────────────────────────────────┐
│         React Native App                │
├─────────────────────────────────────────┤
│  - 认证状态管理                         │
│  - 数据验证                             │
│  - 敏感数据加密                         │
└─────────────────────────────────────────┘
                 ↓ HTTPS
┌─────────────────────────────────────────┐
│         Supabase Client                 │
├─────────────────────────────────────────┤
│  - API Key 验证                         │
│  - JWT Token 管理                       │
│  - 请求签名                             │
└─────────────────────────────────────────┘
                 ↓
┌─────────────────────────────────────────┐
│      Supabase PostgREST API             │
├─────────────────────────────────────────┤
│  - JWT 解析                             │
│  - 用户身份验证                         │
└─────────────────────────────────────────┘
                 ↓
┌─────────────────────────────────────────┐
│         PostgreSQL 数据库                │
├─────────────────────────────────────────┤
│  - 行级安全 (RLS) 策略                  │
│  - 数据加密存储                         │
│  - 审计日志                             │
└─────────────────────────────────────────┘
Code collapsed

Supabase 项目设置

1. 初始化项目

code
# 安装 Supabase CLI
npm install -g supabase

# 创建新项目
supabase init

# 启动本地开发环境
supabase start
Code collapsed

2. 数据库架构设计

code
-- migrations/001_create_health_data.sql

-- 启用必要的扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- 用户配置文件表
CREATE TABLE public.profiles (
    id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
    email TEXT UNIQUE NOT NULL,
    full_name TEXT,
    avatar_url TEXT,
    health_profile JSONB DEFAULT '{
        "birthDate": null,
        "gender": null,
        "height": null,
        "weight": null,
        "bloodType": null
    }'::jsonb,
    preferences JSONB DEFAULT '{
        "units": "metric",
        "notifications": true,
        "dataSharing": false
    }'::jsonb,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 健康指标表
CREATE TABLE public.health_metrics (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
    metric_type TEXT NOT NULL CHECK (metric_type IN (
        'heart_rate',
        'blood_pressure_systolic',
        'blood_pressure_diastolic',
        'steps',
        'calories',
        'sleep_duration',
        'weight',
        'spo2',
        'glucose'
    )),
    value NUMERIC NOT NULL,
    unit TEXT NOT NULL,
    device_id TEXT,
    recorded_at TIMESTAMPTZ NOT NULL,
    synced_at TIMESTAMPTZ DEFAULT NOW(),
    metadata JSONB DEFAULT '{}'::jsonb,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 设备表
CREATE TABLE public.devices (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
    device_name TEXT NOT NULL,
    device_type TEXT NOT NULL,
    manufacturer TEXT,
    model TEXT,
    serial_number TEXT UNIQUE,
    last_sync TIMESTAMPTZ,
    battery_level INTEGER CHECK (battery_level BETWEEN 0 AND 100),
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 数据共享表(用于医生/家人访问)
CREATE TABLE public.data_shares (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    owner_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
    shared_with UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    access_level TEXT NOT NULL CHECK (access_level IN ('read', 'write')),
    data_types TEXT[] NOT NULL,
    expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(owner_id, shared_with)
);

-- 审计日志表
CREATE TABLE public.audit_logs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
    action TEXT NOT NULL,
    table_name TEXT NOT NULL,
    record_id UUID,
    old_values JSONB,
    new_values JSONB,
    ip_address INET,
    user_agent TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 创建索引
CREATE INDEX idx_health_metrics_user_id ON public.health_metrics(user_id);
CREATE INDEX idx_health_metrics_type_date ON public.health_metrics(metric_type, recorded_at DESC);
CREATE INDEX idx_health_metrics_recorded_at ON public.health_metrics(recorded_at DESC);
CREATE INDEX idx_devices_user_id ON public.devices(user_id);
CREATE INDEX idx_data_shares_owner ON public.data_shares(owner_id);
CREATE INDEX idx_data_shares_recipient ON public.data_shares(shared_with);
CREATE INDEX idx_audit_logs_user_id ON public.audit_logs(user_id);
CREATE INDEX idx_audit_logs_created_at ON public.audit_logs(created_at DESC);
Code collapsed

3. RLS 策略配置

code
-- migrations/002_configure_rls.sql

-- 启用 RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.health_metrics ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.devices ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.data_shares ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.audit_logs ENABLE ROW LEVEL SECURITY;

-- Profiles 策略
CREATE POLICY "Users can view own profile"
    ON public.profiles FOR SELECT
    USING (auth.uid() = id);

CREATE POLICY "Users can update own profile"
    ON public.profiles FOR UPDATE
    USING (auth.uid() = id)
    WITH CHECK (auth.uid() = id);

CREATE POLICY "Users can insert own profile"
    ON public.profiles FOR INSERT
    WITH CHECK (auth.uid() = id);

-- Health Metrics 策略
CREATE POLICY "Users can view own metrics"
    ON public.health_metrics FOR SELECT
    USING (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()));

CREATE POLICY "Users can insert own metrics"
    ON public.health_metrics FOR INSERT
    WITH CHECK (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()));

CREATE POLICY "Users can update own metrics"
    ON public.health_metrics FOR UPDATE
    USING (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()))
    WITH CHECK (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()));

CREATE POLICY "Users can delete own metrics"
    ON public.health_metrics FOR DELETE
    USING (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()));

-- 允许通过数据共享访问
CREATE POLICY "Users can view shared metrics"
    ON public.health_metrics FOR SELECT
    USING (
        id IN (
            SELECT unnest(shared_data_types::uuid[])
            FROM public.data_shares
            WHERE owner_id = health_metrics.user_id
            AND shared_with = auth.uid()
            AND access_level = 'read'
            AND (expires_at IS NULL OR expires_at > NOW())
        )
    );

-- Devices 策略
CREATE POLICY "Users can view own devices"
    ON public.devices FOR SELECT
    USING (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()));

CREATE POLICY "Users can manage own devices"
    ON public.devices FOR ALL
    USING (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()))
    WITH CHECK (user_id = (SELECT id FROM public.profiles WHERE id = auth.uid()));

-- Data Shares 策略
CREATE POLICY "Owners can manage shares"
    ON public.data_shares FOR ALL
    USING (owner_id = (SELECT id FROM public.profiles WHERE id = auth.uid()))
    WITH CHECK (owner_id = (SELECT id FROM public.profiles WHERE id = auth.uid()));

CREATE POLICY "Recipients can view shares"
    ON public.data_shares FOR SELECT
    USING (shared_with = auth.uid());

-- Audit Logs 策略
CREATE POLICY "Users can view own audit logs"
    ON public.audit_logs FOR SELECT
    USING (user_id = auth.uid());

CREATE POLICY "System can insert audit logs"
    ON public.audit_logs FOR INSERT
    WITH CHECK (true);

-- 创建自动审计触发器
CREATE OR REPLACE FUNCTION public.log_audit_changes()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO public.audit_logs (
        user_id,
        action,
        table_name,
        record_id,
        old_values,
        new_values
    ) VALUES (
        auth.uid(),
        TG_OP,
        TG_TABLE_NAME,
        CASE WHEN TG_OP = 'DELETE' THEN OLD.id ELSE NEW.id END,
        CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN row_to_json(OLD) END,
        CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) END
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 为健康指标表创建触发器
CREATE TRIGGER audit_health_metrics
    AFTER INSERT OR UPDATE OR DELETE ON public.health_metrics
    FOR EACH ROW EXECUTE FUNCTION public.log_audit_changes();
Code collapsed

React Native 集成

1. 安装依赖

code
npm install @supabase/supabase-js
npm install react-native-url-polyfill
Code collapsed

2. Supabase 客户端配置

code
// lib/supabase/client.ts

import 'react-native-url-polyfill/auto';
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SecureStore } from 'expo-secure-store';

// 从环境变量获取
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;

// 自定义存储适配器(使用 SecureStore 存储敏感数据)
const customStorageAdapter = {
  getItem: (key: string) => {
    return SecureStore.getItemAsync(key);
  },
  setItem: (key: string, value: string) => {
    return SecureStore.setItemAsync(key, value);
  },
  removeItem: (key: string) => {
    return SecureStore.deleteItemAsync(key);
  },
};

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: customStorageAdapter,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
  db: {
    schema: 'public',
  },
  global: {
    headers: {
      'X-Client-Info': 'wearable-health-app',
    },
  },
});

// 类型定义
export interface Database {
  public: {
    Tables: {
      profiles: {
        Row: {
          id: string;
          email: string;
          full_name: string | null;
          avatar_url: string | null;
          health_profile: {
            birthDate: string | null;
            gender: string | null;
            height: number | null;
            weight: number | null;
            bloodType: string | null;
          };
          preferences: {
            units: 'metric' | 'imperial';
            notifications: boolean;
            dataSharing: boolean;
          };
          created_at: string;
          updated_at: string;
        };
        Insert: Omit<Database['public']['Tables']['profiles']['Row'], 'id' | 'created_at' | 'updated_at'>;
        Update: Partial<Database['public']['Tables']['profiles']['Insert']>;
      };
      health_metrics: {
        Row: {
          id: string;
          user_id: string;
          metric_type: 'heart_rate' | 'blood_pressure_systolic' | 'blood_pressure_diastolic' | 'steps' | 'calories' | 'sleep_duration' | 'weight' | 'spo2' | 'glucose';
          value: number;
          unit: string;
          device_id: string | null;
          recorded_at: string;
          synced_at: string;
          metadata: Record<string, any>;
          created_at: string;
        };
        Insert: Omit<Database['public']['Tables']['health_metrics']['Row'], 'id' | 'synced_at' | 'created_at'>;
        Update: Partial<Database['public']['Tables']['health_metrics']['Insert']>;
      };
      // ... 其他表
    };
  };
}
Code collapsed

3. 认证管理

code
// lib/supabase/auth.ts

import { supabase } from './client';
import type { AuthError, User } from '@supabase/supabase-js';

export interface SignUpData {
  email: string;
  password: string;
  fullName: string;
}

export interface SignInData {
  email: string;
  password: string;
}

export class AuthService {
  /**
   * 注册新用户
   */
  static async signUp(data: SignUpData): Promise<{ user: User | null; error: AuthError | null }> {
    const { data: authData, error } = await supabase.auth.signUp({
      email: data.email,
      password: data.password,
      options: {
        data: {
          full_name: data.fullName,
        },
      },
    });

    if (error) return { user: null, error };

    // 创建用户配置文件
    if (authData.user) {
      const { error: profileError } = await supabase
        .from('profiles')
        .insert({
          id: authData.user.id,
          email: data.email,
          full_name: data.fullName,
        });

      if (profileError) {
        console.error('Profile creation failed:', profileError);
        // 可以选择回滚认证
      }
    }

    return { user: authData.user, error: null };
  }

  /**
   * 用户登录
   */
  static async signIn(data: SignInData): Promise<{ user: User | null; error: AuthError | null }> {
    const { data: authData, error } = await supabase.auth.signInWithPassword({
      email: data.email,
      password: data.password,
    });

    return { user: authData.user, error };
  }

  /**
   * 用户登出
   */
  static async signOut(): Promise<{ error: AuthError | null }> {
    const { error } = await supabase.auth.signOut();
    return { error };
  }

  /**
   * 获取当前用户
   */
  static async getCurrentUser(): Promise<User | null> {
    const { data: { user } } = await supabase.auth.getUser();
    return user;
  }

  /**
   * 监听认证状态变化
   */
  static onAuthStateChange(callback: (event: string, session: any) => void) {
    return supabase.auth.onAuthStateChange(callback);
  }

  /**
   * 重置密码
   */
  static async resetPassword(email: string): Promise<{ error: AuthError | null }> {
    const { error } = await supabase.auth.resetPasswordForEmail(email, {
      redirectTo: 'wearableapp://reset-password',
    });
    return { error };
  }

  /**
   * 更新密码
   */
  static async updatePassword(newPassword: string): Promise<{ error: AuthError | null }> {
    const { error } = await supabase.auth.updateUser({
      password: newPassword,
    });
    return { error };
  }
}
Code collapsed

4. 健康数据服务

code
// lib/supabase/healthData.ts

import { supabase, type Database } from './client';
import { PostgrestError } from '@supabase/supabase-js';

export type HealthMetric = Database['public']['Tables']['health_metrics']['Row'];
export type HealthMetricInsert = Database['public']['Tables']['health_metrics']['Insert'];

export class HealthDataService {
  /**
   * 批量插入健康指标
   */
  static async batchInsertMetrics(
    metrics: Omit<HealthMetricInsert, 'user_id'>[]
  ): Promise<{ data: HealthMetric[] | null; error: PostgrestError | null }> {
    // 添加用户 ID
    const { data: { user } } = await supabase.auth.getUser();
    if (!user) return { data: null, error: { message: 'Not authenticated', details: '', hint: '', code: '401' } };

    const metricsWithUserId = metrics.map(metric => ({
      ...metric,
      user_id: user.id,
    }));

    const { data, error } = await supabase
      .from('health_metrics')
      .insert(metricsWithUserId)
      .select();

    return { data, error };
  }

  /**
   * 获取指定日期范围的健康指标
   */
  static async getMetricsByDateRange(
    metricType: HealthMetric['metric_type'],
    startDate: Date,
    endDate: Date
  ): Promise<{ data: HealthMetric[] | null; error: PostgrestError | null }> {
    const { data, error } = await supabase
      .from('health_metrics')
      .select('*')
      .eq('metric_type', metricType)
      .gte('recorded_at', startDate.toISOString())
      .lte('recorded_at', endDate.toISOString())
      .order('recorded_at', { ascending: true });

    return { data, error };
  }

  /**
   * 获取最新指标值
   */
  static async getLatestMetric(
    metricType: HealthMetric['metric_type']
  ): Promise<{ data: HealthMetric | null; error: PostgrestError | null }> {
    const { data, error } = await supabase
      .from('health_metrics')
      .select('*')
      .eq('metric_type', metricType)
      .order('recorded_at', { ascending: false })
      .limit(1)
      .single();

    return { data: data as HealthMetric | null, error };
  }

  /**
   * 获取每日汇总
   */
  static async getDailySummary(
    metricType: HealthMetric['metric_type'],
    days: number = 30
  ): Promise<{ data: Array<{ date: string; value: number }> | null; error: PostgrestError | null }> {
    const { data, error } = await supabase
      .rpc('get_daily_metric_summary', {
        p_metric_type: metricType,
        p_days: days,
      });

    return { data, error };
  }

  /**
   * 删除旧数据(数据保留策略)
   */
  static async deleteOldMetrics(daysToKeep: number = 365): Promise<{ error: PostgrestError | null }> {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);

    const { error } = await supabase
      .from('health_metrics')
      .delete()
      .lt('recorded_at', cutoffDate.toISOString());

    return { error };
  }

  /**
   * 同步本地数据到服务器
   */
  static async syncLocalData(localMetrics: Array<{
    metric_type: HealthMetric['metric_type'];
    value: number;
    unit: string;
    device_id?: string;
    recorded_at: string;
  }>) {
    // 批量上传
    const batchSize = 100;
    const results = [];

    for (let i = 0; i < localMetrics.length; i += batchSize) {
      const batch = localMetrics.slice(i, i + batchSize);
      const { data, error } = await this.batchInsertMetrics(batch);

      if (error) {
        console.error('Sync batch failed:', error);
      }

      results.push({ data, error });
    }

    return results;
  }
}
Code collapsed

5. 数据共享管理

code
// lib/supabase/dataSharing.ts

import { supabase } from './client';
import type { PostgrestError } from '@supabase/supabase-js';

export interface DataShare {
  id: string;
  owner_id: string;
  shared_with: string;
  access_level: 'read' | 'write';
  data_types: string[];
  expires_at: string | null;
  created_at: string;
}

export class DataSharingService {
  /**
   * 创建数据共享
   */
  static async createShare(
    recipientEmail: string,
    accessLevel: 'read' | 'write',
    dataTypes: string[],
    expiresAt?: Date
  ): Promise<{ data: DataShare | null; error: PostgrestError | null }> {
    // 获取接收者用户 ID
    const { data: { users } } = await supabase.auth.admin.listUsers();
    const recipient = users.find(u => u.email === recipientEmail);

    if (!recipient) {
      return { data: null, error: { message: 'User not found', details: '', hint: '', code: '404' } };
    }

    const { data, error } = await supabase
      .from('data_shares')
      .insert({
        shared_with: recipient.id,
        access_level: accessLevel,
        data_types: dataTypes,
        expires_at: expiresAt?.toISOString() || null,
      })
      .select()
      .single();

    return { data, error };
  }

  /**
   * 获取我创建的共享
   */
  static async getMyShares(): Promise<{ data: DataShare[] | null; error: PostgrestError | null }> {
    const { data, error } = await supabase
      .from('data_shares')
      .select('*, shared_with:auth.users(email, full_name)')
      .order('created_at', { ascending: false });

    return { data, error };
  }

  /**
   * 获取与我的共享
   */
  static async getSharedWithMe(): Promise<{ data: DataShare[] | null; error: PostgrestError | null }> {
    const { data, error } = await supabase
      .from('data_shares')
      .select('*, owner:profiles(email, full_name)')
      .eq('shared_with', supabase.auth.user()?.id)
      .order('created_at', { ascending: false });

    return { data, error };
  }

  /**
   * 撤销共享
   */
  static async revokeShare(shareId: string): Promise<{ error: PostgrestError | null }> {
    const { error } = await supabase
      .from('data_shares')
      .delete()
      .eq('id', shareId);

    return { error };
  }
}
Code collapsed

6. React Hooks 集成

code
// hooks/useSupabaseAuth.ts

import { useState, useEffect } from 'react';
import { AuthService } from '@/lib/supabase/auth';
import type { User } from '@supabase/supabase-js';

export function useSupabaseAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 获取初始会话
    AuthService.getCurrentUser().then(setUser);
    setLoading(false);

    // 监听认证状态变化
    const { data: { subscription } } = AuthService.onAuthStateChange((event, session) => {
      setUser(session?.user ?? null);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  const signIn = async (email: string, password: string) => {
    const { user, error } = await AuthService.signIn({ email, password });
    if (user) setUser(user);
    return { user, error };
  };

  const signUp = async (email: string, password: string, fullName: string) => {
    return AuthService.signUp({ email, password, fullName });
  };

  const signOut = async () => {
    const { error } = await AuthService.signOut();
    if (!error) setUser(null);
    return { error };
  };

  return {
    user,
    loading,
    signIn,
    signUp,
    signOut,
  };
}
Code collapsed

安全最佳实践

1. 敏感数据加密

code
// lib/encryption.ts

import CryptoJS from 'crypto-js';

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;

export class DataEncryption {
  /**
   * 加密敏感数据
   */
  static encrypt(data: string): string {
    return CryptoJS.AES.encrypt(data, ENCRYPTION_KEY).toString();
  }

  /**
   * 解密数据
   */
  static decrypt(encryptedData: string): string {
    const bytes = CryptoJS.AES.decrypt(encryptedData, ENCRYPTION_KEY);
    return bytes.toString(CryptoJS.enc.Utf8);
  }

  /**
   * 加密健康数据对象
   */
  static encryptHealthData(data: Record<string, any>): Record<string, any> {
    const sensitiveFields = ['glucose', 'blood_pressure', 'weight'];

    return Object.entries(data).reduce((acc, [key, value]) => {
      if (sensitiveFields.includes(key) && typeof value === 'string') {
        acc[key] = this.encrypt(value);
      } else {
        acc[key] = value;
      }
      return acc;
    }, {} as Record<string, any>);
  }
}
Code collapsed

2. API 密钥管理

code
// lib/env.ts

// 从 .env 文件加载环境变量
const ENV = {
  supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL,
  supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
  // 不要在客户端存储 service_role 密钥
};

// 验证必要的环境变量
if (!ENV.supabaseUrl || !ENV.supabaseAnonKey) {
  throw new Error('Missing required environment variables');
}

export default ENV;
Code collapsed

参考资料

#

文章标签

react-native
supabase
row-level-security
安全
健康数据

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅