我接手这个模块的第一天前端同事就发来一句话登录接口里有个参数叫 authCode说拿它后端换 userID 就行另外还有个加密串要解出手机号。我翻开文档一看发现这条链路比想象中长——前端拿到的临时码后端用来换的不是用户信息而是一组令牌手机号解密又完全是另一套密钥体系。这篇文章就把支付宝小程序登录从端上授权到 Java 后端落库的完整过程写清楚重点落在两个动作上用授权码换用户唯一 userId以及用 encryptedData 解密手机号。适合正在做支付宝小程序、并且负责 Java 后端的同学参考。1. 授权链路全景authCode、accessToken 与 userId 之间到底是什么关系很多人拿到支付宝小程序登录需求时脑子里是微信那套模型以为前端把 code 传过来后端拿着 code 去换 openid 就结束了。支付宝这套流程在思路上类似但参数名、接口名、返回字段完全不一样第一次接的时候很容易被绕晕。先把这条链路拆开看。1.1 端上拿到的 authCode其实是一次性授权码支付宝小程序端的登录入口很简单在my.getAuthCode成功回调里就能拿到一个字符串这个值就是传给后端的核心参数。my.getAuthCode({ scopes: [auth_base], success: (res) { // res.authCode 就是临时授权码 // 前端需要把它提交给服务端 }, fail: (err) { console.error(getAuthCode 失败, err); } });如果只是取用户 userIdscopes用auth_base就够了这个过程用户基本感知不到授权弹窗。如果想拿到更多用户信息会用到auth_user那就会弹出明确的授权确认框。这里有一个很常见的误区手机号解密的数据不是通过getAuthCode拿到的它走的是另一条my.getPhoneNumber链路后面单独讲。authCode本质是 OAuth2 里的 authorization code它不是登录态也不能直接当 token 用。它是一个一次性临时凭证有效期很短用过一次就作废。文档里写的有效期一般是几分钟但这个值真不能指望缓存复用每次登录都必须重新获取。1.2 后端拿 authCode 换到的是 accessToken userId 一组数据端上把authCode传到后端之后后端要调用支付宝开放平台的alipay.system.oauth.token接口。我最初以为这个接口命名带 token返回的肯定就是用户信息结果发现它同时返回了userId、accessToken、refreshToken三个关键字段。{ alipay_system_oauth_token_response: { user_id: 2088xxxxxxxxxxxx, access_token: authusrBxxxxxxxxxxxx, expires_in: 31536000, refresh_token: authusrBxxxxxxxxxxxx } }这里要纠正一个叫法标题里说的 authToken严格来说不是一个东西。前端传过来的是authCode后端调用 token 接口拿到的才是accessToken而真正用于业务关联的是userId。accessToken只是后续调用开放平台接口时的身份凭证它不是用户唯一标识。userId才是支付宝体系下这个用户的唯一标识同一位用户在你自己的多个应用中拿到的userId是一致的适合直接用来做用户表主键或唯一索引。为什么设计成两步而不是直接返回 userId这是 OAuth2 的通用约定。授权码模式先换令牌再通过令牌访问资源好处是授权码有效期极短即使传输过程中被截获风险窗口也比直接暴露长效 token 小得多。理解了这层设计你看到 token 接口返回一堆字段时就不会慌。2. Java 后端换 userId 落地环境参数、核心代码与高频报错链路理清之后后端写代码就是常规操作了。但恰恰是这部分我在联调时踩过不少坑大多数问题不是接口逻辑难而是环境参数配错。2.1DefaultAlipayClient初始化五个参数必须逐一核对不管用哪个版本的 alipay-sdk-java初始化客户端时都要传网关地址、应用 ID、应用私钥、格式、编码、支付宝公钥、签名类型。一个典型的初始化代码长这样AlipayClient alipayClient new DefaultAlipayClient( https://openapi.alipay.com/gateway.do, 202100xxxxxxxx, 你的应用私钥注意是PKCS8格式, json, UTF-8, 支付宝公钥不是应用公钥, RSA2 );看起来简单但实际配置时容易在两个地方翻车。第一个是密钥填反很多人看到配置项里有“应用公钥”“应用私钥”“支付宝公钥”直接混淆。记住一句话私钥永远保存在后端自己手里公钥用于对方验签。你在初始化客户端时填的应用私钥是自己的支付宝公钥是去开放平台“应用详情-开发设置-接口加签方式”里复制的那一段千万别把生成的应用公钥填进去。第二个是签名类型。支付宝现在全面推荐 RSA2也就是 SHA256WithRSA如果因为历史原因配置成 RSA部分新接口会直接验签失败。初始化时第六个参数如果不是 RSA2建议统一改掉。环境上还有一个容易被忽略的点沙箱环境和生产环境的网关地址不同应用 ID、密钥也完全不同。联调阶段用沙箱没问题上线前记得整体切到生产环境和生产配置最好通过配置文件区分不要硬编码。2.2 用授权码换 userId 的完整代码客户端初始化好后换 userId 的逻辑就是一个请求封装。我用AlipaySystemOauthTokenRequest来完成代码可以收敛成一个服务方法。Service public class AlipayAuthService { private final AlipayClient alipayClient; public AlipayAuthService(AlipayClient alipayClient) { this.alipayClient alipayClient; } public String getUserIdByAuthCode(String authCode) { AlipaySystemOauthTokenRequest request new AlipaySystemOauthTokenRequest(); request.setGrantType(authorization_code); request.setCode(authCode); try { AlipaySystemOauthTokenResponse response alipayClient.execute(request); if (!response.isSuccess()) { // 重点是打印 subCode 和 subMsg而不是只看 isSuccess throw new BizException( 支付宝授权码换取userId失败: response.getSubCode() response.getSubMsg() ); } return response.getUserId(); } catch (AlipayApiException e) { throw new BizException(调用支付宝网关异常, e); } } }有人会问为什么不用response.getAccessToken()当业务标识因为accessToken本质是一串临时凭证虽然有效时间很长但它代表的是调用资格不是用户身份。你后端的用户表应该以userId为唯一标识accessToken最多在后续主动调用开放接口时使用并且不建议落库。2.3 三个高频报错和对应的排错思路我联调时翻车最狠的三类问题基本上覆盖了这个接口 90% 的异常场景。第一个是授权码失效。前端把同一个authCode提交了两次第二个请求一定会失败因为授权码只能消费一次。前端登录按钮没加 loading、用户手快双击提交都会触发这个情况。后端接口要容忍这种重复提交要么做幂等处理要么把错误信息透传到前端提示重新授权。第二个是验签失败。返回的错误不是业务错误而是网关在签名校验阶段就拒绝了请求。这时候多半是支付宝公钥没配置对或者应用私钥复制的时候带了多余换行、被 IDE 自动转义。我在本地调了半天最后发现是私钥字符串里混入了\n字面量而不是真正的换行符这个细节非常坑。第三个是response.isSuccess()返回 false 但没抛异常。这时很多人会忽略响应里的subCode和subMsg其实真正的原因全在这两个字段里。比如isv.code-invalid表示授权码不存在或已被使用isv.code-expired表示授权码过期。不要嫌麻烦把这两个字段打出来排查效率能翻倍。3. encryptedData 解密手机号AES/CBC/PKCS5Padding 的实战细节换到 userId 只是登录的一半手机号解密完全是另一条线。一开始我以为手机号也会通过 token 接口返回结果不是前端拿到的是一段encryptedData需要后端用专门的密钥去解。3.1 手机号数据是怎么到前端手里的用户点击“一键登录”或“获取手机号”按钮时支付宝小程序端的流程和登录不一样。通常要先通过一个授权组件触达用户然后在my.getPhoneNumber的成功回调里拿到数据。my.getPhoneNumber({ success: (res) { // res.encryptedData 是加密后的手机号密文 // res.iv 是加密使用的初始向量 // res.sign 是签名信息用于防篡改 // 把这三个值一起提交给后端 }, fail: () { // 用户取消授权时走这里 } });这里的encryptedData不是随便用支付宝公钥就能解的它使用的是 AES 对称加密。密钥在哪里登录开放平台后台进入小程序应用详情在“开发设置-接口加签方式”或“内容加密方式”相关配置里能看到一个允许设置或生成的密钥这个才是解密手机号要用的aesKey。我有一次拿着支付宝公钥去解折腾半天解不出来后来才反应过来非对称加密是为了传输公钥和验签真正加密业务数据的是 AES 密钥两套密钥体系不能混用。3.2 可以直接抄的解密工具类解密算法是 AES/CBC/PKCS5Padding这个和微信的 AES-128-CBC 是同一种套路但参数细节不同。我封装了一个工具方法尽量保持纯净方便直接复制。public class AlipayPhoneNumberDecryptor { public static String decrypt(String encryptedData, String iv, String aesKey) throws Exception { byte[] cipherBytes Base64.getDecoder().decode(encryptedData); byte[] ivBytes Base64.getDecoder().decode(iv); SecretKeySpec keySpec new SecretKeySpec( aesKey.getBytes(StandardCharsets.UTF_8), AES ); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] plainBytes cipher.doFinal(cipherBytes); return new String(plainBytes, StandardCharsets.UTF_8); } }调用方式就三步String plainJson AlipayPhoneNumberDecryptor.decrypt( encryptedData, iv, aesKey ); // plainJson 形如 {mobile:13800138000} JSONObject jsonObject JSON.parseObject(plainJson); String mobile jsonObject.getString(mobile);解密后的 JSON 里通常就是mobile字段直接取出来用。3.3 三类解密失败原因按顺序排查解密报错时最容易的是直接抛IllegalBlockSizeException或者解出来是一堆乱码。按下面顺序排查能解决绝大多数问题。第一个检查 AESKey。这个 key 必须是控制台上复制出来的完整字符串长度通常对应 32 字节。复制的时候小心别少了末尾字符更别在配置里给它加引号。编码上要统一用 UTF-8 转字节很多偏移乱码问题都是编码不一致。第二个检查 IV 的处理。前端传过来的iv是一个 Base64 编码后的字符串直接把它当成字节数组去初始化IvParameterSpec是不对的必须先用Base64.getDecoder().decode(iv)还原成真正的 16 字节向量。如果用iv.getBytes()转字节长度和内容都不对解密必挂。第三个检查encryptedData是否被传输过程破坏。如果前端把密文放在 URL query 参数里号可能被解析成空格/号也可能出问题传输前一定要做安全编码或者干脆用 JSON 请求体提交。我平时要求前端一律用 JSON body 传这三个字段省得跟 URL 编码纠缠。4. 拿到 userId 和手机号之后业务侧怎么设计才更稳很多同学做到“解出手机号”就以为大功告成直接拿 userId 当登录凭证返回给前端。这里有一个安全上的大坑需要单独拿出来说。4.1 不要拿 userId 或 accessToken 直接当登录态userId是支付宝体系里的用户唯一标识它长期稳定但它并不适合作为你自己系统的 session。设想一下如果前端把 userId 存在本地缓存下次请求直接传 userId 当作身份凭证那任何拿到这个值的人都可以伪装成这个用户。accessToken更不合适它是支付宝 API 的调用凭证一旦泄露别人还能拿它去调用有权限的开放接口。正确做法是在后端拿到userId后生成你自己的业务登录令牌比如一个随机的 UUID把它存到 Redis 里设置合理的过期时间并同时记录对应的userId。前端后续请求都带这个业务令牌后端根据令牌找到用户身份。标识用途是否适合做登录态userId支付宝体系下唯一用户标识只用于业务关联不下发前端accessToken支付宝开放接口调用凭证后端内部使用不落日志自建业务令牌自己系统的会话标识下发前端可过期可撤销这套设计对后续扩展也有好处以后接入其他登录方式时只要最终映射到同一个内部用户 ID 就行。4.2 手机号属于敏感数据存储和日志都要处理手机号解密成功后不要直接在日志里打印明文也不要在接口返回值里原样回传。属于个人信息保护法里明确的敏感个人信息能不加就不加必须加的时候要脱敏展示、加密存储。我在项目里的经验是手机号只用于必要场景比如物流、实名认证、消息通知。用户表里会加一个mobile字段落库前用服务端自己的加密工具加密页面展示时只露出前三位和后四位日志一概不打印完整手机号。4.3 一张简单的用户绑定表业务流程理顺之后表结构其实很简单。用一个用户表把支付宝 userId 和手机号都挂上去。CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, alipay_user_id VARCHAR(64) NOT NULL UNIQUE, mobile VARCHAR(32), nickname VARCHAR(64), avatar_url VARCHAR(255), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );登录时先用 userId 查表查不到就新建用户再生成业务令牌返回。手机号解密成功后通过UPDATE补全mobile字段不需要把整条登录流程和手机号绑定在一起。很多业务场景下用户拒绝授权手机号仍然可以先用建议把手机号绑定做成可延后、可补全的独立动作。5. 联调期最容易翻车的边界问题盘点最后这部分内容是我实际联调测试中反复折腾出来的经验。代码本身不复杂但边界问题一旦触发排查起来相当费时间。5.1 authCode 一次性重复提交和并发都要防前文提到过授权码只能消费一次。实际项目里前端在页面快速点击、接口超时后自动重试、用户开两个页面同时登录都会导致同一个authCode被提交两回。第一次请求成功第二次大概率返回code invalid。后端处理方式有两种思路。简单一点的让前端保证按钮 loading避免重复提交复杂一点的用 Redis 在接口入口做一个针对authCode的防重处理消费过的 code 直接标记已使用。我个人建议至少把第一种做了否则联调时你自己都会被重复请求搞烦。5.2 用户取消授权和手机号拿不到的情况用户取消手机号授权、或者授权后拿不到encryptedData时端上会走进fail回调。后端千万不要假设只要发起登录就一定有手机号返回。我的做法是手机号授权失败不影响登录主流程userId 先落库用户挂一个“未绑定手机号”的状态等后续有需要时再引导补全。还有一个更隐蔽的情况用户使用的是某个虚拟号码或者没有绑定手机号的支付宝账号即使解密成功拿到的可能也不是期望的真实手机号。业务上要设计好这个兜底逻辑不要在手机号为空时直接报错。5.3 用 subCode/subMsg 快速定位不要只盯着 isSuccess联调期间我和前端对接口经常遇到“ result 里 isSuccess 是 false”的情况。一开始我以为是网络问题后来才发现真正原因要看subCode和subMsg。下面这些是我遇到过的返回信息不一定覆盖全但排查方向可以参考返回信息示例含义排查方向isv.code-invalid授权码不存在或已使用确认 authCode 是否被重复消费isv.code-expired授权码过期确认获取 code 到后端调用之间的耗时验签失败网关签名校验不通过检查公钥私钥配置和签名类型isv.app-id-not-exist应用 ID 不存在确认网关环境是否和生产匹配5.4 沙箱环境与生产环境永远不要把配置搞混支付宝开放平台有沙箱环境里面有测试账号、沙箱网关、沙箱应用。联调初期确实很方便但有些接口在沙箱里的返回行为和生产并不完全一致。我自己吃过一次亏沙箱里一切正常切到生产第一次调用就报应用不存在查了半天发现是网关地址还指向沙箱。上线前一定要检查四个值网关地址、应用 ID、应用私钥、支付宝公钥。这四项必须全部切到生产配置而且建议放在不同的配置 profile 里杜绝硬编码串号。最后说点个人的体会。这套登录链路整体不复杂但每一步都建立在“参数正确、密钥正确、时效正确”的基础上。我的建议是不要在项目里铺开写业务代码之前先花半小时把端上拿授权码、后端换 userId、再用一条测试密文解手机号这三个环节完整跑通。加解密工具类顺手写两个单测防止以后 AESKey 调整或字段格式变化导致静默出错。把这套链路走顺了后面再对接会员信息、订单信息这些开放接口思路都会清晰很多。