|
昨天是周末,在家闲得无聊,于是去weiphone逛了一圈,偶然发现有人发了一帖叫《微信 for Mac》,这勾起了我的好奇心,国内做Mac开发的人确实很少,对于那些能够独自开发一些Mac第三方工具的开发者我都表示很敬畏,于是点进去看了一个究竟,如果你们好奇也可以点进去看个明白,我最终得出的结论就是:坑爹呢这是!直接用一个WebView去加载了wx.qq.com这个网页也敢自称是微信For Mac?对于这种欺骗用户的行为我十分不屑,同时也让我在思考在微信不提供API的环境下开发一款原生的微信Mac版本是否可行,最有可能的就是去分析微信Web版本的通信过程,然后在程序中模拟这个流程,在我苦苦研究了一个下午之后,终于摸透了这个过程,并用程序实现了大部分功能,下面就详细解说一下微信Web版的流程:; j; m: d& ]+ Q' R+ R/ Z! ]
! M0 I. S( v$ {$ t, A
1.微信服务器返回一个会话ID, @$ J7 n a" O4 ~8 `9 F! ?8 W" d
6 k6 [. E* D3 `) O5 M8 o0 [微信Web版本不使用用户名和密码登录,而是采用二维码登录,所以服务器需要首先分配一个唯一的会话ID,用来标识当前的一次登录,通过请求地址:
6 s, r3 r$ a( i- x+ t" c$ O- x$ R( \: e$ l8 p1 a) A
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这个值是当前距离林威治标准时间的毫秒)( n2 S. n2 Z8 J: k0 B: t
4 x4 m7 Q" ^. B. x* y$ z服务器会返回如下的字符串:* y3 J6 {# m3 ?. U; q3 n. e; R) A% E
9 i. x% I) h! R+ B/ j; a0 W! Ewindow.QRLogin.code = 200; window.QRLogin.uuid = “DeA6idundY9VKn”;
. W. \: [. y, g- v5 l0 v9 @) w, f/ ?. H0 C
而这个DeA6idundY9VKn字符串就是微信服务器返回给我们的ID。% G5 K9 d& v; D8 X7 M
9 r+ I7 Y+ w, M! k' \( f* _3 ^, @2.通过会话ID获得二维码& C9 A9 D: g3 l. X
' `) J( ?/ X+ r9 B! R' Q1 q既然微信Web版本是通过二维码进行登录,如何获得这个随机的二维码呢?答案就是利用刚才获得的ID去请求服务器生成的二维码,通过上面的ID我们组合得到以下的URL地址:9 x* g2 S9 a& b* u9 x. d; q
9 I8 O& ]! O& J1 x: Y
https://login.weixin.qq.com/qrcode/DeA6idundY9VKn?t=webwx; A0 N G+ J3 q8 X9 V& D8 S$ P' P
- A) G/ Z2 u! O& s7 T该请求返回的便是我们需要的二维码,此时需要用户在微信的手机版本中扫描这个二维码(我就搞不明白微信官方是如何想的,登录Web版本竟然还需要手机微信去配合登录,难道没有考虑我被迫选择Web微信就是因为手机不在身边这样的情形么?)
7 l' T* m; j: ?4 T' O8 \1 {9 j8 _: [% H2 [
3.轮询手机端是否已经扫描二维码并确认在Web端登录
5 a/ x2 V/ _0 M" ?* F) F# i0 C7 M' X2 {. X
当获得二维码之后,就需要用户去手机端去扫描二维码,并获得用户的授权,此时我们并不知道用户何时完成这个操作,所以我们只有轮询,而轮询的地址就是:* F5 q, S9 z; ]( s( `# o1 g) Y( c
6 A8 w' O. n/ Z9 D3 [3 _6 o6 Ehttps://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?uuid=DeA6idundY9VKn&tip=1&_=1377482045264(注意UUID和最后时间这两个参数)* z2 }# A5 @* W- i
" ~- v* B: `( X* G' I- x% J. J: E k4 m- [如果服务器返回:
4 @4 |* q6 x3 r5 U A6 t/ P' k5 ]8 J& o5 D4 X0 [) Z
window.code=201;; e5 ^7 W [# I* b# E# | c- U1 ^
, P% e5 L: v0 Q" G9 R则说明此时用户在手机端已经完成扫描,但还没有点击确认;, r7 [5 A" `+ V
+ T) E, \2 K9 L! e5 I+ I6 W- p1 H1 p7 ~如果服务器返回:9 Q) u+ A3 c# z M
+ k0 v0 ^* s* D. o8 W- u4 j% {window.redirect_uri=一个URL地址
. J3 X2 a& w6 D; n, Q; J
4 F4 u) ^4 z; y% q则说明此时用户已经在手机端完成了授权过程,保存下这个URL地址下一步骤中使用。# H/ v/ ]& t) O
s7 a) x. t, X( ]* f4.访问登录地址,获得uin和sid1 Y' {* I- k0 j7 Y
9 T3 r$ Y; ]3 A3 N1 c2 U( q- ]
通过访问上一步骤中获得的URL地址,可以在服务器返回的Cookies中获得到wxuin和wxsid这两个值,这两值在后续的通信过程中都要使用到这两个值,并且Cookies中也需要包括这两项。. H2 e) b$ H9 G* O! x1 F
& S& ]& k) r8 A5 e5.初使化微信信息, S5 o5 a+ E8 N5 V2 c' {" L! g- g
5 `. Q" z. A6 N* y前面的步骤算是完成了这个复杂的登录过程,如果我们需要使用微信就需要获得当前用户的信息、好友列表等,还有一个关键的就是同步信息(后续与服务器轮询中需要使用同步信息),通过访问以下的链接:
* L/ u: B/ T# n+ c( S. Z$ B4 T5 ], p3 K, v( H* f% ]- q
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=1377482058764(r依然是时间)
5 [' l' i3 H1 o9 {& o6 u) i; T* q
访问该链接需要使用POST,并且在Body中带上以下的JSON信息:
. R7 W! ]- n8 x+ y+ s; ?7 i' t/ I
( S; v$ P5 h1 n1
% ]% s2 b' J4 d' |& s) @' P2
8 S. m' v5 k+ D# ]2 @. a. z2 Y{"BaseRequest":
1 J. s& E- O: R+ _{"Uin":"2545437902","Sid":"QfLp+Z+FePzvOFoG","Skey":"","DeviceID":"e1615250492"}}
, V! S& j- w9 m/ O t) _1 i0 ?这个JSON串中Uin和Sid分别是上面步骤中获得的那两个Cookie值,DeviceID是一个本地生成的随机字符串(分析了官方的总是e+一串数字,所以我们也保持这样的格式)。
/ @7 Q! e7 P, U& l* B% ^7 |8 f
% U; {9 S* F m5 K; j: z6 }服务器就会返回一个很长的JSON串,这其中包括:BaseResponse中的值用来表示请求状态码,ContactList主要用来表示联系人(此列表不全,只包括了类似通讯录助手、文件助手、微信团队和一些公众帐号等,后面会通过另一接口去获得更全面的信息),SyncKey是用户与服务器同步的信息,User就是当前登录用户自己的信息。; W9 @0 w4 F0 d$ Z2 `4 E
) b3 O" |) E- I" C& Y3 G( H8 u6.获得所有的好友列表
0 f1 b4 H) X% c& x7 m5 K& w9 d7 Q4 a6 w- X' k" B/ b% y" A
在上一步骤中已经获得了部分好友和公众帐号,如果需要获得完整的好友信息,就需要访问以下的链接:
8 k$ s, a }2 B4 t8 b+ Z7 u7 f
* u8 \1 {/ l2 A" f5 l$ U# G- O# Nhttps://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?r=1377482079876(r依然是时间)1 r2 Q) t- z$ O4 u! ]
: z# }( K0 J# x" E1 h7 W
访问该链接同样需要POST方式,但Body为空JSON:{},服务器对身份的判定是通过Cookies,所以需要保持之前访问的Cookies不被修改(在Objective-C中会自动保存相关的Cookies,无需程序特殊处理),在返回的JSON串中,MemberList中就包含了所有的好友信息。
: J' p* j" m1 F# h" a9 X& ~
: @. s; @9 k+ q+ m4 S, Y. j8 I7.保持与服务器的信息同步/ z% V! G; G5 M3 \
. Z2 ]# |. x4 q M与服务器保持同步需要在客户端做轮询,该轮询的URL如下:
' ]4 G4 G* h5 L" K0 e1 I& G1 |- ^ Y; ^
https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?callback=jQuery18309326978388708085_1377482079946&r=1377482079876&$ S7 n0 m* V& I
sid=QfLp+Z+FePzvOFoG&uin=2545437902&deviceid=e1615250492&synckey=(见以下说明)&_=1377482079876
/ I* e# u9 l' b& |! ?0 K
, P9 n0 L" E# d$ x# _% X其中的参数r和_都是time,sid,uin,deviceid与上面步骤的值相对应,此处的synkey是上步步骤获得的同步键值,但需要按一定的规则组合成以下的字符串:5 O& n: r9 K/ c* i0 Q5 ?( b! x
0 [- O, H( q% w+ A8 g2 t
1_124125|2_452346345|3_65476547|1000_5643635- [8 D; y7 D) B8 g- T0 e' K
$ _! I6 Q2 \, O$ T+ O6 {, k就是将键和值用_隔开,不同的键值对用|隔开,但记得|需要URL编码成%7C,通过访问上面的地址,会返回如下的字符串:9 S9 \4 ?& N2 W) ~+ o/ h
+ m+ q& u. Z! R5 a0 a0 V7 Qwindow.synccheck={retcode:”0”,selector:”0”}
0 W( L' V, d# [ C* W6 O* P9 w. a$ U+ Z, \
如果retcode中的值不为0,则说明与服务器的通信有问题了,但具体问题我就无法预测了,selector中的值表示客户端需要作出的处理,目前已经知道当为6的时候表示有消息来了,就需要去访问另一个接口获得新的消息。6 T+ Q+ M) T$ W
* ^6 t0 \" ]. g0 K) W% h9 f( _8.获得别人发来的消息
& y8 N: r7 ^& x$ E, ]& \ h! O( [( f- w; R, M
当一个步骤中知道有新消息时,就需要去获取消息内容,通过访问以下的链接:
) n3 U8 ]0 J8 A$ y0 Q8 t
( d$ d$ M; n" O% V4 Fhttps://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=QfLp+Z+FePzvOFoG&r=1377482079876
$ [5 M( }( ~, t6 Q v5 |* d8 {+ {
: `4 U0 @4 o1 N9 E上面链接中的参数sid对应上面步骤中的值,r为时间,访问链接需要使用POST方式,Body中包括JSON串,该JSON串格式如下:
?5 m- }; g# H8 w
/ A4 B3 z1 Y# @2 y3 q1
7 f: b' `9 X% _& c1 g5 }: ^2
: ~$ \+ ~4 [2 Q F3
4 B5 P' a4 r! \2 }- a{"BaseRequest" : {"Uin":2545437902,"Sid":"QfLp+Z+FePzvOFoG"},7 B# e& r* J& B6 g K3 V' g
"SyncKey" : {"Count":4,"List":[{"Key":1,"Val":620310295},{"Key":2,"Val":620310303},{"Key":3,"Val":620310285},{"Key":1000,"Val":1377479086}]},
6 l1 @4 o8 ?1 X; d"rr" :1377482079876};
3 z0 _+ m V% \' K. g以下的信息中BaseRequest中包括的Uin与Sid与上面步骤中的值对应,SyncKey也是上面步骤中获得的同步键值对,rr为时间,访问成功之后服务器会返回一个JSON串,其中AddMsgList中是一个数组,包含了所有新消息。
1 R2 m4 Z6 I6 B3 Z4 ?6 u" X* T( h. w# _; v& ?
9.向用户发送消息
4 Q+ `% x6 Z# o2 t3 m. O" ?, t4 v% M3 x
用户主动发送消息,通过以下的URL地址:* X; W) I+ d. G4 Z
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?sid=QfLp+Z+FePzvOFoG&r=1377482079876
7 r" D3 v5 b1 v" \4 m ]9 J: {上面的sid和r参数不再解释了,访问该URL采用POST方式,在Body中的JSON串形如以下的格式:' }: R g8 n4 P7 X
% z) q4 F" E6 _/ m/ e5 x) U9 G4 D f; [17 }) B, b0 |# W- y+ o9 X- `% L
2
# k* b) ~# O! x2 c* Z3 M0 u. }3. j ~( s6 A T9 I. F/ N, M3 `
49 g7 N- O3 S8 o
5: g3 C! D! G1 _1 L. i
69 G9 P: e; M6 h
71 q) ?6 E0 o; S. a3 e
8
" q' r P6 ? C0 s8 m! Y93 Y' M/ C- s+ z5 X6 Q5 S4 v9 h
10
. k; |( W( a' [. \% Z5 {) |11
# _" B) O) W! z1 ^7 J12
( L& D2 a8 X" S( s: J13& ^4 \$ w0 B9 x9 P
14* t( O" B% m: [9 g
158 e3 n+ k0 d( E0 x( R# a" \
167 K( u8 t# f# c2 [, I% B
17/ l X* v2 ]: w, c' ` [8 S
{; X' \- {0 T/ a% M/ D( V
"BaseRequest":{
1 \8 t: O' Z. @ q& o7 l1 L "DeviceID" : "e441551176",
' f0 p1 s0 t$ c2 X' u6 u* H "Sid" : "S8wNi91Zry3024eg",
! B7 P+ Z- }9 ^+ H5 E6 Q2 ? "Skey" : "F820928BBA5D8ECA23448F076D2E8A915E1349E9FB4F4332",
5 F- _5 F$ x' W0 [ "Uin" : "2545437902"$ ^4 p& a4 C2 b6 v" P. ?
},4 L' X! e% i. j* ?1 }
"Msg" : {5 `, P. I! m2 t' r4 H
"ClientMsgId" : 1377504862158, D, G! ~5 Q# ]
"Content" : "hello",
( I, w& `6 z6 D "FromUserName" : "wxid_2rrz8g8ezuox22",
+ L7 |4 I9 A( `$ X "LocalID" : 1377504862158,2 Q0 f$ z( ^! {( ?" o
"ToUserName" : "wxid_j4nu420ojhsr21",% X. h8 O+ {7 A) O" P
"Type" : 1
) j8 y6 Z1 x: x },
" R9 e! i% ^3 M) Y# g! @6 g) F "rr" = 1377504864463
% |* Z8 N( d+ J2 ~" R+ S}
c' _! h) A% W7 p0 r6 e$ ?6 L其中BaseRequest都是授权相关的值,与上面的步骤中的值对应,Msg是对消息的描述,包括了发送人与接收人,消息内容,消息的类型(1为文本),ClientMsgId和LocalID由本地生成。rr可用当前的时间。
( H* ^- D) S' @+ `, r6 r在返回JSON结果中BaseResponse描述了发送情况,Ret为0表示发送成功。
G9 s2 g1 Q5 l6 K, x* V$ p; Q* |- V9 u% q) \( c! {8 _" i, T3 ?9 M& P7 _; {
|
|