Java利用MSNP协议登录MSN

请参见上一篇文章,登录MSN协议
具体Java实现:

命令序列:<<代表发送,>>代表结果

1.连接DS(Dispatcher Server),得到NS(Notification Server)

<<VER 1 MSNP18 CVR0
>>VER 1 MSNP18
<<CVR 2 0x0804 winnt 5.1 i386 MSNMSGR 14.0.8089.0726 msmsgs yourAccount
>>CVR 2 14.0.8089 14.0.8089 14.0.8089 http://msgruser.dlservice.microsoft.com/download/0/9/7/0974F7CD-D082-46FE-922D-806670345793/zh-chs/wlsetup-cvr.exe http://download.live.com/?sku=messenger
<<USR 3 SSO I yourAccount
>>XFR 3 NS 207.46.124.86:1863 U D

private String dsHost = "64.4.9.254";//ds host private String nsHost;//ns host private int port = 1863;//port private int trId = 1;//命令序列号 private String ticketToken;//在获取联系人时用 //得到向MSN服务器发送的命令 private String getMSNCommand(String cmd, String ... args) { StringBuilder sb = new StringBuilder(); sb.append(cmd).append(' '); sb.append(trId++).append(' '); for (int i = 0; i < args.length; i++) { if (i < args.length - 1) { sb.append(args[i]).append(' '); } else { sb.append(args[i]); } } sb.append("/r/n"); return sb.toString(); } //初始化NS地址 private void initNSHost() throws TelnetException { NonBlockTelnetClient client = new NonBlockTelnetClient(dsHost, port); client.connect(); String cmd = getMSNCommand("VER", "MSNP18", "CVR0"); client.sendCommand(cmd); String resp = client.getOutputByLine(); System.out.println("resp=" + resp); cmd = getMSNCommand("CVR", "0x0804", "winnt", "5.1", "i386", "MSNMSGR", "14.0.8089.0726", "msmsgs", username); client.sendCommand(cmd); resp = client.getOutputByLine(); System.out.println("resp=" + resp); cmd = getMSNCommand("USR", "SSO", "I", username); client.sendCommand(cmd); resp = client.getOutputByLine(); System.out.println("resp=" + resp); String[] tmpArr = resp.split(" "); nsHost = tmpArr[3].split(":")[0]; client.disconnect(); }

这里有一个类叫NonBlockTelnetClient是为了向服务器发送命令用的类(这只是一个简单的示例,未考虑更多其他问题)

public class NonBlockTelnetClient { private String host; private int port; private SocketChannel socketChannel; private Thread thread; private StringBuilder output = new StringBuilder(); private int timeout = 5000; private int lastPos = 0; public NonBlockTelnetClient(String host, int port) { this.host = host; this.port = port; } public void connect() throws TelnetException { try { if (socketChannel == null) { SocketAddress sAddr = new InetSocketAddress(host, port); socketChannel = SocketChannel.open(sAddr); socketChannel.configureBlocking(false); thread = new Thread(new Runnable() { public void run() { try { ByteBuffer buffer = ByteBuffer.allocate(4096); while (true) { buffer.clear(); socketChannel.read(buffer); buffer.flip(); int s = 0; int e = buffer.limit(); byte[] tmpBuf = new byte[e]; buffer.get(tmpBuf); synchronized (output) { output.append(new String(tmpBuf)); } Thread.sleep(100); } } catch (Exception e) { if (!(e instanceof InterruptedException)) { e.printStackTrace(); } } } }); thread.setDaemon(true); thread.start(); } } catch (IOException e) { throw new TelnetException(e); } } public void disconnect() throws TelnetException { try { if (socketChannel != null) { if (socketChannel.isOpen()) { socketChannel.close(); } if (!thread.isInterrupted()) { thread.interrupt(); } } } catch (IOException e) { throw new TelnetException(e); } } public void sendCommand(String cmd) throws TelnetException { try { System.out.println("cmd=" + cmd); ByteBuffer buffer = ByteBuffer.wrap(cmd.getBytes()); while(buffer.hasRemaining()) { socketChannel.write(buffer); } } catch (IOException e) { throw new TelnetException(e); } } public String getOutputByLine() throws TelnetException { long start = System.currentTimeMillis(); while (true) { long curr = System.currentTimeMillis(); if ((curr - start) >= timeout) { return ""; } int idx = output.indexOf("/r/n", lastPos); if (idx > -1) { String tmp = null; synchronized (output) { tmp = output.substring(lastPos, idx); } lastPos = idx + 2; return tmp; } try { Thread.sleep(100); } catch (InterruptedException e) { throw new TelnetException(e); } } } }

