fix(alipay): 适配 tobias 5.x 新 auth API,后端生成签名 authString
tobias 3.x+ 移除了顶层函数 aliPayAuth(appId, scope), 改为需要后端预签名的 Tobias().auth(authString)。 变更: - alipay.provider.ts: 新增 generateMobileAuthString(scope) 方法, 用 RSA2 私钥生成符合 Alipay SDK 格式的签名授权字符串 - auth.controller.ts: 新增 GET /auth/alipay/auth-string 接口 - pubspec.yaml: tobias ^3.0.0 → ^5.0.0 - auth_service.dart: 新增 getAlipayAuthString() 方法 - welcome_page.dart: 更新支付宝登录流程,先获取 authString 再调用 tobias Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a27baa1181
commit
828770add8
|
|
@ -86,10 +86,67 @@ export class AlipayProvider {
|
||||||
private readonly logger = new Logger('AlipayProvider');
|
private readonly logger = new Logger('AlipayProvider');
|
||||||
|
|
||||||
private readonly appId = process.env.ALIPAY_APP_ID;
|
private readonly appId = process.env.ALIPAY_APP_ID;
|
||||||
|
// PID(Partner ID):支付宝商户号,16位数字(2088xxxxxxxxxx),
|
||||||
|
// 在支付宝开放平台「账号信息」页面查看。
|
||||||
|
// 若未配置则降级使用 appId(部分沙箱环境可行,正式环境必须配置真实 PID)
|
||||||
|
private readonly pid = process.env.ALIPAY_PID || process.env.ALIPAY_APP_ID;
|
||||||
private readonly privateKey = process.env.ALIPAY_PRIVATE_KEY;
|
private readonly privateKey = process.env.ALIPAY_PRIVATE_KEY;
|
||||||
private readonly gateway =
|
private readonly gateway =
|
||||||
process.env.ALIPAY_GATEWAY || 'openapi.alipay.com';
|
process.env.ALIPAY_GATEWAY || 'openapi.alipay.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成移动端 App 授权字符串(供 tobias Flutter 包使用)
|
||||||
|
*
|
||||||
|
* tobias 3.x+ 的 `Tobias().auth(authString)` 需要传入一个由服务端签名的
|
||||||
|
* 授权请求字符串,而非直接传 appId+scope。这是 Alipay SDK 的安全升级要求:
|
||||||
|
* 将签名计算移到服务端,防止客户端泄露私钥。
|
||||||
|
*
|
||||||
|
* 生成的 authString 传给移动端后,由 Alipay SDK 解析并拉起支付宝 App,
|
||||||
|
* 用户授权后支付宝 App 回传 auth_code,再走后端 POST /auth/alipay 流程。
|
||||||
|
*
|
||||||
|
* 参数格式(字典序排列,URL 编码,最后附 sign):
|
||||||
|
* apiname=com.alipay.account.auth
|
||||||
|
* app_id=2021xxxxxxxx
|
||||||
|
* biz_type=openservice
|
||||||
|
* charset=UTF-8
|
||||||
|
* pid=2088xxxxxxxx ← 商户 PID(ALIPAY_PID 环境变量,非 app_id)
|
||||||
|
* product_id=APP_FAST_LOGIN
|
||||||
|
* scope=auth_user ← 或 kuaijie(静默授权)
|
||||||
|
* sign_type=RSA2
|
||||||
|
* timestamp=2024-01-15 10:30:00
|
||||||
|
* sign=BASE64SIGNATURE
|
||||||
|
*
|
||||||
|
* @param scope 'auth_user'(获取用户信息,需授权页)| 'kuaijie'(静默,仅 user_id)
|
||||||
|
*/
|
||||||
|
generateMobileAuthString(scope: string = 'auth_user'): string {
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace('T', ' ')
|
||||||
|
.substring(0, 19);
|
||||||
|
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
apiname: 'com.alipay.account.auth',
|
||||||
|
app_id: this.appId!,
|
||||||
|
biz_type: 'openservice',
|
||||||
|
charset: 'UTF-8',
|
||||||
|
pid: this.pid!,
|
||||||
|
product_id: 'APP_FAST_LOGIN',
|
||||||
|
scope,
|
||||||
|
sign_type: 'RSA2',
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 签名:与网关请求签名算法一致(字典序拼接 → RSA-SHA256 → Base64)
|
||||||
|
const sign = this.sign(params);
|
||||||
|
|
||||||
|
// 拼接最终 authString(字典序排列,值 URL 编码,末尾附 sign)
|
||||||
|
const sortedKeys = Object.keys(params).sort();
|
||||||
|
const encoded = sortedKeys
|
||||||
|
.map((k) => `${k}=${encodeURIComponent(params[k])}`)
|
||||||
|
.join('&');
|
||||||
|
return `${encoded}&sign=${encodeURIComponent(sign)}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 2: 用 auth_code 换取 access_token
|
* Step 2: 用 auth_code 换取 access_token
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
Body,
|
Body,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
|
@ -14,6 +16,7 @@ import { ThrottlerGuard } from '@nestjs/throttler';
|
||||||
import { AuthService } from '../../../application/services/auth.service';
|
import { AuthService } from '../../../application/services/auth.service';
|
||||||
import { WechatService } from '../../../application/services/wechat.service';
|
import { WechatService } from '../../../application/services/wechat.service';
|
||||||
import { AlipayService } from '../../../application/services/alipay.service';
|
import { AlipayService } from '../../../application/services/alipay.service';
|
||||||
|
import { AlipayProvider } from '../../../infrastructure/alipay/alipay.provider';
|
||||||
import { GoogleService } from '../../../application/services/google.service';
|
import { GoogleService } from '../../../application/services/google.service';
|
||||||
import { AppleService } from '../../../application/services/apple.service';
|
import { AppleService } from '../../../application/services/apple.service';
|
||||||
import { RegisterDto } from '../dto/register.dto';
|
import { RegisterDto } from '../dto/register.dto';
|
||||||
|
|
@ -42,6 +45,7 @@ export class AuthController {
|
||||||
private readonly alipayService: AlipayService,
|
private readonly alipayService: AlipayService,
|
||||||
private readonly googleService: GoogleService,
|
private readonly googleService: GoogleService,
|
||||||
private readonly appleService: AppleService,
|
private readonly appleService: AppleService,
|
||||||
|
private readonly alipayProvider: AlipayProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/* ── SMS 验证码 ── */
|
/* ── SMS 验证码 ── */
|
||||||
|
|
@ -273,6 +277,28 @@ export class AuthController {
|
||||||
|
|
||||||
/* ── 支付宝登录 / 注册 ── */
|
/* ── 支付宝登录 / 注册 ── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付宝移动端授权字符串(供 tobias 3.x+ 使用)
|
||||||
|
*
|
||||||
|
* tobias 3.x 后 `Tobias().auth(authString)` 需要由服务端提供已签名的 authString,
|
||||||
|
* 不再允许客户端直接传 appId+scope(安全升级,防止私钥泄露到客户端)。
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* Flutter → GET /auth/alipay/auth-string
|
||||||
|
* → Tobias().auth(authString) → 拉起支付宝 App
|
||||||
|
* → 用户授权 → 支付宝回传 auth_code
|
||||||
|
* → POST /auth/alipay (现有接口)
|
||||||
|
*/
|
||||||
|
@Get('alipay/auth-string')
|
||||||
|
@ApiOperation({ summary: '获取支付宝移动端签名授权字符串(tobias 3.x+)' })
|
||||||
|
@ApiResponse({ status: 200, description: '返回签名后的 authString' })
|
||||||
|
getAlipayAuthString(@Query('scope') scope?: string) {
|
||||||
|
const authString = this.alipayProvider.generateMobileAuthString(
|
||||||
|
scope || 'auth_user',
|
||||||
|
);
|
||||||
|
return { code: 0, data: { authString }, message: 'ok' };
|
||||||
|
}
|
||||||
|
|
||||||
@Post('alipay')
|
@Post('alipay')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: '支付宝一键登录(新用户自动注册)' })
|
@ApiOperation({ summary: '支付宝一键登录(新用户自动注册)' })
|
||||||
|
|
|
||||||
|
|
@ -256,9 +256,21 @@ class AuthService {
|
||||||
|
|
||||||
// ── 支付宝登录 ────────────────────────────────────────────────────────────
|
// ── 支付宝登录 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 获取支付宝移动端签名授权字符串(tobias 3.x+ 需要)
|
||||||
|
///
|
||||||
|
/// tobias 3.x 后不再支持客户端直接传 appId+scope,
|
||||||
|
/// 需要由后端用 RSA2 私钥生成签名后的 authString,
|
||||||
|
/// 再传给 `Tobias().auth(authString)` 拉起支付宝授权页。
|
||||||
|
///
|
||||||
|
/// 返回: `{ 'authString': '...' }`
|
||||||
|
Future<String> getAlipayAuthString() async {
|
||||||
|
final resp = await _api.get('/api/v1/auth/alipay/auth-string');
|
||||||
|
return resp.data['data']['authString'] as String;
|
||||||
|
}
|
||||||
|
|
||||||
/// 支付宝一键登录 / 自动注册
|
/// 支付宝一键登录 / 自动注册
|
||||||
///
|
///
|
||||||
/// [authCode] 来自 tobias Alipay.authCode()(一次性,3 分钟有效)
|
/// [authCode] 来自 tobias Tobias().auth() 回调中的 auth_code(一次性,3 分钟有效)
|
||||||
Future<AuthResult> loginByAlipay({
|
Future<AuthResult> loginByAlipay({
|
||||||
required String authCode,
|
required String authCode,
|
||||||
String? referralCode,
|
String? referralCode,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import '../../../../core/services/auth_service.dart';
|
||||||
///
|
///
|
||||||
/// ── 登录流程(各平台)────────────────────────────────────
|
/// ── 登录流程(各平台)────────────────────────────────────
|
||||||
/// 微信: fluwx.authBy(NormalAuth) → WeChatAuthResponse(code) → POST /auth/wechat
|
/// 微信: fluwx.authBy(NormalAuth) → WeChatAuthResponse(code) → POST /auth/wechat
|
||||||
/// 支付宝: tobias.aliPayAuth → Map(result 含 auth_code) → POST /auth/alipay
|
/// 支付宝: GET /auth/alipay/auth-string → Tobias().auth(authStr) → Map(result 含 auth_code) → POST /auth/alipay
|
||||||
/// Google: google_sign_in.signIn → GoogleSignInAuthentication.idToken → POST /auth/google
|
/// Google: google_sign_in.signIn → GoogleSignInAuthentication.idToken → POST /auth/google
|
||||||
/// Apple: sign_in_with_apple.getAppleIDCredential → identityToken → POST /auth/apple
|
/// Apple: sign_in_with_apple.getAppleIDCredential → identityToken → POST /auth/apple
|
||||||
class WelcomePage extends StatefulWidget {
|
class WelcomePage extends StatefulWidget {
|
||||||
|
|
@ -58,6 +58,10 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
// registerApi 已在 main.dart 的 app 启动时完成,此处直接使用。
|
// registerApi 已在 main.dart 的 app 启动时完成,此处直接使用。
|
||||||
final _fluwx = Fluwx();
|
final _fluwx = Fluwx();
|
||||||
|
|
||||||
|
// tobias 5.x 使用实例方式调用(tobias 3.x+ 已移除顶层函数 aliPayAuth / isAliPayInstalled)
|
||||||
|
// 同时,auth(authString) 需要后端预签名的 authString,而非直接传 appId+scope。
|
||||||
|
final _tobias = Tobias();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -138,7 +142,8 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
// <queries><package android:name="com.eg.android.AlipayGphone" /></queries>
|
// <queries><package android:name="com.eg.android.AlipayGphone" /></queries>
|
||||||
|
|
||||||
Future<void> _onAlipayTap() async {
|
Future<void> _onAlipayTap() async {
|
||||||
final installed = await isAliPayInstalled;
|
// tobias 5.x: 实例 getter(旧版顶层函数 isAliPayInstalled 已移除)
|
||||||
|
final installed = await _tobias.isAliPayInstalled;
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -150,10 +155,17 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
|
|
||||||
setState(() => _alipayLoading = true);
|
setState(() => _alipayLoading = true);
|
||||||
try {
|
try {
|
||||||
// ALIPAY_APP_ID 通过 --dart-define=ALIPAY_APP_ID=xxx 在编译时注入
|
// tobias 3.x+ 移除了 aliPayAuth(appId, scope) 顶层函数,
|
||||||
// 与后端 .env 中的 ALIPAY_APP_ID 保持一致(同一支付宝移动应用的 AppID)
|
// 改为 Tobias().auth(authString),authString 须由后端 RSA2 签名。
|
||||||
const alipayAppId = String.fromEnvironment('ALIPAY_APP_ID', defaultValue: '');
|
// 原因: 安全升级,防止 Alipay 私钥暴露在客户端。
|
||||||
final result = await aliPayAuth(alipayAppId, 'auth_user');
|
//
|
||||||
|
// 新流程:
|
||||||
|
// 1. 从后端 GET /auth/alipay/auth-string 拿到签名后的 authString
|
||||||
|
// 2. 传给 Tobias().auth(authString) 拉起支付宝 App
|
||||||
|
// 3. 用户授权后 result['result'] 中含 auth_code(同旧流程)
|
||||||
|
// 4. auth_code → POST /auth/alipay(不变)
|
||||||
|
final authString = await AuthService.instance.getAlipayAuthString();
|
||||||
|
final result = await _tobias.auth(authString);
|
||||||
|
|
||||||
final status = result['resultStatus']?.toString() ?? '';
|
final status = result['resultStatus']?.toString() ?? '';
|
||||||
if (status != '9000') {
|
if (status != '9000') {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ dependencies:
|
||||||
share_plus: ^10.0.2
|
share_plus: ^10.0.2
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
fluwx: ^5.0.0
|
fluwx: ^5.0.0
|
||||||
tobias: ^3.0.0
|
tobias: ^5.0.0
|
||||||
google_sign_in: ^6.2.1
|
google_sign_in: ^6.2.1
|
||||||
sign_in_with_apple: ^6.1.0
|
sign_in_with_apple: ^6.1.0
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue