|
昨天是周末,在家闲得无聊,于是去weiphone逛了一圈,偶然发现有人发了一帖叫《微信 for Mac》,这勾起了我的好奇心,国内做Mac开发的人确实很少,对于那些能够独自开发一些Mac第三方工具的开发者我都表示很敬畏,于是点进去看了一个究竟,如果你们好奇也可以点进去看个明白,我最终得出的结论就是:坑爹呢这是!直接用一个WebView去加载了wx.qq.com这个网页也敢自称是微信For Mac?对于这种欺骗用户的行为我十分不屑,同时也让我在思考在微信不提供API的环境下开发一款原生的微信Mac版本是否可行,最有可能的就是去分析微信Web版本的通信过程,然后在程序中模拟这个流程,在我苦苦研究了一个下午之后,终于摸透了这个过程,并用程序实现了大部分功能,下面就详细解说一下微信Web版的流程:
1 W0 i4 a K- {$ ^* [0 Q! w l$ W
1.微信服务器返回一个会话ID
% ]# l7 I# P4 Y4 b. h) h/ ~- K8 o, H- k, E" u
微信Web版本不使用用户名和密码登录,而是采用二维码登录,所以服务器需要首先分配一个唯一的会话ID,用来标识当前的一次登录,通过请求地址:* k) \; d( A# U; k4 B
% V) M6 G% A2 b6 G" k
https://login.weixin.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1377482012272(其中1377482012272这个值是当前距离林威治标准时间的毫秒)
2 M6 h; P5 F* _7 T2 Q# c& c j8 ?% x
) j4 _& F, o3 _4 O1 p1 ^服务器会返回如下的字符串:
' b! h) {' z, ^4 Z
8 ~3 N7 F; R) l9 o# Gwindow.QRLogin.code = 200; window.QRLogin.uuid = “DeA6idundY9VKn”;
) K" z" ~0 Z! C H7 |) i' F& s; X7 }
而这个DeA6idundY9VKn字符串就是微信服务器返回给我们的ID。
! |" A1 j- E+ f
+ M, |6 [* E ]2.通过会话ID获得二维码
5 `0 p# ?7 f9 _
* q7 u% `# s9 l7 z3 T5 g既然微信Web版本是通过二维码进行登录,如何获得这个随机的二维码呢?答案就是利用刚才获得的ID去请求服务器生成的二维码,通过上面的ID我们组合得到以下的URL地址:$ r; G4 P( j1 \+ {
# D/ |3 s/ N6 G$ {4 z
https://login.weixin.qq.com/qrcode/DeA6idundY9VKn?t=webwx) B+ e/ N1 ?, N* a6 [
- @3 L8 l& @; ?& x' J0 f该请求返回的便是我们需要的二维码,此时需要用户在微信的手机版本中扫描这个二维码(我就搞不明白微信官方是如何想的,登录Web版本竟然还需要手机微信去配合登录,难道没有考虑我被迫选择Web微信就是因为手机不在身边这样的情形么?)
0 t4 s- ~; Z' `0 x* E. X* u2 p h3 v
3.轮询手机端是否已经扫描二维码并确认在Web端登录& V, L; p$ H' M* ?# u
, s! z ^2 g7 e% c1 W. w
当获得二维码之后,就需要用户去手机端去扫描二维码,并获得用户的授权,此时我们并不知道用户何时完成这个操作,所以我们只有轮询,而轮询的地址就是:
! R2 t, w! X4 F3 S4 R
" @ ^) d7 S# M5 k1 A& |( w9 Yhttps://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?uuid=DeA6idundY9VKn&tip=1&_=1377482045264(注意UUID和最后时间这两个参数)4 J% a- Z" I1 E/ g6 u _
3 J% ]) R* }8 B8 p/ W4 ~& M
如果服务器返回:9 C' O5 i5 J d/ O+ `7 N$ P3 L
' n/ }: @0 Y5 |; ~$ m) X& w
window.code=201;4 f& W: }+ U: Z8 S' d1 K
4 N; b, o" U5 H则说明此时用户在手机端已经完成扫描,但还没有点击确认;3 T7 s2 _8 d% I" O
; O# A5 } W! p% z& z# i |
如果服务器返回:
; i0 w& u E7 P0 T* M6 d
8 h. D P; q7 S, Lwindow.redirect_uri=一个URL地址( {: J/ j$ X4 r u' b! L k* Z" Q
; M: ]4 F! y) x& q9 n& V( I5 V则说明此时用户已经在手机端完成了授权过程,保存下这个URL地址下一步骤中使用。 \& x# g& v1 W8 K. m
# W$ c, t+ M: _* h4.访问登录地址,获得uin和sid
+ X4 B9 h' x9 f( Y2 [2 z
! G8 t; D2 K3 U通过访问上一步骤中获得的URL地址,可以在服务器返回的Cookies中获得到wxuin和wxsid这两个值,这两值在后续的通信过程中都要使用到这两个值,并且Cookies中也需要包括这两项。
, {: g2 a+ I H+ ~+ i% v4 W2 R
" R, r+ x9 B9 g0 K! Z2 G0 I9 Y8 D5.初使化微信信息) }/ `7 L z9 g# t
' Z5 Q4 }" I+ o5 u前面的步骤算是完成了这个复杂的登录过程,如果我们需要使用微信就需要获得当前用户的信息、好友列表等,还有一个关键的就是同步信息(后续与服务器轮询中需要使用同步信息),通过访问以下的链接:6 t. }) L2 J5 K
. G3 P. _8 g! C* J. X
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=1377482058764(r依然是时间)
0 {2 `9 ^6 Q8 F1 t& |/ r6 B6 M5 F/ A2 T0 s
访问该链接需要使用POST,并且在Body中带上以下的JSON信息:
: l. L/ n+ u& t
! ~1 x- u" ?5 U* _# E1) J: J2 H8 S' g/ P+ p$ p
2( f. q8 K0 O3 E' E
{"BaseRequest":
* _4 o; b# I4 c4 o* o8 V# H{"Uin":"2545437902","Sid":"QfLp+Z+FePzvOFoG","Skey":"","DeviceID":"e1615250492"}}
3 _1 V' C; s: B# n$ r J; B这个JSON串中Uin和Sid分别是上面步骤中获得的那两个Cookie值,DeviceID是一个本地生成的随机字符串(分析了官方的总是e+一串数字,所以我们也保持这样的格式)。
& E3 D+ X( [+ B8 k
& t( I0 B) i1 C. i) ~/ K' ^服务器就会返回一个很长的JSON串,这其中包括:BaseResponse中的值用来表示请求状态码,ContactList主要用来表示联系人(此列表不全,只包括了类似通讯录助手、文件助手、微信团队和一些公众帐号等,后面会通过另一接口去获得更全面的信息),SyncKey是用户与服务器同步的信息,User就是当前登录用户自己的信息。2 O' _9 {% ^* W0 u& ]
o- T- G7 k; s* r( i8 ?5 K
6.获得所有的好友列表
- }8 M& P: w6 g( Q- n7 \; s t
4 `& A2 s3 a/ [# P在上一步骤中已经获得了部分好友和公众帐号,如果需要获得完整的好友信息,就需要访问以下的链接:
( \4 t7 b6 v7 e+ y/ x: Y* T8 _0 {" C. @# |
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?r=1377482079876(r依然是时间)
' U; F ?1 o& c8 e7 x$ e* W$ a
( k' K% c) c! H) `- a" W' f访问该链接同样需要POST方式,但Body为空JSON:{},服务器对身份的判定是通过Cookies,所以需要保持之前访问的Cookies不被修改(在Objective-C中会自动保存相关的Cookies,无需程序特殊处理),在返回的JSON串中,MemberList中就包含了所有的好友信息。# r" Y! B- U# U% Y; G+ D$ D# `
& r0 G0 a5 [1 {' S8 j' E# g* x) P7.保持与服务器的信息同步% \2 a$ U: o' j" F! s
% X, {+ g% [# C5 y
与服务器保持同步需要在客户端做轮询,该轮询的URL如下:4 Q6 N8 ]) |% x0 d1 t
9 k8 g% O# E5 Chttps://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?callback=jQuery18309326978388708085_1377482079946&r=1377482079876&6 l# t- ?' U4 v4 b2 J" W; C4 ]
sid=QfLp+Z+FePzvOFoG&uin=2545437902&deviceid=e1615250492&synckey=(见以下说明)&_=1377482079876
" A6 @* |7 {: m& c- J
+ z# s8 J# t2 u% k4 z其中的参数r和_都是time,sid,uin,deviceid与上面步骤的值相对应,此处的synkey是上步步骤获得的同步键值,但需要按一定的规则组合成以下的字符串:: K k5 o- {$ M: D7 P
0 P+ X \) B8 `3 h; l5 ?" Z
1_124125|2_452346345|3_65476547|1000_5643635& x1 I( P8 S' z1 N& {5 T& C
& e2 H( D+ f, ^就是将键和值用_隔开,不同的键值对用|隔开,但记得|需要URL编码成%7C,通过访问上面的地址,会返回如下的字符串:, [" w/ L" |& a- q) r
& j g4 \# ]! W! A9 d! v
window.synccheck={retcode:”0”,selector:”0”}2 P: B$ s v. b3 ^
) ^2 y! }4 `; e6 O" ^. D' W6 S如果retcode中的值不为0,则说明与服务器的通信有问题了,但具体问题我就无法预测了,selector中的值表示客户端需要作出的处理,目前已经知道当为6的时候表示有消息来了,就需要去访问另一个接口获得新的消息。
1 t3 J% ?0 e& `# v1 a0 i0 ]% r6 o/ T% ~
8.获得别人发来的消息$ k' W& L4 L3 r; y* ?( f7 z
9 h0 D% |7 `" }5 A0 ^7 W0 R9 i* `
当一个步骤中知道有新消息时,就需要去获取消息内容,通过访问以下的链接:7 q4 @/ j, i% H" _* m" E7 @ x
; V- n" y, \+ W8 J1 [1 R4 {
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=QfLp+Z+FePzvOFoG&r=1377482079876
. G+ }( e( O: z$ F! u/ r
9 X0 k N9 @. q( L上面链接中的参数sid对应上面步骤中的值,r为时间,访问链接需要使用POST方式,Body中包括JSON串,该JSON串格式如下:" U$ m8 }% c3 i$ `; [( M8 }. |
1 `6 K* W+ k3 o
12 ~+ F7 |: ]8 f$ m
29 U* F( I; m" [1 ] X* t
3
1 _2 d! X& B6 M' D: t{"BaseRequest" : {"Uin":2545437902,"Sid":"QfLp+Z+FePzvOFoG"},
3 \' i7 X) _$ ]) ^3 W"SyncKey" : {"Count":4,"List":[{"Key":1,"Val":620310295},{"Key":2,"Val":620310303},{"Key":3,"Val":620310285},{"Key":1000,"Val":1377479086}]},3 m; }( J( f% A, T0 M) F
"rr" :1377482079876};
/ L- l, \: h. M4 c5 ?以下的信息中BaseRequest中包括的Uin与Sid与上面步骤中的值对应,SyncKey也是上面步骤中获得的同步键值对,rr为时间,访问成功之后服务器会返回一个JSON串,其中AddMsgList中是一个数组,包含了所有新消息。$ K, a* [6 x4 Q5 A5 I
% `5 }" d! D, E: [. X- m9.向用户发送消息
' h5 ~ ^3 ^; } J t" @& _/ a0 W% w, G! y0 i+ n
用户主动发送消息,通过以下的URL地址:
8 J+ ]9 q* [: d6 \+ {2 H/ |https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?sid=QfLp+Z+FePzvOFoG&r=13774820798766 K" ]- @) I) d+ s: U% C, ]. r
上面的sid和r参数不再解释了,访问该URL采用POST方式,在Body中的JSON串形如以下的格式:. \# {1 Z$ V! ^! k+ { F' @* @$ v' w. H
+ S# j) o; b" T6 d0 P9 X1
$ m' d) R7 Q/ o' y4 J) w2. o6 I7 p% G& [* x8 r Q7 d7 f# Y
3
; U# f* E* P6 ~/ L2 C: j4$ o; g% ?2 R, L, u3 K
55 ^/ k2 C: i" k$ J: ]* {
6
! D* d5 P1 g% A$ k+ {7
& P2 c- p8 F" w" ^) n3 f8. c( T* O8 S! V+ r l* \2 j
9
@4 L. w% {0 I0 ?+ ^- w' W10! q7 ~0 {* c9 l/ }, n
11
/ s( A Y D; r x12& K4 X* F0 Q" w( c
13
& q1 N4 ^3 ?$ b- T9 K. \9 |148 \- f4 Y. i; P+ `
15
8 c/ w V3 z9 T) a: z16
8 Q* E" f" q k9 H+ a17
: R, U; P$ [# @7 Y% R! K }( O{' r7 u; `+ s3 V& p, o
"BaseRequest":{
/ T& @( g! u' H: l. d "DeviceID" : "e441551176",
9 ` U7 }1 E9 {* u: H4 | "Sid" : "S8wNi91Zry3024eg",
# g# t7 U' U7 B) r "Skey" : "F820928BBA5D8ECA23448F076D2E8A915E1349E9FB4F4332",
2 f/ @( p5 W" F/ g! ^1 g5 @ "Uin" : "2545437902"6 i! N4 }5 p h, A
}, z* @+ F0 I& y4 v9 c7 _2 K0 v: V
"Msg" : {
5 u& c' J' O8 \( p1 Y2 M5 ] "ClientMsgId" : 1377504862158,
+ Z8 C" ^/ M3 v' d2 I2 `, z "Content" : "hello",
( w* G5 O4 M7 g# W% s "FromUserName" : "wxid_2rrz8g8ezuox22",
3 d: x6 N8 R2 g: O "LocalID" : 1377504862158,5 J: R5 ^. D1 v2 [ N
"ToUserName" : "wxid_j4nu420ojhsr21",, ], I& Z' f' p: i; Z* i, K4 Y/ R
"Type" : 1
. l. y5 E h8 D) U: J$ t6 `' c },( A: P6 c7 O3 G2 k7 G/ d7 }
"rr" = 1377504864463
4 @' O) w$ i6 P8 J1 c: p9 I6 l( x}4 f& e2 T( H8 H; ~ g1 |( `
其中BaseRequest都是授权相关的值,与上面的步骤中的值对应,Msg是对消息的描述,包括了发送人与接收人,消息内容,消息的类型(1为文本),ClientMsgId和LocalID由本地生成。rr可用当前的时间。 i$ Z& T. e) a
在返回JSON结果中BaseResponse描述了发送情况,Ret为0表示发送成功。
7 q4 ]3 T! C. e) V
3 A% d7 ?+ W7 n2 v+ J& k8 v1 x8 l6 _ |
|