2.通过第一步,可以得到NS的IP地址了,接着进行登录

<<VER 4 MSNP18 CVR0
>>VER 4 MSNP18
<<CVR 5 0x0804 winnt 5.1 i386 MSNMSGR 14.0.8089.0726 msmsgs yourAccount
>>CVR 5 14.0.8089 14.0.8089 14.0.8089 http://msgruser.dlservice.microsoft.com/download/0/9/7/0974F7CD-D082-46FE-922D-806670345793/zh-chs/wlsetup-cvr.exe http://download.live.com/?sku=messenger
<<USR 6 SSO I yourAccount
>>GCF 0 4804
<Policies>...</Policies>USR 6 SSO S MBI_KEY_OLD cfznpPIvKOe2GlNv6z3OleqF8lrlNas2MM+fpobPjNLy2LZAeWQb6wcrcbMomSyn

以下这段代码完成上述协议序列

String cmd = getMSNCommand("VER", "MSNP18", "CVR0"); client.sendCommand(cmd); String resp = client.getOutputByLine(); System.out.println("resp=" + resp); cmd = getMSNCommand("CVR", "0x0804", "winnt", "5.1", "i386", "MSNMSGR", "14.0.8089.0726", "msmsgs", username); client.sendCommand(cmd); resp = client.getOutputByLine(); System.out.println("resp=" + resp); cmd = getMSNCommand("USR", "SSO", "I", username); client.sendCommand(cmd); resp = client.getOutputByLine(); System.out.println("resp=" + resp); resp = client.getOutputByLine(); System.out.println("resp=" + resp);

接着得到policy和nonce

int idx = resp.indexOf("USR"); String tmp = resp.substring(idx); System.out.println("tmp=" + tmp); String[] tmpArr = tmp.split(" "); String policy = tmpArr[4]; String nonce = tmpArr[5]; System.out.println("policy=" + policy); System.out.println("nonce=" + nonce);

