微信小程序|在 Supabase 中集成微信登录
在微信小程序中使用 Supabase 作为后端时,无法直接使用 Supabase 内置的 OAuth 登录流程。我们可以通过 Supabase Edge Functions 实现微信原生登录,让用户可以在小程序中无缝注册和登录。
在 Supabase 完成完整登录流程,无需引入格外的后端 API(例如 Vercel)。
架构与原理概述
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 微信小程序 │ ───▶ │ Edge Function │ ───▶ │ Supabase │
│ wx.login() │ │ wechat-login │ │ Auth + DB │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
获取 code 换取 openid 创建/登录用户
(微信 API) 返回 JWT Token
调用链路:
- 小程序调用
wx.login()→ 获取 code - 小程序 POST 请求 → Edge Function
- Edge Function 调用微信 API → 获取 opened
- Edge Function 检查用户是否存在
- 如果不存在 → 调用
supabaseAdmin.auth.admin.createUser()创建用户 - 调用
signInWithPassword()获取 Session(Session 由 Supabase 提供) - 返回 Token 给小程序
登录有两种方式:
- wx.login() - 无需用户授权,可以静默登录
- xxx - 可以获取用户手机号,需要用户授权,需要企业身份。
Supabase 端的配置
了解 Edge Functions 的作用
微信登录需要调用外部 HTTP API(微信的 jscode2session),在高徒项目中,我们通过创建一个后端 API 来处理微信请求 —— 可以在 Vercel 中托管一个 API。
Supabase 提供了 Edge Functions 来接收外部请求,通过使用它,我们可以实现类似的功能,这样就可以简化架构,无需引入格外的 API。
- 接收小程序的请求
- 调用微信 API 获取
- 创建/登录 Supabase 用户
- 返回 Session
编写函数代码
编辑 supabase/functions/wechat-login/index.ts:
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
const WECHAT_API = "https://api.weixin.qq.com/sns/jscode2session";
// 生成确定性密码
async function generatePassword(openid: string, secret: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(openid + secret);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const WECHAT_APP_ID = Deno.env.get("WECHAT_APP_ID");
const WECHAT_APP_SECRET = Deno.env.get("WECHAT_APP_SECRET");
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
const PASSWORD_SECRET = Deno.env.get("PASSWORD_SECRET") ?? "your-secret";
const { code } = await req.json();
// 1. 调用微信 API 换取 openid
const wxUrl = `${WECHAT_API}?appid=${WECHAT_APP_ID}&secret=${WECHAT_APP_SECRET}&js_code=${code}&grant_type=authorization_code`;
const wxResponse = await fetch(wxUrl);
const wxData = await wxResponse.json();
if (wxData.errcode || !wxData.openid) {
return new Response(
JSON.stringify({ error: "WeChat login failed", details: wxData }),
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const openid = wxData.openid;
// 2. 创建 Supabase Admin 客户端
const supabaseAdmin = createClient(SUPABASE_URL!, SUPABASE_SERVICE_ROLE_KEY!, {
auth: { autoRefreshToken: false, persistSession: false },
});
// 3. 生成 Shadow Account 凭据
const emailPrefix = openid.replace(/[^a-zA-Z0-9]/g, '').substring(0, 20);
const email = `${emailPrefix}@example.com`;
const password = await generatePassword(openid, PASSWORD_SECRET);
// 4. 查找用户是否存在
const { data: existingUsers } = await supabaseAdmin.auth.admin.listUsers();
const existingUser = existingUsers?.users?.find((u: any) =>
u.user_metadata?.openid === openid
);
let session;
if (existingUser) {
// 用户已存在,登录
const { data, error } = await supabaseAdmin.auth.signInWithPassword({
email: existingUser.email!,
password,
});
if (error) throw error;
session = data.session;
} else {
// 用户不存在,创建(使用 Admin API 跳过邮箱验证)
const { data: newUser, error: createError } = await supabaseAdmin.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: { openid, provider: "wechat" },
});
if (createError) throw createError;
// 创建后登录
const { data, error } = await supabaseAdmin.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
session = data.session;
}
return new Response(
JSON.stringify({
access_token: session!.access_token,
refresh_token: session!.refresh_token,
expires_in: session!.expires_in,
user: { id: session!.user.id, openid },
}),
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
} catch (error: any) {
return new Response(
JSON.stringify({ error: "Internal server error", details: error.message }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});
注意:需要使用 admin.createUser 而不是 signUp。
signUp 会验证邮箱域名是否真实存在,而 admin.createUser 可以跳过验证。我们使用虚拟邮箱,所以必须用 Admin API。
由于 Supabase Auth 必须使用邮箱+密码登录,我们采用以下方案:
- 用微信的
openid生成一个确定性的虚拟邮箱 - 用
openid+ 密钥的哈希值作为密码 - 这样同一用户每次登录都会匹配到同一账户
配置 AppID 和 AppSecret
在微信公众号管理端获取 AppID 和 AppSecret,

supabase secrets set WECHAT_APP_ID=your_app_id
supabase secrets set WECHAT_APP_SECRET=your_app_secret
部署函数
supabase functions deploy wechat-login --no-verify-jwt
--no-verify-jwt 允许未登录用户调用此函数(登录前用户还没有 Token)小程序端配置
小程序端调用
创建 services/auth.js:
const EDGE_FUNCTION_URL = 'https://your-project-ref.supabase.co/functions/v1/wechat-login';
const SUPABASE_KEY = 'your-anon-key';
export function loginWithWeChat() {
return new Promise((resolve, reject) => {
wx.login({
success: (loginRes) => {
wx.request({
url: EDGE_FUNCTION_URL,
method: 'POST',
header: {
'Content-Type': 'application/json',
'apikey': SUPABASE_KEY,
},
data: { code: loginRes.code },
success: (res) => {
if (res.statusCode === 200) {
// 存储 Token
wx.setStorageSync('access_token', res.data.access_token);
wx.setStorageSync('user', JSON.stringify(res.data.user));
resolve(res.data);
} else {
reject(new Error(res.data.error));
}
},
fail: reject,
});
},
fail: reject,
});
});
}
如何关联用户数据?
在 RLS 策略中使用 auth.uid() 即可:
CREATE POLICY "用户只能访问自己的收藏"
ON favorites FOR ALL
USING (auth.uid() = user_id);
如何获取用户的微信头像和昵称?
需要用户授权 wx.getUserProfile,获取后可以更新 user_metadata:
const { data } = await supabase.auth.updateUser({
data: { nickname, avatar_url }
});
Comments ()