From 828770add8420cec83bf45557b208faeb2838530 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 4 Mar 2026 09:26:30 -0800 Subject: [PATCH] =?UTF-8?q?fix(alipay):=20=E9=80=82=E9=85=8D=20tobias=205.?= =?UTF-8?q?x=20=E6=96=B0=20auth=20API=EF=BC=8C=E5=90=8E=E7=AB=AF=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=AD=BE=E5=90=8D=20authString?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../infrastructure/alipay/alipay.provider.ts | 57 +++++++++++++++++++ .../http/controllers/auth.controller.ts | 26 +++++++++ .../lib/core/services/auth_service.dart | 14 ++++- .../auth/presentation/pages/welcome_page.dart | 24 ++++++-- frontend/genex-mobile/pubspec.yaml | 2 +- 5 files changed, 115 insertions(+), 8 deletions(-) diff --git a/backend/services/auth-service/src/infrastructure/alipay/alipay.provider.ts b/backend/services/auth-service/src/infrastructure/alipay/alipay.provider.ts index 8481da6..9a39838 100644 --- a/backend/services/auth-service/src/infrastructure/alipay/alipay.provider.ts +++ b/backend/services/auth-service/src/infrastructure/alipay/alipay.provider.ts @@ -86,10 +86,67 @@ export class AlipayProvider { private readonly logger = new Logger('AlipayProvider'); 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 gateway = 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 = { + 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 * diff --git a/backend/services/auth-service/src/interface/http/controllers/auth.controller.ts b/backend/services/auth-service/src/interface/http/controllers/auth.controller.ts index 35ff8c6..de87b89 100644 --- a/backend/services/auth-service/src/interface/http/controllers/auth.controller.ts +++ b/backend/services/auth-service/src/interface/http/controllers/auth.controller.ts @@ -1,6 +1,8 @@ import { Controller, Post, + Get, + Query, Body, HttpCode, HttpStatus, @@ -14,6 +16,7 @@ import { ThrottlerGuard } from '@nestjs/throttler'; import { AuthService } from '../../../application/services/auth.service'; import { WechatService } from '../../../application/services/wechat.service'; import { AlipayService } from '../../../application/services/alipay.service'; +import { AlipayProvider } from '../../../infrastructure/alipay/alipay.provider'; import { GoogleService } from '../../../application/services/google.service'; import { AppleService } from '../../../application/services/apple.service'; import { RegisterDto } from '../dto/register.dto'; @@ -42,6 +45,7 @@ export class AuthController { private readonly alipayService: AlipayService, private readonly googleService: GoogleService, private readonly appleService: AppleService, + private readonly alipayProvider: AlipayProvider, ) {} /* ── 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') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: '支付宝一键登录(新用户自动注册)' }) diff --git a/frontend/genex-mobile/lib/core/services/auth_service.dart b/frontend/genex-mobile/lib/core/services/auth_service.dart index 19cbca4..9661cdc 100644 --- a/frontend/genex-mobile/lib/core/services/auth_service.dart +++ b/frontend/genex-mobile/lib/core/services/auth_service.dart @@ -256,9 +256,21 @@ class AuthService { // ── 支付宝登录 ──────────────────────────────────────────────────────────── + /// 获取支付宝移动端签名授权字符串(tobias 3.x+ 需要) + /// + /// tobias 3.x 后不再支持客户端直接传 appId+scope, + /// 需要由后端用 RSA2 私钥生成签名后的 authString, + /// 再传给 `Tobias().auth(authString)` 拉起支付宝授权页。 + /// + /// 返回: `{ 'authString': '...' }` + Future 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 loginByAlipay({ required String authCode, String? referralCode, diff --git a/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart b/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart index 66e868c..32bea08 100644 --- a/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart +++ b/frontend/genex-mobile/lib/features/auth/presentation/pages/welcome_page.dart @@ -29,7 +29,7 @@ import '../../../../core/services/auth_service.dart'; /// /// ── 登录流程(各平台)──────────────────────────────────── /// 微信: 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 /// Apple: sign_in_with_apple.getAppleIDCredential → identityToken → POST /auth/apple class WelcomePage extends StatefulWidget { @@ -58,6 +58,10 @@ class _WelcomePageState extends State { // registerApi 已在 main.dart 的 app 启动时完成,此处直接使用。 final _fluwx = Fluwx(); + // tobias 5.x 使用实例方式调用(tobias 3.x+ 已移除顶层函数 aliPayAuth / isAliPayInstalled) + // 同时,auth(authString) 需要后端预签名的 authString,而非直接传 appId+scope。 + final _tobias = Tobias(); + @override void initState() { super.initState(); @@ -138,7 +142,8 @@ class _WelcomePageState extends State { // Future _onAlipayTap() async { - final installed = await isAliPayInstalled; + // tobias 5.x: 实例 getter(旧版顶层函数 isAliPayInstalled 已移除) + final installed = await _tobias.isAliPayInstalled; if (!installed) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -150,10 +155,17 @@ class _WelcomePageState extends State { setState(() => _alipayLoading = true); try { - // ALIPAY_APP_ID 通过 --dart-define=ALIPAY_APP_ID=xxx 在编译时注入 - // 与后端 .env 中的 ALIPAY_APP_ID 保持一致(同一支付宝移动应用的 AppID) - const alipayAppId = String.fromEnvironment('ALIPAY_APP_ID', defaultValue: ''); - final result = await aliPayAuth(alipayAppId, 'auth_user'); + // tobias 3.x+ 移除了 aliPayAuth(appId, scope) 顶层函数, + // 改为 Tobias().auth(authString),authString 须由后端 RSA2 签名。 + // 原因: 安全升级,防止 Alipay 私钥暴露在客户端。 + // + // 新流程: + // 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() ?? ''; if (status != '9000') { diff --git a/frontend/genex-mobile/pubspec.yaml b/frontend/genex-mobile/pubspec.yaml index a6234aa..bcf0a59 100644 --- a/frontend/genex-mobile/pubspec.yaml +++ b/frontend/genex-mobile/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: share_plus: ^10.0.2 flutter_secure_storage: ^9.2.2 fluwx: ^5.0.0 - tobias: ^3.0.0 + tobias: ^5.0.0 google_sign_in: ^6.2.1 sign_in_with_apple: ^6.1.0