然后发送SOAP到https://login.live.com/RST.srf(soap格式:http://msnpiki.msnfanatic.com/index.php/MSNP15:SSO)
从结果的xml中(soap格式:http://msnpiki.msnfanatic.com/index.php/MSNP15:SSO)可以取到secret和ticket
对其进行编码得到ssoticket

以上过程的代码如下:

String ticket = getTicket(policy, nonce); System.out.println("ticket=" + ticket); private String getTicket(String policy, String nonce) { // create the xml for the SOAP request StringBuilder xml = new StringBuilder(); xml.append("<?xml version=/"1.0/" encoding=/"UTF-8/"?>"); xml.append("<Envelope xmlns=/"http://schemas.xmlsoap.org/soap/envelope//" xmlns:wsse=/"http://schemas.xmlsoap.org/ws/2003/06/secext/" xmlns:saml=/"urn:oasis:names:tc:SAML:1.0:assertion/" xmlns:wsp=/"http://schemas.xmlsoap.org/ws/2002/12/policy/" xmlns:wsu=/"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd/" xmlns:wsa=/"http://schemas.xmlsoap.org/ws/2004/03/addressing/" xmlns:wssc=/"http://schemas.xmlsoap.org/ws/2004/04/sc/" xmlns:wst=/"http://schemas.xmlsoap.org/ws/2004/04/trust/"><Header>"); xml.append("<ps:AuthInfo xmlns:ps=/"http://schemas.microsoft.com/Passport/SoapServices/PPCRL/" Id=/"PPAuthInfo/">"); xml.append("<ps:HostingApp>{7108E71A-9926-4FCB-BCC9-9A9D3F32E423}</ps:HostingApp>"); xml.append("<ps:BinaryVersion>4</ps:BinaryVersion>"); xml.append("<ps:UIVersion>1</ps:UIVersion>"); xml.append("<ps:Cookies></ps:Cookies>"); xml.append("<ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams>"); xml.append("</ps:AuthInfo>"); xml.append("<wsse:Security><wsse:UsernameToken Id=/"user/">"); xml.append("<wsse:Username>" + username + "</wsse:Username>"); xml.append("<wsse:Password>" + password + "</wsse:Password>"); xml.append("</wsse:UsernameToken></wsse:Security></Header><Body>"); xml.append("<ps:RequestMultipleSecurityTokens xmlns:ps=/"http://schemas.microsoft.com/Passport/SoapServices/PPCRL/" Id=/"RSTS/">"); xml.append("<wst:RequestSecurityToken Id=/"RST0/">"); xml.append("<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>"); xml.append("<wsp:AppliesTo><wsa:EndpointReference><wsa:Address>http://Passport.NET/tb"); xml.append("</wsa:Address></wsa:EndpointReference></wsp:AppliesTo></wst:RequestSecurityToken>"); xml.append("<wst:RequestSecurityToken Id=/"RST1/">"); xml.append("<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference>"); xml.append("<wsa:Address>messengerclear.live.com</wsa:Address></wsa:EndpointReference></wsp:AppliesTo>"); xml.append("<wsse:PolicyReference URI=/"" + policy + "/"></wsse:PolicyReference></wst:RequestSecurityToken>"); xml.append("<wst:RequestSecurityToken Id=/"RST2/">"); xml.append("<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>"); xml.append("<wsp:AppliesTo>"); xml.append("<wsa:EndpointReference>"); xml.append("<wsa:Address>contacts.msn.com</wsa:Address>"); xml.append("</wsa:EndpointReference>"); xml.append("</wsp:AppliesTo>"); xml.append("<wsse:PolicyReference URI=/"MBI/">"); xml.append("</wsse:PolicyReference>"); xml.append("</wst:RequestSecurityToken>"); xml.append("</ps:RequestMultipleSecurityTokens></Body></Envelope>"); // create a new SOAP request with the xml HttpBrowser browser = new HttpBrowser(); try { HttpResponse resp = browser.doPost("https://login.live.com/RST.srf", xml.toString()); String respXml = resp.getBodyAsString(); System.out.println("soap resp=" + respXml); int idx1 = respXml.indexOf("<wst:BinarySecret>") + "<wst:BinarySecret>".length(); String secret = respXml.substring(idx1); int idx2 = secret.indexOf("<wst:BinarySecret>") + "<wst:BinarySecret>".length(); secret = secret.substring(idx2); int idx3 = secret.indexOf("</wst:BinarySecret>"); secret = secret.substring(0, idx3); System.out.println("secret=" + secret); int idx4 = respXml.indexOf("<wsse:BinarySecurityToken Id=/"Compact1/">") + "<wsse:BinarySecurityToken Id=/"Compact1/">".length(); String ticket1 = respXml.substring(idx4); int idx5 = ticket1.indexOf("</wsse:BinarySecurityToken>"); ticket1 = ticket1.substring(0, idx5); ticket1 = ticket1.replaceAll("&amp;", "&"); System.out.println("ticket1=" + ticket1); int idx6 = respXml.indexOf("<wsse:BinarySecurityToken Id=/"Compact2/">") + "<wsse:BinarySecurityToken Id=/"Compact2/">".length(); String ticket2 = respXml.substring(idx6); int idx7 = ticket2.indexOf("</wsse:BinarySecurityToken>"); ticket2 = ticket2.substring(0, idx7); System.out.println("ticket2=" + ticket2); ticketToken = ticket2; String ssoTicket = getSSOTicket(secret, nonce); return ticket1 + " " + ssoTicket; } catch (Exception e) { e.printStackTrace(); } return ""; } public static String getSSOTicket(String secret, String nonce) { // fill random iv byte[] iv = new byte[8]; for (int i = 0; i < 8; i++) { byte rand = (byte) Math.floor(Math.random() * 256); iv[i] = rand; } // fill the begining of the struct byte start[] = new byte[28]; // uStructHeaderSize = 28 start[0] = 0x1c; start[1] = 0x00; start[2] = 0x00; start[3] = 0x00; // uCryptMode = 1 start[4] = 0x01; start[5] = 0x00; start[6] = 0x00; start[7] = 0x00; // uCipherType = 0x6603 start[8] = 0x03; start[9] = 0x66; start[10] = 0x00; start[11] = 0x00; // uHashType = 0x8004 start[12] = 0x04; start[13] = (byte) 0x80; start[14] = 0x00; start[15] = 0x00; // uIVLen = 8 start[16] = 0x08; start[17] = 0x00; start[18] = 0x00; start[19] = 0x00; // uHashLen = 20 start[20] = 0x14; start[21] = 0x00; start[22] = 0x00; start[23] = 0x00; // uCipherLen = 72 start[24] = 0x48; start[25] = 0x00; start[26] = 0x00; start[27] = 0x00; // generate key's byte key1[] = Base64Coder.decode(secret); byte key2[] = derive_key(key1, "WS-SecureConversationSESSION KEY HASH".getBytes()); byte key3[] = derive_key(key1, "WS-SecureConversationSESSION KEY ENCRYPTION".getBytes()); // create hash byte hash[] = HMAC(key2, nonce.getBytes()); // add 8 bytes to the nonce byte nonceFillup[] = new byte[8]; for (int i = 0; i < 8; i++) { nonceFillup[i] = 0x08; } // create des3 thingy byte des[] = DES3(key3, byteCombine(nonce.getBytes(), nonceFillup), iv); // fill struct byte struct[] = byteCombine(byteCombine(byteCombine(start, iv), hash), des); // return stuff return new String(Base64Coder.encode(struct)); } private static byte[] derive_key(byte[] key, byte[] magic) { byte hash1[] = HMAC(key, magic); byte hash2[] = HMAC(key, byteCombine(hash1, magic)); byte hash3[] = HMAC(key, hash1); byte hash4[] = HMAC(key, byteCombine(hash3, magic)); byte out[] = new byte[4]; out[0] = hash4[0]; out[1] = hash4[1]; out[2] = hash4[2]; out[3] = hash4[3]; return byteCombine(hash2, out); } private static byte[] HMAC(byte[] key, byte[] subject) { try { Mac mac = Mac.getInstance("HmacSHA1"); SecretKeySpec sk = new SecretKeySpec(key, "HmacSHA1"); mac.init(sk); return mac.doFinal(subject); } catch (NoSuchAlgorithmException ex) { ex.printStackTrace(); } catch (InvalidKeyException ex) { ex.printStackTrace(); } return null; } private static byte[] byteCombine(byte[] front, byte[] back) { byte output[] = new byte[front.length + back.length]; for (int i = 0; i < front.length; i++) { output[i] = front[i]; } for (int i = 0; i < back.length; i++) { output[i + front.length] = back[i]; } return output; } private static byte[] DES3(byte[] key, byte[] subject, byte[] iv) { try { Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding"); SecretKeySpec sk = new SecretKeySpec(key, "DESede"); IvParameterSpec sr = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, sk, sr); return cipher.doFinal(subject); } catch (Exception ex) { ex.printStackTrace(); } return null; }

注意SOAP请求中,发送了RST0,RST1,RST2分别对应三个域http://Passport.NET/tb,messengerclear.live.com,contacts.msn.com,即从这三个域中得到相应的ticketToken,每个ticketToken分别用于不同的域(不能混用)。

跳过第一个<wst:BinarySecret>,得到第二个<wst:BinarySecret>中的内容为secret

得到<wsse:BinarySecurityToken Id="Compact1">中的ticket为ticket1,得到<wsse:BinarySecurityToken Id="Compact2">中的ticket为ticket2,CompactN对应于RSTN,因此Compact1对应于messengerclear.live.com(这个是用于msn验证的域)的ticket,而Compact2对应于contacts.msn.com的ticket,登录时只用ticket1,而ticket2被赋给ticketToken用于取联系人时的ticket

为什么要发送RST0呢,下面的链接文档中只说明了这个是登录必须的,但其返回的Compact0中的ticket是没用的

[协议中说:如果你的policy为MBI, MBI_SSL or MBI_KEY_OLD,则你的ticket为<wsse:BinarySecurityToken Id="Compactn">中的内容 ;如果你的policy包括一个以问号开始的串,则ticket为<wsse:BinarySecurityToken Id="PPTokenn"> 中的内容

具体见:http://msnpiki.msnfanatic.com/index.php/MSNP15:SSO

HttpBrowser类,只是一个httpclient的封装,可以发送post和get请求

HttpResponse类,是http回复内容

以上两个类在此就不给出了

另外还有一个Base64Coder类,是进行base64编码的类

public class Base64Coder { // Mapping table from 6-bit nibbles to Base64 characters. private static char[] map1 = new char[64]; static { int i = 0; for (char c = 'A'; c <= 'Z'; c++) map1[i++] = c; for (char c = 'a'; c <= 'z'; c++) map1[i++] = c; for (char c = '0'; c <= '9'; c++) map1[i++] = c; map1[i++] = '+'; map1[i++] = '/'; } // Mapping table from Base64 characters to 6-bit nibbles. private static byte[] map2 = new byte[128]; static { for (int i = 0; i < map2.length; i++) map2[i] = -1; for (int i = 0; i < 64; i++) map2[map1[i]] = (byte) i; } /** * Encodes a string into Base64 format. * No blanks or line breaks are inserted. * @param s a String to be encoded. * @return A String with the Base64 encoded data. */ public static String encodeString(String s) { return new String(encode(s.getBytes())); } /** * Encodes a byte array into Base64 format. * No blanks or line breaks are inserted. * @param in an array containing the data bytes to be encoded. * @return A character array with the Base64 encoded data. */ public static char[] encode(byte[] in) { return encode(in, in.length); } /** * Encodes a byte array into Base64 format. * No blanks or line breaks are inserted. * @param in an array containing the data bytes to be encoded. * @param iLen number of bytes to process in <CODE>in</CODE>. * @return A character array with the Base64 encoded data. */ public static char[] encode(byte[] in, int iLen) { int oDataLen = (iLen * 4 + 2) / 3; // output length without padding int oLen = ((iLen + 2) / 3) * 4; // output length including padding char[] out = new char[oLen]; int ip = 0; int op = 0; while (ip < iLen) { int i0 = in[ip++] & 0xff; int i1 = ip < iLen ? in[ip++] & 0xff : 0; int i2 = ip < iLen ? in[ip++] & 0xff : 0; int o0 = i0 >>> 2; int o1 = ((i0 & 3) << 4) | (i1 >>> 4); int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); int o3 = i2 & 0x3F; out[op++] = map1[o0]; out[op++] = map1[o1]; out[op] = op < oDataLen ? map1[o2] : '='; op++; out[op] = op < oDataLen ? map1[o3] : '='; op++; } return out; } /** * Decodes a string from Base64 format. * @param s a Base64 String to be decoded. * @return A String containing the decoded data. * @throws IllegalArgumentException if the input is not valid Base64 encoded data. */ public static String decodeString(String s) { return new String(decode(s)); } /** * Decodes a byte array from Base64 format. * @param s a Base64 String to be decoded. * @return An array containing the decoded data bytes. * @throws IllegalArgumentException if the input is not valid Base64 encoded data. */ public static byte[] decode(String s) { return decode(s.toCharArray()); } /** * Decodes a byte array from Base64 format. * No blanks or line breaks are allowed within the Base64 encoded data. * @param in a character array containing the Base64 encoded data. * @return An array containing the decoded data bytes. * @throws IllegalArgumentException if the input is not valid Base64 encoded data. */ public static byte[] decode(char[] in) { int iLen = in.length; if (iLen % 4 != 0) throw new IllegalArgumentException("Length of Base64 encoded input string is not a multiple of 4."); while (iLen > 0 && in[iLen - 1] == '=') iLen--; int oLen = (iLen * 3) / 4; byte[] out = new byte[oLen]; int ip = 0; int op = 0; while (ip < iLen) { int i0 = in[ip++]; int i1 = in[ip++]; int i2 = ip < iLen ? in[ip++] : 'A'; int i3 = ip < iLen ? in[ip++] : 'A'; if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) throw new IllegalArgumentException("Illegal character in Base64 encoded data."); int b0 = map2[i0]; int b1 = map2[i1]; int b2 = map2[i2]; int b3 = map2[i3]; if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) throw new IllegalArgumentException("Illegal character in Base64 encoded data."); int o0 = (b0 << 2) | (b1 >>> 4); int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); int o2 = ((b2 & 3) << 6) | b3; out[op++] = (byte) o0; if (op < oLen) out[op++] = (byte) o1; if (op < oLen) out[op++] = (byte) o2; } return out; } }

接着:利用上述返回的ticket进行登录

<<USR 7 SSO S ssoticket {691CC6CA-F23C-4E86-BB91-82965CBC5429}
>>USR 7 OK yourAccount 1 0  (登录成功)

cmd = getMSNCommand("USR", "SSO", "S", ticket, "{691CC6CA-F23C-4E86-BB91-82965CBC5429}"); client.sendCommand(cmd); resp = client.getOutputByLine(); System.out.println("resp=" + resp);

以上便登录成功了

你可能感兴趣的:(Java利用MSNP协议登录MSN)