微信OAuth2.0单域名限制破解:回调中继服务架构设计与实战

微信OAuth2.0单域名限制破解:回调中继服务架构设计与实战

1. 项目概述与核心痛点

做微信生态开发,无论是公众号、小程序还是网页应用,OAuth2.0网页授权都是绕不开的一环。它负责将用户从你的页面引导至微信授权,再带着用户身份信息(openid, unionid)跳转回来,是用户身份体系的基石。但微信官方给这个流程上了一道“紧箍咒”:一个公众号或小程序,只能设置一个网页授权回调域名。这个限制,在单体应用时代或许还能忍受,但在微服务架构、多环境部署、前后端分离成为标配的今天,就成了一个实实在在的“拦路虎”。

想象一下这个场景:你有一个主站www.example.com,一个管理后台admin.example.com,还有一个给合作伙伴用的子站partner.example.com,它们都需要微信登录。按照微信的规则,你只能从这三个里选一个填到后台的“网页授权域名”配置里。选了主站,管理和合作伙伴站点就无法正常完成授权回调,用户会看到“redirect_uri参数错误”的提示。更别提开发、测试、预发布这些不同环境了,每个环境一个域名,微信后台可不会让你填那么多。

这个限制的本质,是微信出于安全考虑,防止授权码被劫持到恶意域名。但它确实给开发者带来了巨大的架构复杂度和运维成本。网上常见的“解决方案”,比如让所有服务都走一个统一的网关域名进行授权,或者用Nginx做复杂的反向代理和重写,往往治标不治本,要么引入单点故障,要么配置繁琐容易出错。

今天要分享的,是我们团队在多个中大型项目中验证过的一套实战方案。它不依赖于复杂的网关配置,也不需要对现有后端服务进行伤筋动骨的改造,核心思路是将授权回调的“物理”动作与“逻辑”处理进行解耦。我们通过一个轻量的、独立的“授权回调中继服务”,来统一对接微信,再由这个中继服务将授权结果分发给真正的业务后端。这套方案稳定运行了两年多,轻松支撑了日均百万级的授权请求,今天就把它的设计思路、技术细节和踩过的坑,毫无保留地分享出来。

2. 方案核心设计:回调中继与状态分发

要突破单域名限制,最直接的思路就是:让微信只认识一个域名,但这个域名背后是一个“智能路由器”,它能根据请求的上下文,把用户和授权码准确“路由”到真正的目标业务服务去。这就是“回调中继”模式的核心。

2.1 整体架构与数据流

整个方案的架构可以分为三个核心角色:

  1. 业务前端(多个):散布在不同域名下的网页或H5,如www.a.com,admin.b.com
  2. 授权回调中继服务(一个):一个独立的、对公网暴露的服务,域名固定为oauth-relay.yourcompany.com,并且将这个域名配置到微信公众平台的后台。
  3. 业务后端服务(多个):各个业务线自己的后端服务器,处理具体的登录逻辑。

