微信公众号自定义菜单配置的跳转链接如果指向与微信服务不同源的业务H5站点,用户在微信内点击菜单后,浏览器会从业务域名发起资源与接口请求。此时业务系统往往依赖Cookie或本地Token判断登录态,但微信环境的特殊性和浏览器的同源策略,使得这些凭证无法被跨域读取,单点登录随之失效。要解决这个问题,需要从身份颁发、请求代理和票据校验三个层面重新设计登录链路。

跨域导致单点登录失效的底层原理
浏览器的同源策略规定,协议、域名、端口三者任意不同即视为跨域。微信公众号菜单跳转的H5页面通常部署在https://h5.ipipp.com,而接口服务可能在https://api.ipipp.com,两者域名不同,属于典型跨域。当页面通过XMLHttpRequest或fetch调用接口时,若接口未正确返回Access-Control-Allow-Origin等CORS头,浏览器会直接拦截响应。更重要的是,即使接口允许跨域,默认情况下跨域请求的withCredentials不开启,Cookie也不会自动携带,导致服务端无法识别用户会话。
微信内嵌浏览器(X5或微信自带的WebView)同样遵循这套规则。很多开发者误以为微信会自动注入登录态,实际上微信仅能在自己的域名下保持登录,跳转出去后业务域完全没有用户标识。如果采用传统的服务端Session加Cookie方案,跨域后SessionID无法传递,后端拿不到会话,就会把用户当作未登录重定向到登录页,单点登录名存实亡。理解这一点,才能明白为什么简单的AJAX跨域配置不足以解决问题。
此外,单点登录的核心是要让多个系统信任同一个身份凭证。在跨域场景中,凭证不能依赖浏览器自动携带,而应由前端主动传递或由后端中转。例如使用Token时,若Token存放在业务域A的localStorage中,域B的页面脚本因同源限制根本读不到该值。因此必须借助统一身份层或后端代理,把凭证的校验与发放从浏览器隔离,放到服务端完成,才能规避跨域读取限制。
基于OAuth2与后端代理的跨域单点登录方案
一种稳妥的做法是利用微信网页授权(OAuth2授权码模式)作为统一身份入口。用户点击菜单进入H5时,若未发现有效会话,前端将用户重定向到微信的https://open.weixin.qq.com/connect/oauth2/authorize地址,微信回跳业务页并携带code参数。业务后端用code换取openid和access_token,再在自己域内种下登录Cookie或签发JWT,这样登录态始终在业务域内部建立,不存在跨域读Cookie的问题。
当H5页面还需要调用其他域的微服务接口时,可以部署一个统一网关做反向代理。例如将/api/*请求代理到api.ipipp.com,对浏览器而言请求仍是同域,由网关在服务端完成跨域转发并附带内部凭证。下面是一段Node.js的代理示例,展示如何避免浏览器跨域:
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 将前端的 /api 请求代理到后端域,对浏览器透明
app.use('/api', createProxyMiddleware({
target: 'https://api.ipipp.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
onProxyReq: (proxyReq, req) => {
// 服务端附加内部鉴权头,不依赖浏览器Cookie跨域
proxyReq.setHeader('X-Internal-Token', 'internal_secret_value');
}
}));
app.listen(3000, () => console.log('网关已启动'));
这种代理方案的优势是前端代码完全不用处理跨域逻辑,所有接口看起来都是同域调用。缺点是网关成为单点,需要做好鉴权与限流。另一种轻量做法是后端签发一次性票据(ticket):用户在某域登录后,后端生成短期有效的ticket并通过URL传给目标域,目标域后端用ticket换正式Token并种下本域Cookie。该方式避免了凭证跨域传输,又不需要长期代理,适合菜单跳转这类一次性跨域进入场景。
票据校验与CORS策略的安全实践
若采用ticket跨域登录,必须保证ticket只能使用一次且有效期极短(如60秒)。目标域后端接收到ticket后,立即请求身份服务校验并销毁该ticket,防止中间人截获复用。同时,H5页面若仍需直接调用跨域接口(非代理),应在接口网关精确配置CORS白名单,而不是简单地返回Access-Control-Allow-Origin: *,尤其当接口涉及用户数据时,宽松的CORS会放大被盗用风险。
下面是一段Java Servlet中安全配置CORS的示例,仅允许指定H5域并支持凭证:
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) {
String origin = req.getHeader("Origin");
if ("https://h5.ipipp.com".equals(origin)) {
resp.setHeader("Access-Control-Allow-Origin", origin);
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Methods", "GET,POST");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
}
resp.setStatus(HttpServletResponse.SC_OK);
}
值得注意的是,当Access-Control-Allow-Credentials为true时,Access-Control-Allow-Origin不能为通配符,必须明确写出前端域,否则浏览器拒绝发送Cookie。配合前面提到的ticket或代理方案,微信菜单跳转H5的跨域单点登录就能在安全和体验之间取得平衡。最终用户从菜单进入后无感知完成登录,后端各域通过服务间信任而非浏览器跨域共享来维持身份一致性。