原 简述XMPP WEB登录
前言
XMPP在WEB浏览器端,可以使用两种方式进行登录,一种是流行的HTML5 websocket方式,还有一种是适合传统浏览器的BOSH(半轮询AJAX)的方式,两种方式的步骤结构都是一样的,只是发送的XML稍有差别。由于BOSH方式使用的是AJAX半轮询方式,而且通过HTTP交互是无状态的,为了保持每次与服务端通讯的会话session,因此发送的XML数据包,在BOSH方式传输过程中,使用<body>标签包裹,<body>标签有两个重要的属性,一个是sid:表示会话session的id;还有一个是rid:表示每一次请求的request id,且request id每一次都是在上一次的rid的基础上加1,因此需特别的注意。而websocket方式是浏览器端的类socket方式,一旦与服务器端连接成功后,就一直存在,是有状态的,因此不需要额外的session id用于保持会话,所以发送给服务端的XML是直接的,下面就这两种方式逐一说明。
登录流程
XMPP登录的时序图如下:
详细步骤说明
1.发送握手XML,通知服务器开启会话
websocket:
<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" to="example.com" version="1.0" />
这里要注意的就是发送websoket与xmpp服务器进行交互时,请求头header中必须设置Sec-WebSocket-Protocol
为xmpp
,不然xmpp服务器会拒绝握手。
下面是建立websocket的一次示例,请求头:
GET wss://example.com/xmpp-websocket/ HTTP/1.1 Host: example.com Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: file:// Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36 Accept-Encoding: gzip, deflate, br Accept-Language: zh,zh-CN;q=0.9,en;q=0.8 Sec-WebSocket-Key: sfJXhCiDM4aqZ5Ni05gygA== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Protocol: xmpp对应服务器的响应头:
HTTP/1.1 101 Switching Protocols Server: nginx/1.4.6 (Ubuntu) Date: Sun, 22 Jul 2018 07:03:00 GMT Connection: upgrade Sec-WebSocket-Accept: Z70tg3djp4oVABnf820YLscYl08= Sec-WebSocket-Extensions: permessage-deflate Sec-WebSocket-Protocol: xmpp Upgrade: WebSocketBOSH:
<body rid="4127380478" xmlns="http://jabber.org/protocol/httpbind" to="example.com" xml:lang="en" wait="60" hold="1" content="text/xml; charset=utf-8" ver="1.6" xmpp:version="1.0" xmlns:xmpp="urn:xmpp:xbosh"/>
关于BOSH中body标签的如上属性可以详情参考:BOSH (XEP-0124), BOSH(XEP-0124)中文。
2.服务端在接收握手打招呼的XML后,对应的也会回应握手成功信息,并开启同客户端的会话
websocket:
<open from='example.com' id='8r9cjmpyj7' xmlns='urn:ietf:params:xml:ns:xmpp-framing' xml:lang='en' version='1.0'/>BOSH,这一步合并到第3步,与发送<stream:features>流一起返回。
3.服务器发送<stream:featrures>流,开启SASL身份验证,说明支持的几项身份加密方式
websocket:
<stream:features xmlns:stream='http://etherx.jabber.org/streams'> <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> <mechanism>PLAIN</mechanism> <mechanism>CRAM-MD5</mechanism> <mechanism>DIGEST-MD5</mechanism> </mechanisms> </stream:features>BOSH:
<body xmlns="http://jabber.org/protocol/httpbind" xmlns:stream="http://etherx.jabber.org/streams" from="example.com" authid="10qwgaiqec" sid="10qwgaiqec" secure="true" requests="2" inactivity="30" polling="5" wait="60" hold="1" ack="4127380478" maxpause="300" ver="1.6"> <stream:features> <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> <mechanism>PLAIN</mechanism> <mechanism>CRAM-MD5</mechanism> <mechanism>DIGEST-MD5</mechanism> </mechanisms> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/> <session xmlns="urn:ietf:params:xml:ns:xmpp-session"><optional/></session> </stream:features> </body>
4.客户端应答服务器,告诉服务器将要采取的身证验证加密方式
websocket:
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>BOSH:
<body rid="4127380479" xmlns="http://jabber.org/protocol/httpbind" sid="10qwgaiqec"> <auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5"/> </body>如上的例子说明客户端采用DIGEST-MD5的加密方式。关于SASL的身份验证加密方式可以参考:SASL Mechanisms
5.服务端确认客户端的加密方式后,将会发送一个盘问<challenge>,采用base64的编码,包含身份加密的nonce密钥
websocket:
<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cj1vTXNUQUF3QUFBQU1BQUFBTlAwVEFBQUFBQUJQVTBBQWUxMjQ2OTViLTY5Y TktNGRlNi05YzMwLWI1MWIzODA4YzU5ZSxzPU5qaGtZVE0wTURndE5HWTBaaT AwTmpkbUxUa3hNbVV0TkRsbU5UTm1ORE5rTURNeixpPTQwOTY=</challenge>
BOSH:
<body xmlns="http://jabber.org/protocol/httpbind" ack="4127380479"> <challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cj1vTXNUQUF3QUFBQU1BQUFBTlAwVEFBQUFBQUJQVTBBQWUxMjQ2OTViLTY5Y TktNGRlNi05YzMwLWI1MWIzODA4YzU5ZSxzPU5qaGtZVE0wTURndE5HWTBaaT AwTmpkbUxUa3hNbVV0TkRsbU5UTm1ORE5rTURNeixpPTQwOTY=</challenge> </body>
6.客户端根据选择的加密方式及收到的服务端密钥,加密用户帐户与密码,发送<response>用于身证验证
websocket:
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> Yz1iaXdzLHI9b01zVEFBd0FBQUFNQUFBQU5QMFRBQUFBQUFCUFUwQUFlMTI0N jk1Yi02OWE5LTRkZTYtOWMzMC1iNTFiMzgwOGM1OWUscD1VQTU3dE0vU3ZwQV RCa0gyRlhzMFdEWHZKWXc9 </response>
BOSH:
<body rid="4127380480" xmlns="http://jabber.org/protocol/httpbind" sid="10qwgaiqec"> <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> Yz1iaXdzLHI9b01zVEFBd0FBQUFNQUFBQU5QMFRBQUFBQUFCUFUwQUFlMTI0N jk1Yi02OWE5LTRkZTYtOWMzMC1iNTFiMzgwOGM1OWUscD1VQTU3dE0vU3ZwQV RCa0gyRlhzMFdEWHZKWXc9 </response> </body>
7.服务端,在接收用户的加密<response>后,进行用户帐户的验证,如果成功则发送成功<success>标签
websocket:
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cnNwYXV0aD0wMzQ2ZGQxMTk0NzNlZjczNDU5NzA1YjRmNjAwYTJkOA==</success>
BOSH:
<body xmlns="http://jabber.org/protocol/httpbind" ack="4127380480"> <success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cnNwYXV0aD1iMTRjM2MzY2FjYTIxOTc2MmI2NzM4ZDY3NDY1NGU4ZQ==</success> </body>
对应验证失败,则返回错误信息:
websocket:
<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized/></failure>
BOSH:
<body xmlns="http://jabber.org/protocol/httpbind" ack="4127380480"> <failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> <not-authorized/> </failure> </body>
8.重启会话,重新建立连接,发送握手XML
这里需要特别的注意,当用户完成身份验证完成后,接下来必须进行的操作就是身份绑定资源resource和会话,需要强制重启一下会话,重新发送握手信息。
websocket:
<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" to="example.com" version="1.0" />
BOSH:
<body rid="4127380481" xmlns="http://jabber.org/protocol/httpbind" sid="10qwgaiqec" to="example.com" xml:lang="en" xmpp:restart="true" xmlns:xmpp="urn:xmpp:xbosh"/>
这里的重启会话相当于隐式的关闭了Stream。
9.服务端响应重新握手信息,重启会话Stream
websocket:
<open from='example.com' id='8r9cjmpyj7' xmlns='urn:ietf:params:xml:ns:xmpp-framing' xml:lang='en' version='1.0'/>
BOSH,这里同第10步发送<stream:features>
一起返回.
10.服务端发送<stream:features>,说明绑定resouce和session操作
websocket:
<stream:features xmlns:stream='http://etherx.jabber.org/streams'> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/> <session xmlns='urn:ietf:params:xml:ns:xmpp-session'><optional/></session> <sm xmlns='urn:xmpp:sm:3'/> </stream:features>
BOSH:
<body xmlns="http://jabber.org/protocol/httpbind" xmlns:stream="http://etherx.jabber.org/streams"> <stream:features> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/> <session xmlns="urn:ietf:params:xml:ns:xmpp-session"><optional/></session> </stream:features> </body>
11.客户端进行绑定登录用户的resource或session.
绑定resource
websocket:
<iq id="bind_auth_001" type="set" xmlns="jabber:client"> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"> <resource>websocket-test</resource> </bind> </iq>
BOSH:
<body rid="4127380482" xmlns="http://jabber.org/protocol/httpbind" sid="10qwgaiqec"> <iq type="set" id="_bind_auth_2" xmlns="jabber:client"> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"> <resource>bosh-test</resource> </bind> </iq> </body>
绑定session
websocket:
<iq id='session_auth_001' type='set' xmlns='jabber:client'> <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/> </iq>
BOSH:
<body rid="4127380483" xmlns="http://jabber.org/protocol/httpbind" sid="10qwgaiqec"> <iq type="set" id="_session_auth_2" xmlns="jabber:client"> <session xmlns="urn:ietf:params:xml:ns:xmpp-session"/> </iq> </body>
12.服务端响应绑定resource成功信息,返回登录用户的JID
websocket:
<iq xmlns="jabber:client" type="result" id="bind_auth_001" to="example.com/8r9cjmpyj7"> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"> <jid>[email protected]/websocket-test</jid> </bind> </iq>
BOSH:
<body xmlns="http://jabber.org/protocol/httpbind" ack="4127380482"> <iq xmlns="jabber:client" type="result" id="_bind_auth_2" to="example.com/10qwgaiqec"> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"> <jid>[email protected]/bosh-test</jid> </bind> </iq> </body>
对应绑定session的服务端返回
websocket:
<iq xmlns="jabber:client" type="result" id="session_auth_001" to="[email protected]/websocket-test"/>
BOSH:
<body xmlns="http://jabber.org/protocol/httpbind" ack="4127380483"> <iq xmlns="jabber:client" type="result" id="_session_auth_2" to="[email protected]/bosh-test"/> </body>
13.最后就是客户端发送用户出席登录信息,用于通知服务器用户已登录
websocket:
<presence xmlns="jabber:client"><priority>1</priority></presence>
BOSH:
<body rid="4127380484" xmlns="http://jabber.org/protocol/httpbind" sid="10qwgaiqec"> <presence xmlns="jabber:client"><priority>1</priority></presence> </body>
完整通讯流程
这里提供一份使用Strophe.js登录由Openfire搭建的XMPP服务器,登录通讯的完整XML交互日志,如下:
websocket:
BOSH:
说明:Openfire从4.2.0开始就己经合并了websocket插件到核心代码,因此使用websocket在Openfire 4.2.0以后的版本中不需要额外的安装插件,对应的websocket默认地址为:
ws://youeropenfirehost:7070/ws/
wss://youeropenfirehost:7443/ws/
这里调用的Javascript示例代码为:
$(function () { var conn = new Strophe.Connection("wss://youeropenfirehost:7443/ws/"); conn.xmlInput = function (elem) { console.log("%cS: " + elem.outerHTML, "color: #d14"); }; conn.xmlOutput = function (elem) { console.log("%cC: " + elem.outerHTML, "color: #0769ad"); }; conn.connect("username@youeropenfirehost", "password", function (status) { if (status == Strophe.Status.CONNECTING) { console.log('Strophe is connecting.'); } else if (status == Strophe.Status.CONNFAIL) { console.log('Strophe failed to connect.'); } else if (status == Strophe.Status.DISCONNECTING) { console.log('Strophe is disconnecting.'); } else if (status == Strophe.Status.DISCONNECTED) { console.log('Strophe is disconnected.'); } else if (status == Strophe.Status.CONNECTED) { console.log('Strophe is connected.'); conn.send($pres().c("priority").t(1)); } }); });
Strophe.js支持BOSH与websocket两种方式,对于这两种方式的处理是内建的,因此对于使用Strophe.js的用户来说是无感的,Strophe.js区分采用哪种方式主要查看的是用户传入的Service URL前缀判断的,如https://
和wss://
。
这里还需要注意的是使用Strophe.js按websocket方式进行通信时,不再支持Strophe.Connection.attach
和Strophe.Connection.restore
这两个方法,这两个方法为BOSH独享的。原因很简单,websocket断开后,由于session会话是内置在socket通信中的,不像BOSH提供了sid与rid,可以重连。断开即websocket对象丢失 ,则无法重建。对于BOSH的这两种方法很特别,他们可以实现用户在后端登录成功后,在JS前台使用后端登录成功返回的sid和rid实现前端的同步整合登录,无需用户名与密码。(相关例子PHP程序:https://github.com/lzqwebsoft/xmpp-prebind-php)。websocket刷新即丢失,如果要实现如上功能,估计要修改Openfire代码,自定义扩展XMPP功能。
关于Strophe.js的更详细用法,参见官方文档。
参考网址
RFC 6120:XMPP Core, RFC 6120:XMPP Core(中文)
[Openfire]使用WebSocket建立Openfire的客户端
HTTPS(SSL)站点使用WebSocket(ws)出现SecurityError问题
暂无评论