fix: 小程序服务号绑定先打开门户域名再跳转微信授权。

web-view 不能直接打开 open.weixin.qq.com,移动端改为打开已配置的业务域名中转页。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-10-29 01:44:40 +08:00
1 parent 459b820f95
commit e7c1762113
7 files changed
+137 -8

No files matched your search

+14 -1
View File
@@ -1,5 +1,13 @@
import { resolveApiBase, resolveWebOrigin } from '@/config/api'
import { request, parseResponseJson } from '@/utils/request'
const LAUNCH_PATH = '/api/public/wechat-notices/oauth/launch'
function httpsHost(raw) {
const match = String(raw || '').match(/^https:\/\/([^/?#]+)/i)
return match ? match[1].toLowerCase() : ''
}
export async function fetchWechatNoticeStatus() {
const response = await request({
url: '/web/wechat-notices/binding',
@@ -27,7 +35,12 @@ async function bindingRequest(method, path, data) {
}
export function isWechatAuthorizeUrl(value) {
return typeof value === 'string' && /^https:\/\/open\.weixin\.qq\.com\/connect\/oauth2\/authorize\?[^\s]+$/.test(value)
// web-view 只能打开业务域名。这里只接受门户上的授权中转页,不直接打开 open.weixin.qq.com。
if (typeof value !== 'string' || value.length > 300) return false
const match = value.match(/^https:\/\/([^/?#@]+)(\/[^?#]*)\?state=([A-Za-z0-9_-]{20,100})$/)
if (!match || match[2] !== LAUNCH_PATH) return false
const allowed = new Set([httpsHost(resolveWebOrigin()), httpsHost(resolveApiBase())].filter(Boolean))
return allowed.has(match[1].toLowerCase())
}
export async function startWechatBinding() {
@@ -8,16 +8,22 @@ const { parse, compileTemplate } = require('@vue/compiler-sfc')
const read = p => readFileSync(new URL(p, import.meta.url), 'utf8')
const strip = s => s.replace(/^import[^\n]+\n/gm, '').replace(/^export /gm, '')
const launchState = 'a'.repeat(32)
const launchUrl = `https://w.shanxiachem.com/api/public/wechat-notices/oauth/launch?state=${launchState}`
function api(data, statusCode = 200, code = 200) {
const calls = []
const source = strip(read('../api/wechat-notices.js'))
const fns = new Function('request', 'parseResponseJson', `${source}; return { startWechatBinding, fetchWechatBindingSession, confirmWechatBinding, isWechatAuthorizeUrl }`)(
async options => { calls.push(options); return { statusCode, data: { code, data, message: '请求失败' } } }, response => response.data)
const fns = new Function('request', 'parseResponseJson', 'resolveWebOrigin', 'resolveApiBase', `${source}; return { startWechatBinding, fetchWechatBindingSession, confirmWechatBinding, isWechatAuthorizeUrl }`)(
async options => { calls.push(options); return { statusCode, data: { code, data, message: '请求失败' } } },
response => response.data,
() => 'https://w.shanxiachem.com',
() => 'https://w.shanxiachem.com/api')
return { ...fns, calls }
}
test('binding API uses owner-authenticated endpoints and only returns necessary fields', async () => {
const start = api({ sessionId: 'a'.repeat(32), authorizeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?state=state#wechat_redirect', openid: 'private' })
const start = api({ sessionId: 'a'.repeat(32), authorizeUrl: launchUrl, openid: 'private' })
assert.equal((await start.startWechatBinding()).openid, undefined)
assert.equal(start.calls[0].method, 'POST')
assert.deepEqual(start.calls[0].data, { mode: 'mobile' })
@@ -32,7 +38,13 @@ test('binding API uses owner-authenticated endpoints and only returns necessary
test('binding API rejects foreign URLs, failed and malformed results', async () => {
const fns = api({})
for (const url of ['javascript:alert(1)', 'http://open.weixin.qq.com/connect/oauth2/authorize?state=x', 'https://open.weixin.qq.com.evil.test/connect/oauth2/authorize?state=x']) {
for (const url of [
'javascript:alert(1)',
'http://open.weixin.qq.com/connect/oauth2/authorize?state=x',
'https://open.weixin.qq.com/connect/oauth2/authorize?state=' + launchState,
'https://evil.test/api/public/wechat-notices/oauth/launch?state=' + launchState,
launchUrl + '#wechat_redirect'
]) {
assert.equal(fns.isWechatAuthorizeUrl(url), false)
await assert.rejects(api({ sessionId: 'x'.repeat(32), authorizeUrl: url }).startWechatBinding())
}
@@ -53,7 +65,7 @@ test('authorization page mounts approved URL after ready and unregisters its cha
const page = new Function(...Object.keys(deps), `${body}; return { src, errorText, onError }`)(...Object.values(deps))
hooks.load({ url: 'https://evil.test' })
assert.equal(page.src.value, '')
const url = 'https://open.weixin.qq.com/connect/oauth2/authorize?state=xyz'
const url = launchUrl
handlers.wechatBindingAuthorize({ authorizeUrl: url })
assert.equal(page.src.value, '')
hooks.ready()