feat(auth): add WeChat mini program login endpoint

Add jscode2session exchange, CSRF bypass for servicewechat.com,
and openid-based auto register/login for the mini program client.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-11 12:41:37 +08:00
parent fe6d631ba1
commit 04ef1c9105
5 changed files with 221 additions and 0 deletions
+54
View File
@@ -172,6 +172,7 @@ import {
WECHAT_NOTIFY_SUCCESS_V2,
} from './wechat-pay.mjs';
import { createWechatOAuthService, isWechatUserAgent, loadWechatOAuthConfig } from './wechat-oauth.mjs';
import { exchangeMiniProgramCode, loadWechatMiniappConfig } from './wechat-miniapp.mjs';
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
import { loadWechatMpModule } from './wechat-mp-loader.mjs';
import { validateWechatShareSignatureUrl } from './wechat-share.mjs';
@@ -262,6 +263,15 @@ app.use((req, res, next) => {
next();
});
function isWechatMiniProgramSource(value) {
if (!value) return false;
try {
return new URL(value).hostname.endsWith('servicewechat.com');
} catch {
return false;
}
}
function csrfOriginCheck(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const host = req.get('host');
@@ -269,6 +279,9 @@ function csrfOriginCheck(req, res, next) {
const referer = req.get('referer');
if (!origin && !referer) return next();
const requestHostname = (host ?? '').split(':')[0];
if ([origin, referer].some(isWechatMiniProgramSource)) {
return next();
}
const allowed = [origin, referer].some((value) => {
if (!value) return false;
try {
@@ -904,6 +917,47 @@ app.post('/auth/login', jsonBody, async (req, res) => {
return res.json({ authenticated: true, mode: 'legacy' });
});
app.post('/auth/wechat-miniapp/login', jsonBody, async (req, res) => {
await userAuthReady;
if (!userAuth) {
return res.status(503).json({ message: '未启用用户系统' });
}
const miniappConfig = loadWechatMiniappConfig();
if (!miniappConfig.enabled) {
return res.status(503).json({ message: '小程序登录未配置,请联系管理员' });
}
const code = typeof req.body?.code === 'string' ? req.body.code.trim() : '';
if (!code) {
return res.status(400).json({ message: '缺少微信登录 code' });
}
try {
const session = await exchangeMiniProgramCode({
appId: miniappConfig.appId,
appSecret: miniappConfig.appSecret,
code,
});
const result = await userAuth.loginByWechatMiniProgram({
appId: miniappConfig.appId,
openid: session.openid,
unionid: session.unionid,
});
if (!result.ok) {
return res.status(401).json({ message: result.message || '微信登录失败' });
}
setUserLoginCookies(res, req, result.token);
return res.json({
authenticated: true,
user: result.user,
mode: 'user',
isNewUser: Boolean(result.isNewUser),
});
} catch (err) {
const message = err instanceof Error ? err.message : '微信登录失败';
const status = err?.code === 'wechat_miniapp_code_failed' ? 401 : 400;
return res.status(status).json({ message });
}
});
app.post('/auth/register', jsonBody, async (req, res) => {
await userAuthReady;
if (!userAuth) {