授权流程的数据流如下:

  1. 用户访问业务前端A(例如www.a.com/home)。
  2. 前端A需要微信登录,它不直接向微信发起授权请求,而是先请求自己的业务后端A,获取一个带状态的授权跳转URL
  3. 业务后端A生成一个全局唯一的state参数(例如UUID),这个state必须包含足够的信息来标识“谁在请求”。我们将state设计为一个JWT(JSON Web Token)或加密字符串,里面至少包含:target_app(目标业务应用标识)、final_redirect_uri(用户最终应该跳转回的页面,如www.a.com/home)。然后,后端A将这个statetarget_app的映射关系,临时存储到分布式缓存(如Redis)中,设置一个较短的过期时间(如5分钟)。
  4. 业务后端A拼接出指向中继服务的URL,格式为:https://oauth-relay.yourcompany.com/wechat/auth?state=加密后的state&app_id=wx123456。将这个URL返回给前端A。
  5. 前端A引导用户跳转到上述中继服务URL。
  6. 中继服务收到请求,解析出app_id(通常指公众号的AppID)。它用这个app_id去获取对应的公众号配置(如AppSecret),然后重定向用户到微信官方的授权页面。关键一步:中继服务在向微信发起请求时,使用的redirect_uri参数,固定为https://oauth-relay.yourcompany.com/wechat/callback。而将前端传来的那个包含业务信息的state,原封不动地传递给微信。
  7. 用户在微信端确认授权。
  8. 微信带着授权码code和之前传递的state,回调到中继服务的/wechat/callback接口。
  9. 中继服务的回调接口被触发。它首先用code和缓存的公众号配置,向微信服务器发起请求,换取access_token和用户的openid这是整个流程中唯一需要AppSecret的地方,而AppSecret只存在于中继服务这一处,安全性更高。
  10. 换取用户信息成功后,中继服务需要将结果“送还”给最初发起请求的业务后端。它解密或解析state参数,得到target_appfinal_redirect_uri
  11. 中继服务根据target_app,查找或配置好的对应关系,找到目标业务后端B的回调接收地址(例如https://api-a.yourcompany.com/auth/wechat/callback)。
  12. 中继服务将openidaccess_token(短期)、state等信息,通过一个服务间安全的POST请求(可使用签名验证)发送给业务后端B的接收地址。
  13. 业务后端B收到信息后,执行自己的业务逻辑:用openid查找或创建本地用户,生成自身的会话Token(如JWT),最后将用户重定向到final_redirect_uri,并在URL中附带登录成功的令牌(或通过Set-Cookie)。
  14. 用户浏览器跳转回最初的业务页面,完成登录。

核心要点:在整个过程中,微信只与oauth-relay.yourcompany.com这一个域名交互,完全符合平台规则。而复杂的多业务路由逻辑,则由我们自建的中继服务在“幕后”完成。

2.2 关键设计决策与考量

为什么选择“中继”而非“网关代理”?早期我们考虑过使用API网关(如Kong, Nginx)对所有/wechat/callback路径的请求,根据域名或路径前缀反向代理到不同的后端。但这有几个问题:一是网关配置复杂,每新增一个业务服务就要改动网关配置;二是业务逻辑侵入网关,网关通常只适合做流量转发和通用策略,不适合处理像解析state、换取token这类业务逻辑;三是错误处理不便,网关难以实现业务级的错误重试或状态跟踪。中继服务作为一个独立的轻量级应用,专注处理授权这一件事,逻辑更清晰,也更易于维护和扩展。

State参数的设计与安全state参数是连接一次授权请求始末的“生命线”,其设计至关重要。

  • 内容:必须包含业务标识(target_app)和最终回跳地址(final_redirect_uri)。还可以加入时间戳(timestamp)防重放,加入随机数(nonce)防猜测。
  • 形式:推荐使用JWT或对称加密(如AES)。JWT的好处是自包含、可验证,中继服务和业务后端如果共享一个密钥,都可以独立验证其有效性。加密字符串则需要中继服务来解密。绝对禁止使用明文或简单编码(如Base64),这会导致final_redirect_uri被篡改,引发开放重定向漏洞。
  • 存储:业务后端生成state后,需要将state与本次会话的某些临时信息(如生成state时用的session_id)存入缓存,以便在最后一步验证state是否被使用过,防止同一个code被重复消费。

中继服务与业务后端的安全通信中继服务将用户信息传递给业务后端时,必须确保通道安全。

  1. 内网通信:理想情况下,中继服务与业务后端部署在同一个VPC内网,通过内网域名或IP调用,隔绝外网访问。
  2. 签名验证:如果必须公网通信,则每次请求都需要签名。例如,使用双方预先共享的密钥,对请求参数(如openidtimestampnonce)按规则拼接后计算HMAC-SHA256签名,业务后端收到后以同样规则验签。
  3. HTTPS:这是必须的,无论内外网。

分布式缓存的选择我们选择Redis,因为它性能高、支持过期时间、数据结构丰富。存储的Key可以设计为wechat:auth:state:{state_value},Value存储一个简单的JSON,包含target_app,create_time等。设置过期时间(如300秒)非常重要,可以自动清理未使用的垃圾数据,防止缓存被撑满。

3. 核心细节解析与实操要点

理解了整体架构,我们深入到代码和配置层面,看看每个环节具体如何实现,有哪些坑需要提前避开。

3.1 中继服务的详细实现

我们以使用Spring Boot(Java)为例,其他语言框架原理相通。

1. 授权跳转接口 (/wechat/auth)这个接口处理来自各个业务前端的初次跳转请求。

@RestController @RequestMapping("/wechat") public class RelayAuthController { @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private WechatConfigService wechatConfigService; // 获取公众号配置的服务 @GetMapping("/auth") public void auth(@RequestParam String state, @RequestParam String app_id, HttpServletResponse response) throws IOException { // 1. 校验state基本格式(可选,防垃圾请求) if (!isValidStateFormat(state)) { response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid state parameter"); return; } // 2. 根据app_id获取公众号配置 WechatAppConfig config = wechatConfigService.getConfig(app_id); if (config == null) { response.sendError(HttpStatus.NOT_FOUND.value(), "Wechat app config not found"); return; } // 3. 准备跳转到微信授权页的参数 String redirectUri = "https://oauth-relay.yourcompany.com/wechat/callback"; // 固定回调地址 String scope = "snsapi_userinfo"; // 或 snsapi_base String wechatAuthUrl = String.format( "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect", URLEncoder.encode(config.getAppId(), StandardCharsets.UTF_8.name()), URLEncoder.encode(redirectUri, StandardCharsets.UTF_8.name()), scope, URLEncoder.encode(state, StandardCharsets.UTF_8.name()) // 原样传递业务方state ); // 4. 重定向到微信 response.sendRedirect(wechatAuthUrl); } }

注意:这里state参数我们不做解密,只是原样传递给微信。因为它是业务后端生成的、给业务后端自己用的。中继服务只充当一个传递者。

2. 回调处理接口 (/wechat/callback)这是中继服务的核心,处理微信的回调。

@GetMapping("/callback") public void callback(@RequestParam String code, @RequestParam String state, HttpServletResponse response) throws IOException { if (StringUtils.isBlank(code) || StringUtils.isBlank(state)) { response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing code or state"); return; } // 1. 解析state,获取目标业务应用标识 TargetAppInfo targetAppInfo; try { targetAppInfo = stateDecoder.decode(state); // 解密或解析JWT } catch (Exception e) { log.error("Failed to decode state: {}", state, e); response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid state"); return; } // 2. 根据state中的app_id(或从targetAppInfo中获取)换取配置 WechatAppConfig config = wechatConfigService.getConfig(targetAppInfo.getAppId()); if (config == null) { response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Config not found"); return; } // 3. 用code向微信换取access_token和openid WechatAuthToken wechatToken; try { wechatToken = wechatApiClient.exchangeCodeForToken(config.getAppId(), config.getAppSecret(), code); } catch (WechatApiException e) { log.error("Failed to exchange code for token, appId: {}, code: {}", config.getAppId(), code, e); // 这里可以返回一个友好的错误页面给用户,提示授权失败 response.sendRedirect(targetAppInfo.getFinalRedirectUri() + "?error=auth_failed"); return; } // 4. (可选) 如果需要,用access_token拉取用户基本信息 WechatUserInfo userInfo = null; if ("snsapi_userinfo".equals(targetAppInfo.getScope())) { userInfo = wechatApiClient.getUserInfo(wechatToken.getAccessToken(), wechatToken.getOpenid()); } // 5. 构建传递给业务后端的数据包 CallbackPayload payload = new CallbackPayload(); payload.setOpenid(wechatToken.getOpenid()); payload.setUnionid(wechatToken.getUnionid()); payload.setAccessToken(wechatToken.getAccessToken()); payload.setExpiresIn(wechatToken.getExpiresIn()); payload.setUserInfo(userInfo); payload.setOriginalState(state); // 传回原始state,供业务方校验 // 6. 根据targetAppInfo,找到对应业务后端的回调接收地址 String backendCallbackUrl = backendRoutingService.getCallbackUrl(targetAppInfo.getTargetApp()); if (backendCallbackUrl == null) { log.error("Backend callback URL not found for targetApp: {}", targetAppInfo.getTargetApp()); response.sendRedirect(targetAppInfo.getFinalRedirectUri() + "?error=internal_error"); return; } // 7. 安全地调用业务后端(内网HTTP + 签名) boolean deliverySuccess; try { deliverySuccess = backendDeliveryService.deliverPayload(backendCallbackUrl, payload); } catch (Exception e) { log.error("Failed to deliver payload to backend: {}", backendCallbackUrl, e); deliverySuccess = false; } // 8. 根据投递结果,最终跳转回用户浏览器 if (deliverySuccess) { // 告诉业务后端成功后,由业务后端重定向。这里中继服务也可以直接重定向到final_redirect_uri。 // 我们选择让业务后端控制最终跳转,所以这里返回一个“处理中”页面,或通过前端JS轮询业务后端状态。 // 更简单的做法:中继服务直接重定向到业务后端的一个“完成”页面,由该页面设置cookie后再跳转。 // 这里演示一个简化版:假设业务后端接收成功后,会返回一个重定向指令的URL。 // 实际上,步骤7的投递是服务间调用,不涉及浏览器。我们需要让浏览器跳转到业务后端的一个页面。 // 因此,我们构造一个跳转URL,将openid等参数以安全的方式(如一次性token)带给业务后端。 String token = tokenService.createOneTimeToken(payload); // 生成一次性令牌,存入缓存 String backendFinalUrl = String.format("%s?relay_token=%s", backendRoutingService.getFinalizeUrl(targetAppInfo.getTargetApp()), token); response.sendRedirect(backendFinalUrl); } else { // 投递失败,跳回原页面并报错 response.sendRedirect(targetAppInfo.getFinalRedirectUri() + "?error=delivery_failed"); } }

这段代码逻辑较长,核心是步骤3、5、7。步骤7的backendDeliveryService.deliverPayload是实现服务间通信的关键。

3.2 业务后端的改造要点

业务后端需要新增或修改两个接口:

  1. 生成跳转URL的接口:供自己的前端调用,用于生成指向中继服务的、带state的URL。
  2. 最终回调处理接口:接收中继服务传递过来的用户信息,完成本地登录逻辑。

接口1:生成授权URL

// 业务后端A的接口 @GetMapping("/auth/wechat/url") public ApiResponse<String> getWechatAuthUrl(@RequestParam String redirectUri) { // 1. 生成唯一state,包含target_app和final_redirect_uri String targetApp = "app_a"; // 本应用的标识 AuthState state = new AuthState(); state.setTargetApp(targetApp); state.setFinalRedirectUri(redirectUri); // 前端传来的最终回跳地址 state.setTimestamp(System.currentTimeMillis()); state.setNonce(RandomStringUtils.randomAlphanumeric(8)); // 2. 将state加密或编码为JWT String encodedState = stateEncoder.encode(state); // 3. 可选:将state与当前会话关联存入缓存,用于后续防重放验证 redisTemplate.opsForValue().set("wechat:state:" + encodedState, sessionId, 5, TimeUnit.MINUTES); // 4. 拼接中继服务URL String relayAuthUrl = String.format("https://oauth-relay.yourcompany.com/wechat/auth?app_id=%s&state=%s", URLEncoder.encode(wechatAppId, "UTF-8"), URLEncoder.encode(encodedState, "UTF-8")); return ApiResponse.success(relayAuthUrl); }

接口2:处理最终回调(由中继服务引导用户浏览器访问)这个接口通常是一个简单的GET请求,通过一次性令牌(relay_token)从缓存中获取用户信息。

// 业务后端A的接口 @GetMapping("/auth/wechat/finalize") public void finalizeWechatAuth(@RequestParam String relay_token, HttpServletRequest request, HttpServletResponse response) throws IOException { // 1. 用relay_token从缓存换取中继服务传递过来的用户信息 CallbackPayload payload = tokenService.consumePayload(relay_token); // 获取并删除 if (payload == null) { response.sendRedirect("/error?msg=invalid_token"); return; } // 2. 验证原始state(防止伪造) String originalState = payload.getOriginalState(); AuthState decodedState = stateEncoder.decode(originalState); if (!"app_a".equals(decodedState.getTargetApp())) { log.warn("State targetApp mismatch."); response.sendRedirect("/error?msg=state_mismatch"); return; } // 可选:验证state是否已使用过(防重放) String usedStateKey = "wechat:state:used:" + originalState; if (redisTemplate.hasKey(usedStateKey)) { log.warn("State reused detected: {}", originalState); response.sendRedirect("/error?msg=state_reused"); return; } redisTemplate.opsForValue().set(usedStateKey, "1", 10, TimeUnit.MINUTES); // 3. 核心业务逻辑:用openid/unionid处理本地用户登录 User localUser = userService.findOrCreateByWechatOpenid(payload.getOpenid(), payload.getUserInfo()); // 4. 创建本地会话(如生成JWT,设置Cookie) String localToken = jwtService.generateToken(localUser); CookieUtils.setCookie(response, "auth_token", localToken, 7 * 24 * 3600); // 5. 跳转回用户最初想访问的页面 response.sendRedirect(decodedState.getFinalRedirectUri()); }

3.3 配置与管理

微信公众平台配置只需将oauth-relay.yourcompany.com配置为唯一的“网页授权域名”。确保域名已经ICP备案。

中继服务配置需要维护一个映射关系,将target_app(如app_a,app_b)映射到对应的业务后端回调地址。这个配置可以放在数据库、配置中心或环境变量里。

# application.yml 示例 wechat: relay: backend-mappings: app_a: callback-url: https://api-a.internal.com/auth/wechat/callback # 服务间通信地址 finalize-url: https://www.a.com/auth/wechat/finalize # 浏览器跳转地址 app_b: callback-url: https://api-b.internal.com/auth/wechat/callback finalize-url: https://admin.b.com/auth/wechat/finalize

跨域问题当中继服务最终将用户重定向到final_redirect_uri(另一个域名)时,不存在跨域问题,因为这是浏览器重定向。但在前端最初从中继服务获取授权URL时,如果中继服务与前端域名不同,则需要配置CORS。

4. 实操过程与核心环节实现

让我们模拟一个完整的用户登录流程,串联起所有组件。

场景:用户访问https://www.a.com/dashboard(业务前端A),点击“微信登录”。

步骤1:前端发起登录请求前端A的JavaScript代码:

// 在 www.a.com 域名下 async function requestWechatLogin() { const currentUrl = window.location.href; // 最终要跳回来的地址 const resp = await fetch('https://api-a.yourcompany.com/auth/wechat/url?redirect_uri=' + encodeURIComponent(currentUrl)); const result = await resp.json(); if (result.code === 0) { // 获取到中继服务的跳转地址 window.location.href = result.data; // 跳转到类似 https://oauth-relay.yourcompany.com/wechat/auth?state=xxx&app_id=xxx } else { alert('获取登录地址失败'); } }

步骤2:中继服务引导至微信授权页用户浏览器到达中继服务的/wechat/auth,中继服务校验app_id后,返回302重定向到微信官方授权页。地址栏变为微信的域名。

步骤3:用户授权并跳回中继服务用户在微信页面确认授权,微信带着codestate回调到https://oauth-relay.yourcompany.com/wechat/callback

步骤4:中继服务处理回调中继服务用codetoken,解析state得到target_app=app_afinal_redirect_uri=https://www.a.com/dashboard。然后根据app_a找到业务后端A的finalize-urlhttps://www.a.com/auth/wechat/finalize),并生成一个一次性relay_token关联用户信息。最后,中继服务返回302重定向到https://www.a.com/auth/wechat/finalize?relay_token=xxx

步骤5:业务后端完成本地登录用户浏览器跳转到业务前端A域名下的/auth/wechat/finalize页面。该页面(或对应的后端接口)使用relay_token向业务后端A的API发起请求(或直接由后端处理),换取openid,完成本地用户查找/创建,并设置本地的登录Cookie或Session。

步骤6:最终跳转业务后端A返回302重定向到最初的final_redirect_urihttps://www.a.com/dashboard)。用户浏览器跳转回去,此时已携带本地登录凭证,页面显示用户已登录。

整个流程中,用户感知到的跳转是:www.a.com->微信授权页->www.a.com(登录成功)。oauth-relay.yourcompany.com这个域名对用户是完全透明的,体验上与直接使用微信授权无异。

5. 常见问题与排查技巧实录

这套方案在线上运行,会遇到各种意想不到的问题。下面是我们踩过的一些坑和对应的解决方案。

5.1 State参数无效或过期

问题现象:用户在微信授权后,跳转回业务页面时提示“state无效”或“state已过期”。

  • 可能原因1:State有效期太短。业务后端生成state存入Redis,设置5分钟过期。如果用户在微信授权页面停留时间过长(比如扫码后去忙别的事了),回来时state可能已失效。
    • 解决:适当延长state缓存时间,例如10-15分钟。同时,在前端发起授权时,可以增加一个加载动画或提示,告诉用户操作需在一定时间内完成。
  • 可能原因2:State被重复使用。中继服务回调业务后端时,业务后端需要验证state是否已被消费过。如果网络超时导致业务后端未正确处理,但中继服务已重定向用户,用户刷新页面可能导致携带相同code和state重试,触发防重放机制。
    • 解决:业务后端验证state使用记录时,采用“获取并删除”(GET and DELETE)的原子操作,确保一个state只能被成功消费一次。即使第一次处理失败,这个state也应标记为已使用,避免重复创建用户会话。对于处理失败的情况,应引导用户重新发起授权流程。
  • 可能原因3:State加解密密钥不一致。业务后端加密state用的密钥,和中继服务解密用的密钥不一致。在多服务部署时,如果配置同步出现问题,就会导致此错误。
    • 解决:将加解密密钥放到统一的配置中心(如Apollo, Nacos),确保所有实例获取的密钥一致。并建立配置变更的监控告警。

5.2 中继服务成为性能瓶颈或单点故障

问题现象:所有微信登录请求都经过中继服务,高峰期响应变慢,或者中继服务宕机导致全站微信登录不可用。

  • 可能原因:中继服务是单点部署,或资源不足。
    • 解决
      1. 水平扩展:中继服务本身应设计为无状态,方便水平扩容。前面用负载均衡器(如ELB, SLB)分发流量。
      2. 缓存微信AccessToken:中继服务用code换access_token是调用微信API,有频率限制。可以用appid+openidappid为key,将换取的access_token缓存起来(注意有效期,通常为2小时)。这样同一用户短时间内多次授权(如不同业务端),可以避免重复向微信换token,提升速度并减少微信API调用。
      3. 优雅降级与熔断:在中继服务调用业务后端投递消息时,如果某个业务后端持续超时或失败,应触发熔断机制,暂时停止向该后端投递,并记录日志告警。同时,中继服务应具备降级能力,例如在无法联系业务后端时,可以将用户重定向到一个统一的“服务繁忙”页面,而不是一直转圈圈。
      4. 监控与告警:对中继服务的QPS、响应时间、错误率进行监控。对Redis缓存状态、微信API调用成功率进行监控。

5.3 用户信息投递失败

问题现象:微信授权成功,但用户最终没有登录成功,业务页面显示未登录。日志显示中继服务调用业务后端回调接口超时或返回错误。

  • 排查步骤
    1. 检查网络连通性:确认中继服务所在网络能否访问业务后端的内网地址或公网地址。
    2. 检查接口协议:确认业务后端的回调接口(/auth/wechat/callback)是否存在,方法(POST/GET)是否正确,请求路径是否匹配。
    3. 检查签名验证:如果通信使用了签名,检查中继服务生成的签名和业务后端验证的算法、密钥、参数拼接顺序是否完全一致。这里极易出错,建议编写详细的单元测试和集成测试来保证。
    4. 检查业务后端负载:业务后端服务可能因为负载过高,无法及时处理回调请求。需要监控业务后端的CPU、内存和接口响应时间。
    5. 查看业务日志:在中继服务投递payload时,将关键信息(如target_app,openid,state)记录到日志中。在业务后端的回调接口里,也详细记录接收到的参数和处理结果。通过日志链可以快速定位问题发生在哪一环。

5.4 开放重定向漏洞风险

风险:如果final_redirect_uri参数没有经过严格校验,攻击者可能构造一个恶意state,将用户授权后的跳转指向钓鱼网站。

  • 防护措施
    1. 业务后端校验:在业务后端生成state时,应对final_redirect_uri进行白名单校验。只允许跳转到本应用已知的、安全的域名和路径下。例如,对于app_a,只允许跳转到https://www.a.comhttps://m.a.com下的页面。
    2. 中继服务二次校验(可选):中继服务在解析state后,也可以根据target_app配置一个对应的域名白名单,对final_redirect_uri进行校验。这提供了双重保险。
    3. 使用相对路径:尽可能让final_redirect_uri使用相对路径(如/dashboard),由业务后端在最终跳转时拼接上自己的基础域名。

5.5 多公众号/小程序的支持

需求:一个中继服务可能需要对接多个不同的微信公众号或小程序。

  • 实现:我们在中继服务中设计了一个WechatConfigService,它根据请求中的app_id来获取对应的配置(AppSecret等)。这些配置可以存储在数据库里。关键是要确保每个公众号/小程序的“网页授权域名”都配置为同一个中继服务域名。在跳转到微信授权时,appid参数使用对应公众号的即可。

一个线上问题的真实案例:我们有一次上线后,发现部分用户登录后跳转到了错误的业务域名。排查发现,是Redis缓存中target_app到后端地址的映射配置,在发布时被旧版本覆盖,导致app_a错误地指向了app_b的回调地址。教训:配置信息,特别是路由映射,必须有严格的发布流程和回滚机制,变更后要立刻进行功能验证。