docs/examples: 扫码登录改为官方 POST+local_token_list+iLink-App-Id/ClientVersion 头 + 完整状态机 + 换码竞态警告
This commit is contained in:
1 parent
38b776705d
commit
8a36915377
6 files changed
+566
-96
No files matched your search
@@ -5,6 +5,7 @@
|
||||
// 注意: 为保持零依赖,JSON 解析用最朴素的正则提取,仅适合演示。
|
||||
// 正式项目请换 Jackson / Gson,并按 docs/04 的模块划分组织代码。
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
@@ -17,6 +18,8 @@ import java.util.regex.*;
|
||||
|
||||
public class IlinkBridge {
|
||||
static final String BASE_URL = "https://ilinkai.weixin.qq.com";
|
||||
// 扫码轮询的 base;收到 scaned_but_redirect 时切换,其余 API 仍走固定 BASE_URL。
|
||||
static String apiBaseUrl = BASE_URL;
|
||||
static final Path STATE_FILE = Path.of("bridge-state.json");
|
||||
|
||||
// ---------- 状态: bot_token / 游标 / context_token(极简 key=value 存储) ----------
|
||||
@@ -33,14 +36,19 @@ public class IlinkBridge {
|
||||
STATE_FILE.toFile().setReadable(true, true); // 近似 600
|
||||
}
|
||||
|
||||
// ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN ----------
|
||||
// ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN + iLink-App 标识 ----------
|
||||
// 注: get_bot_qrcode / get_qrcode_status(预绑定阶段)无 token 时不带 Authorization,
|
||||
// 但 iLink-App-Id / iLink-App-ClientVersion / AuthorizationType / X-WECHAT-UIN 恒带。
|
||||
static HttpRequest.Builder req(String url) {
|
||||
var b = HttpRequest.newBuilder(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(50))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("AuthorizationType", "ilink_bot_token")
|
||||
.header("X-WECHAT-UIN", Base64.getEncoder().encodeToString(
|
||||
Integer.toString(new SecureRandom().nextInt() >>> 1).getBytes()));
|
||||
Integer.toString(new SecureRandom().nextInt() >>> 1).getBytes()))
|
||||
.header("iLink-App-Id", "bot")
|
||||
// 2.4.8 -> (2&0xff)<<16 | (4&0xff)<<8 | (8&0xff) = 132104(数字字符串,非 "1")
|
||||
.header("iLink-App-ClientVersion", "132104");
|
||||
String token = state.getProperty("bot_token");
|
||||
if (token != null) b.header("Authorization", "Bearer " + token);
|
||||
return b;
|
||||
@@ -68,23 +76,85 @@ public class IlinkBridge {
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ----------
|
||||
// ---------- 接口1: POST /ilink/bot/get_bot_qrcode 获取登录二维码(官方姿势) ----------
|
||||
static String fetchQrcode() throws Exception {
|
||||
// 官方为 POST + body {"local_token_list":[...]},非裸 GET。首次绑定传空数组;
|
||||
// 若已持久化其它 bot_token 可收集传入(最多 10 个),官方据此识别触发 binded_redirect。
|
||||
return post("/ilink/bot/get_bot_qrcode?bot_type=3", "{\"local_token_list\":[]}");
|
||||
}
|
||||
|
||||
// ---------- 接口2: GET /ilink/bot/get_qrcode_status 轮询扫码状态(完整状态机) ----------
|
||||
static void login() throws Exception {
|
||||
var qr = client.send(req(BASE_URL + "/ilink/bot/get_bot_qrcode?bot_type=3")
|
||||
.GET().build(), HttpResponse.BodyHandlers.ofString()).body();
|
||||
// 状态机: wait / scaned / scaned_but_redirect / need_verifycode /
|
||||
// verify_code_blocked / binded_redirect / confirmed / expired
|
||||
// 两大坑(务必读 docs/02 §4.5):
|
||||
// ① 码服务端 TTL≈60s,生成后须在窗口内扫(超窗服务端自返回 expired,此处换新码)。
|
||||
// ② 「换码竞态」:一旦收到 scaned(或 scaned_but_redirect / need_verifycode),
|
||||
// 绝不能再换码——换码会砍掉"扫码→确认"会话,微信端显示成功但后端永远收不到 confirmed。
|
||||
var qr = fetchQrcode();
|
||||
String qrcode = pick(qr, "qrcode");
|
||||
if (qrcode == null) throw new RuntimeException("获取二维码失败");
|
||||
System.out.println("请用微信扫码: " + pick(qr, "qrcode_img_content"));
|
||||
System.out.println("(二维码内容即上面的 liteapp URL,请用二维码库编码展示;勿把 qrcode 字段当二维码)");
|
||||
long deadline = System.currentTimeMillis() + 8 * 60_000;
|
||||
String verifyCode = null;
|
||||
var sc = new Scanner(System.in);
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
var s = client.send(req(BASE_URL + "/ilink/bot/get_qrcode_status?qrcode=" + qrcode)
|
||||
.header("iLink-App-ClientVersion", "1").GET().build(),
|
||||
String path = "/ilink/bot/get_qrcode_status?qrcode="
|
||||
+ URLEncoder.encode(qrcode, StandardCharsets.UTF_8);
|
||||
if (verifyCode != null)
|
||||
path += "&verify_code=" + URLEncoder.encode(verifyCode, StandardCharsets.UTF_8);
|
||||
String s = client.send(req(apiBaseUrl + path).GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString()).body();
|
||||
if ("confirmed".equals(pick(s, "status"))) { // wait → scaned → confirmed
|
||||
state.setProperty("bot_token", pick(s, "bot_token"));
|
||||
state.setProperty("bot_id", pick(s, "ilink_bot_id"));
|
||||
saveState();
|
||||
System.out.println("登录成功, bot: " + state.getProperty("bot_id"));
|
||||
return;
|
||||
String status = pick(s, "status");
|
||||
switch (status == null ? "" : status) {
|
||||
case "wait":
|
||||
System.out.println("等待扫码...");
|
||||
break;
|
||||
case "scaned":
|
||||
verifyCode = null;
|
||||
System.out.println("已扫码,请在手机上确认...(此后绝不换码,避免砍掉确认会话)");
|
||||
break;
|
||||
case "scaned_but_redirect":
|
||||
verifyCode = null;
|
||||
String host = pick(s, "redirect_host");
|
||||
if (host != null && !host.isEmpty()) {
|
||||
apiBaseUrl = "https://" + host;
|
||||
System.out.println("IDC 重定向,切换轮询至 " + apiBaseUrl);
|
||||
}
|
||||
break;
|
||||
case "need_verifycode":
|
||||
System.out.print("请在手机微信上输入显示的数字后回车: ");
|
||||
System.out.flush();
|
||||
verifyCode = sc.nextLine().trim();
|
||||
continue;
|
||||
case "verify_code_blocked":
|
||||
verifyCode = null;
|
||||
System.out.println("配对码多次输入错误,重新获取二维码");
|
||||
qr = fetchQrcode();
|
||||
qrcode = pick(qr, "qrcode");
|
||||
System.out.println("新二维码: " + pick(qr, "qrcode_img_content"));
|
||||
continue;
|
||||
case "binded_redirect":
|
||||
System.out.println("该 bot 已绑定过本实例,视为连接成功");
|
||||
return;
|
||||
case "confirmed":
|
||||
state.setProperty("bot_token", pick(s, "bot_token"));
|
||||
state.setProperty("bot_id", pick(s, "ilink_bot_id"));
|
||||
if (pick(s, "baseurl") != null) state.setProperty("baseurl", pick(s, "baseurl"));
|
||||
saveState();
|
||||
System.out.println("登录成功, bot: " + state.getProperty("bot_id"));
|
||||
return;
|
||||
case "expired":
|
||||
// 码过期(服务端作废)→ 换新码;仅在 expired 时换,scaned 后绝不换
|
||||
verifyCode = null;
|
||||
System.out.println("二维码已过期,重新获取");
|
||||
qr = fetchQrcode();
|
||||
qrcode = pick(qr, "qrcode");
|
||||
System.out.println("新二维码: " + pick(qr, "qrcode_img_content"));
|
||||
continue;
|
||||
default:
|
||||
System.out.println("未知状态: " + status);
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user