如何结合 payment processor、Firebase 与 Auth0 登录?
结论
建议按下面的方式划分职责:
- Auth0 负责身份认证:确认用户身份,并向客户端签发 token。
- 后端 API 负责授权和业务逻辑:验证 token、读写进度、创建支付订单。
- Firebase/Firestore 或其他数据库负责持久化:保存用户资料、任务进度和支付状态。
- payment processor 负责收款:支付平台的客户编号、订单编号与内部用户 ID 的对应关系保存在数据库中。
不要通过姓名或邮箱关联数据,也不要把 Auth0 token 当成用户数据库。可靠的身份标识通常是 token 中的 iss 和 sub,其中 sub 是用户在当前 Auth0 租户中的标识。
只要应用需要保存进度,数据库就不能省略。token 只能在一次 API 请求中证明用户身份,不能保存进度、订阅状态或付款记录。
Auth0 返回的数据分别有什么用途
Auth0 登录成功后,常见的 token 有两类:
- ID token:供客户端使用,包含当前登录用户的信息,例如姓名、头像和邮箱。
- Access token:发送给后端 API,证明客户端有权调用该 API。
客户端调用后端时,通常按下面的方式发送 Access token:
Authorization: Bearer eyJ...
后端必须验证:
- token 的签名;
iss,也就是签发者;aud,也就是目标 API;- token 是否过期;
- 所需的权限或 scope。
验证通过后,后端从声明中读取 sub。不要让客户端额外提交 userId,再根据这个 ID 访问数据。攻击者可能替换 ID,从而读取或修改其他用户的数据。
姓名、头像和邮箱可以用于展示,但不能作为数据库主键,因为这些信息可能被修改、重复或缺失。
建议的数据模型
系统可以生成一个不依赖认证平台的内部用户 ID:
users
id
auth_issuer
auth_subject
email
display_name
payment_customer_id
created_at
progress
id
user_id
task_id
completed_steps
updated_at
payments
id
user_id
provider
provider_customer_id
provider_payment_id
status
amount
currency
updated_at
users 表应对 (auth_issuer, auth_subject) 建立唯一约束。
用户第一次访问业务 API 时,后端执行一次“查找或创建”:
- 验证 Access token。
- 读取
iss和sub。 - 查找对应的内部用户。
- 如果用户不存在,则创建记录。
- 此后的进度和付款记录都关联这个内部用户 ID。
这种做法通常称为即时建档,不需要在用户完成注册时马上创建业务数据。
ASP.NET Core 中识别 Auth0 用户
下面是一个简化示例。具体配置名称可能因项目模板和 Auth0 SDK 版本而异,但验证原则相同。
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://YOUR_AUTH0_DOMAIN/";
options.Audience = "https://api.example.com";
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
受保护的接口可以从验证后的身份中读取声明:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/progress")]
[Authorize]
public class ProgressController : ControllerBase
{
private readonly IUserRepository _users;
private readonly IProgressRepository _progress;
public ProgressController(
IUserRepository users,
IProgressRepository progress)
{
_users = users;
_progress = progress;
}
[HttpPost]
public async Task<IActionResult> Save(
[FromBody] SaveProgressRequest request)
{
string? subject = User.FindFirstValue("sub")
?? User.FindFirstValue(ClaimTypes.NameIdentifier);
string? issuer = User.FindFirst("iss")?.Value;
if (string.IsNullOrWhiteSpace(subject) ||
string.IsNullOrWhiteSpace(issuer))
{
return Unauthorized();
}
var user = await _users.FindOrCreateAsync(
issuer,
subject,
User.FindFirstValue(ClaimTypes.Email));
await _progress.UpsertAsync(
user.Id,
request.TaskId,
request.CompletedSteps);
return NoContent();
}
}
public sealed record SaveProgressRequest(
string TaskId,
int CompletedSteps);
这里最重要的是,user.Id 由后端根据验证过的 token 确定,而不是直接采用客户端提交的用户编号。
Auth0 与 Firebase 应该如何组合
Firebase 不是一个单独的产品,需要区分其中的不同服务:
- Firebase Authentication:负责身份认证。
- Cloud Firestore / Realtime Database:负责数据存储。
- Firebase Admin SDK:供受信任的后端访问 Firebase 服务。
Auth0 token 未经配置或转换,不能直接当作 Firebase Authentication token 使用。常见的组合方式有两种。
方案一:Auth0 加后端 API,再由后端访问 Firestore
这种方式的职责比较清楚:
C# 客户端
-> 使用 Auth0 登录
-> 携带 Auth0 Access token 调用后端
-> 后端验证 token
-> 后端通过 Firebase Admin SDK 读写 Firestore
客户端不需要直接登录 Firebase,也不需要获取 Firebase token。数据库的安全边界集中在后端,支付逻辑也更容易统一处理。
方案二:用 Auth0 身份换取 Firebase custom token
如果客户端必须直接访问 Firestore,可以增加一个受保护的 token exchange 接口:
- 客户端携带 Auth0 Access token 调用后端。
- 后端验证 Auth0 token。
- 后端把 Auth0 用户映射到内部用户或 Firebase UID。
- 后端通过 Firebase Admin SDK 创建 Firebase custom token。
- 客户端使用 custom token 登录 Firebase。
- Firestore Security Rules 根据 Firebase UID 限制数据访问。
概念示例如下:
[Authorize]
[HttpPost("firebase-token")]
public async Task<IActionResult> CreateFirebaseToken()
{
string subject = GetRequiredSubject(User);
string issuer = GetRequiredIssuer(User);
var user = await _users.FindOrCreateAsync(issuer, subject);
string firebaseToken =
await FirebaseAdmin.Auth.FirebaseAuth.DefaultInstance
.CreateCustomTokenAsync(user.Id.ToString());
return Ok(new { token = firebaseToken });
}
不要把未经验证的 sub 或客户端提交的 UID 直接传给 Admin SDK。映射和签发必须在受信任的后端完成。
如果客户端不需要直接访问 Firestore,方案一一般更简单。
是否改用 Firebase Authentication
可以。Firebase Authentication 与 Auth0 多数情况下是替代关系,不需要让用户分别在两套系统中注册。
如果 Firebase Authentication 已经能满足登录方式、账号管理和平台支持需求,可以采用下面的流程:
Firebase Authentication
-> 客户端获得 Firebase ID token
-> 后端验证 Firebase ID token
-> 使用 Firebase UID 查找内部用户
-> 保存进度或发起支付
如果应用需要 Auth0 的企业身份源、复杂登录流程或授权能力,则可以继续使用 Auth0。选择哪一种主要取决于认证需求,与 payment processor 关系不大,因为支付平台通常不会要求使用特定的登录服务。
payment processor 如何关联正确用户
支付必须由后端发起,不能只依赖客户端提交的邮箱、金额或用户 ID。
建议采用下面的流程:
- 用户登录后,携带 Access token 调用后端。
- 后端验证 token,并取得内部用户 ID。
- 后端查询该用户的
payment_customer_id。 - 如果还没有支付客户记录,后端调用支付平台 API 创建客户,并保存 customer ID。
- 后端创建订单或结账会话。
- 把内部订单 ID 写入支付平台支持的 metadata 或 reference 字段。
- 支付平台通过 webhook 通知支付结果。
- 后端验证 webhook 签名,再通过支付平台的 payment/customer ID 查找本地记录。
- 后端更新订单、订阅或权限状态。
伪代码如下:
[Authorize]
[HttpPost("checkout")]
public async Task<IActionResult> CreateCheckout()
{
var user = await ResolveCurrentUserAsync(User);
if (user.PaymentCustomerId is null)
{
var customer = await _payments.CreateCustomerAsync(
email: user.Email,
reference: user.Id.ToString());
await _users.SetPaymentCustomerIdAsync(
user.Id,
customer.Id);
}
var order = await _orders.CreatePendingAsync(user.Id);
var checkout = await _payments.CreateCheckoutAsync(
customerId: user.PaymentCustomerId!,
metadata: new Dictionary<string, string>
{
["order_id"] = order.Id.ToString()
});
return Ok(new { checkoutUrl = checkout.Url });
}
这里的 API 只是抽象示例,实际方法取决于支付平台及其 SDK。
不要仅根据支付成功后的浏览器跳转把订单标记为已付款。用户可能关闭页面,跳转地址也可能被伪造。支付状态应主要以通过签名验证的 webhook 为准,同时需要做好幂等处理,避免重复处理同一个 webhook。
安全和实现注意事项
- 不要把 Auth0 Client Secret、Firebase 服务账号密钥或支付平台 Secret Key 放进 C# 桌面客户端。
- 不要用姓名作为主键。
- 邮箱可以作为联系信息保存,但不能作为唯一且永久的身份依据。
- 不要把 Access token 存入用户表并作为长期标识。token 会过期,也会轮换。
- 使用 HTTPS,并按照平台建议安全保存客户端 token。
- 更新进度时,应由后端从认证上下文中确定用户,防止水平越权。
- 支付金额和商品价格必须由后端计算,不能采用客户端提交的金额。
- webhook 应验证签名、记录事件 ID,并防止重复处理同一事件。
- 如果允许用户合并多个登录方式,需要单独设计账号关联流程,不能假定所有 provider 的
sub都相同。 - 如果只需要在本地保存单机进度,可以暂时采用本地存储。一旦需要跨设备同步、订阅或付款权益,就需要后端和持久化存储。
实际落地时,可以先保留 Auth0,建立一个 ASP.NET Core API,通过 iss + sub 创建内部用户。Firestore 和 payment processor 都由这个 API 访问。这样,认证、业务数据和支付彼此分离,同时又能通过内部用户 ID 稳定关联。
备注:内容仅供参考。
