简述XMPP WEB登录

发表于:2018-07-22 19:50:19,已有3908次阅读

前言

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-Protocolxmpp,不然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: WebSocket
BOSH:
<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.attachStrophe.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的更详细用法,参见官方文档

参考网址

XEP-0206: XMPP Over BOSH

RFC 7395:XMPP Over WebSocket

RFC 6120:XMPP CoreRFC 6120:XMPP Core(中文)

XMPP登录步骤

[Openfire]使用WebSocket建立Openfire的客户端

HTTPS(SSL)站点使用WebSocket(ws)出现SecurityError问题

SASL与身份验证

The Java SASL API


评论

暂无评论

您还可输入120个字