12.2 实例:IM应用开发安全
大家在日常工作、生活中都有使用QQ、MSN、RTX、Skype、GTalk等IM(Instant Message,即时通信)工具的经历。换言之,缺少了QQ、MSN、RTX、Skype、GTalk的网络如同脱离了网络的电脑,变得索然无味。
IM工具与我们的工作和生活几乎是密不可分的,我们常常通过IM工具与亲友交流感情,与同事交换意见,甚至与合作伙伴互换商业数据。现如今的淘宝网生意做得越来越火,我们已经可以足不出户就在网上下订单,通过阿里旺旺跟卖家/买家讨价还价,享受网络生活的便利。
但是,当我们使用各种IM工具与对方交换数据时,可曾想过我们的聊天信息可能被监听,与合作伙伴交换的数据可能被窃取,刚刚通过阿里旺旺收到的手机充值卡号居然在瞬间被盗用。一切皆有可能,因为通过IM工具泄露银行卡号、密码的事情已经是不争的事实。
虽然这些IM工具都有自定的通信协议,但这些协议又必须公开,因此协议本身几乎没有任何安全性可言。尽管这些IM工具对自身不断进行升级,但极少数IM工具会对网络数据进行加密。因此,网络聊天通常都是不安全的。
QQ、MSN、RTX、Skype、GTalk等IM工具均基于UDP(User Datagram Protocol,用户数据报协议)进行通信。UDP是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。相关细节请读者参考文档RFC 768(http://www.ietf.org/rfc/rfc768.txt)。
本文将构建简单的基于UDP的聊天工具,展示如何使用加密算法对聊天信息进行加密。
12.2.1 IM应用开发基本实现
相信大家对如图12-6所示的聊天窗口一定不会陌生。这是一个使用Java实现的基于UDP通信协议的聊天工具(我们称它为UDPChat)。当然,我们生活中使用的IM工具远比这个要复杂得多。接下来,我们将构建这样一个简单的桌面应用。
图 12-6 UDP聊天窗口1
请读者在阅读本节内容之前了解与UDP相关的Java API,以及与Java Swing相关的API。本书将不对这些内容做相关细节介绍。
1.构建应用
我们将构建UDPSocket、InitDialog和MainFrame 3个类,分别用于UDP通信协议交互、连接初始化和GUI实现。
❑构建UDPSocket
首先,我们需要构建UDPSocket类,用于控制UDP通信协议的交互。
这里,我们构建了两个用于数据报通信的套接字实例化对象:sendSocket和receiveSocket。sendSocket用于发送消息,receiveSocket用于接收消息,并将发送端口与接收端口分离,通过构造函数提供配置入口。这么做的目的是使得该桌面应用同时具有客户端和服务器端的通信功能。在现实生活中,我们真正使用的QQ、MSN等基本上都是通过一个固定端口与服务器进行交互。客户端轮询请求服务器端,获取消息。
在初始化数据报套接字时,需要注意将数据报接收套接字绑定在本机指定的端口上,而用于数据报发送的套接字则无需绑定。初始化数据报套接字如代码清单12-13所示。
代码清单12-13 初始化数据报套接字
//初始化套接字
this.receiveSocket=new DatagramSocket(new InetSocketAddress(localHost, receivePort));
this.sendSocket=new DatagramSocket();
这里,我们将发送消息和接收消息分为两个方法:send()和receive()。
用于发送消息的方法如代码清单12-14所示:
代码清单12-14 发送消息
/**
*发送消息
*@param data消息
*@throws IOException
*/
public void send(byte[]data)throws IOException{
DatagramPacket dp=new DatagramPacket(buffer, buffer.length,
InetAddress.getByName(remoteHost),sendPort);
dp.setData(data);
sendSocket.send(dp);
}
在上述方法中,我们通过数据报对象绑定目标主机IP和端口号,并执行发送操作。
用于接收消息的方法如代码清单12-15所示:
代码清单12-15 接收消息
/**
*接收消息
*@return byte[]消息
*@throws IOException
*/
public byte[]receive()throws IOException{
DatagramPacket dp=new DatagramPacket(buffer, buffer.length);
receiveSocket.receive(dp);
ByteArrayOutputStream baos=new ByteArrayOutputStream();
baos.write(dp.getData(),0,dp.getLength());
byte[]data=baos.toByteArray();
baos.flush();
baos.close();
return data;
}
与消息发送方法不同,用于消息接收的方法无须在数据报对象中绑定目标主机IP和端口号。我们在初始化receiveSocket对象时,已经将其绑定在本机的指定端口号。
完整代码如代码清单12-16所示。
代码清单12-16 UDPSocket
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
/**
*UDP套接字
*@author梁栋
*@since 1.0
*/
public class UDPSocket{
//缓冲
private byte[]buffer=new byte[1024];
//接收套接字
private DatagramSocket receiveSocket;
//发送套接字
private DatagramSocket sendSocket;
//目标主机
private String remoteHost;
//发送端口
private int sendPort;
/**
*初始化
*@param localHost本地主机IP
*@param remoteHost远程主机IP
*@param receivePort接收端口
*@param sendPort发送端口
*@throws SocketException
*/
public UDPSocket(String localHost, String remoteHost, int receivePort, int
sendPort)throws SocketException{
this.remoteHost=remoteHost;
this.sendPort=sendPort;
//初始化套接字
this.receiveSocket=new DatagramSocket(new InetSocketAddress
(localHost, receivePort));
this.sendSocket=new DatagramSocket();
}
/**
*接收消息
*@return byte[]消息
*@throws IOException
*/
public byte[]receive()throws IOException{
DatagramPacket dp=new DatagramPacket(buffer, buffer.length);
receiveSocket.receive(dp);
ByteArrayOutputStream baos=new ByteArrayOutputStream();
baos.write(dp.getData(),0,dp.getLength());
byte[]data=baos.toByteArray();
baos.flush();
baos.close();
return data;
}
/**
*发送消息
*@param data消息
*@throws IOException
*/
public void send(byte[]data)throws IOException{
DatagramPacket dp=new DatagramPacket(buffer, buffer.length,
InetAddress.getByName(remoteHost),sendPort);
dp.setData(data);
sendSocket.send(dp);
}
/**
*关闭UDP套接字
*/
public void close(){
try{
//关闭接收套接字
if(receiveSocket.isConnected()){
receiveSocket.disconnect();
receiveSocket.close();
}
//关闭发送套接字
if(sendSocket.isConnected()){
sendSocket.disconnect();
sendSocket.close();
}
}catch(Exception ex){
ex.printStackTrace();
}
}
}
❑构建InitDialog
通过上述实现,我们可以构建一个简单的基于UDP协议的通信系统。在此基础上,我们需要通过一些简单的GUI界面配置相关参数。
我们将通过InitDialog类完成目标主机IP、本机IP、发送端口、接收端口和用户昵称5项信息的配置。有关Swing实现,不属于本书阐述内容,请读者参考相关的Java API。完整代码实现如代码清单12-17所示。
代码清单12-17 InitDialog
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
/**
*@author梁栋
*@since 1.0
*/
public class InitDialog extends JDialog{
private static final long serialVersionUID=-8482349275221329655L;
//默认宽度
private static final int DEFAULT_WIDTH=200;
//默认高度
private static final int DEFAULT_HEIGHT=210;
//接收端口
private int receivePort;
//发送端口
private int sendPort;
//用户昵称
private String username;
//目标主机
private String remoteHost;
//本地主机
private String localHost;
//取消状态
private boolean cancelled=true;
//@return the localHost
public String getLocalHost(){
return localHost;
}
//@return the cancelled
public boolean isCancelled(){
return cancelled;
}
//@return the username
public String getUsername(){
return username;
}
//@return the receivePort
public int getReceivePort(){
return receivePort;
}
//@return the sendPort
public int getSendPort(){
return sendPort;
}
//@return the remoteHost
public String getRemoteHost(){
return remoteHost;
}
//@param owner
public InitDialog(Frame owner){
super(owner,"初始化对话框",true);
//初始化文本输入字段
String local;
try{
local=InetAddress.getLocalHost().getHostAddress();
}catch(UnknownHostException e){
local="localhost";
}
final JTextField remoteHostField=new JTextField(local,10);
final JTextField localHostField=new JTextField(local,10);
final JTextField receivePortField=new JTextField("8001",10);
final JTextField sendPortField=new JTextField("8002",10);
final JTextField usernameField=new JTextField("zlex",10);
//构建输入面板
JPanel inputPanel=new JPanel();
inputPanel.setMinimumSize(new Dimension(80,120));
inputPanel.setBorder(BorderFactory.createEtchedBorder());
inputPanel.add(new JLabel("目标主机:"));
inputPanel.add(remoteHostField);
inputPanel.add(new JLabel("本地主机:"));
inputPanel.add(localHostField);
inputPanel.add(new JLabel("接收端口:"));
inputPanel.add(receivePortField);
inputPanel.add(new JLabel("发送端口:"));
inputPanel.add(sendPortField);
inputPanel.add(new JLabel("用户昵称:"));
inputPanel.add(usernameField);
//构建确认按钮
JButton okButton=new JButton("确定");
okButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
//赋值
remoteHost=remoteHostField.getText();
localHost=localHostField.getText();
receivePort=Integer.parseInt(receivePortField.getText());
sendPort=Integer.parseInt(sendPortField.getText());
username=usernameField.getText();
cancelled=false;
InitDialog.this.dispose();
}
});
//构建取消按钮
JButton cancelButton=new JButton("取消");
cancelButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
InitDialog.this.dispose();
}
});
//构建按钮面板
JPanel buttonPanel=new JPanel();
buttonPanel.add(okButton);
buttonPanel.add(cancelButton);
getContentPane().add(inputPanel, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
//设置最小尺寸
setMinimumSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
//设置窗口大小不可调
setResizable(false);
//窗口在屏幕中间显示
setLocationRelativeTo(null);
//显示
setVisible(true);
}
}
❑构建MainFrame
最后,我们将构建一个主体GUI窗口,用于初始化配置、消息的接收/发送等操作。Swing相关实现请读者参考Java API。完整代码如代码清单12-18所示。
代码清单12-18 MainFrame
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.net.SocketException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.text.DefaultCaret;
/**
*UDP协议聊天工具
*@author梁栋
*@since 1.0
*/
public class MainFrame extends JFrame implements Runnable{
private static final long serialVersionUID=647944495306233293L;
//字符集
private static final String CHARSET="UTF-8";
//UDP套接字
private UDPSocket socket;
//初始化对话框
private InitDialog initDialog;
//默认宽度
public static final int DEFAULT_WIDTH=500;
//默认高度
public static final int DEFAULT_HEIGHT=400;
//设置拆分窗格
private JSplitPane splitPane=new JSplitPane(JSplitPane.VERTICAL_SPLIT);
//发送文本域
private JTextArea sendTextArea=new JTextArea();
//接收文本域
private JTextArea receiveTextArea=new JTextArea();
//按钮面板
private JPanel buttonPanel=new JPanel();
/**
*绑定主机和端口
*@throws Exception
*/
public MainFrame(){
//优先启动初始化对话框
this.initDialog=new InitDialog(this);
//若初始化对话框取消,则关闭系统,反之初始化系统
if(initDialog.isCancelled()){
System.exit(0);
}else{
initSocket();
initGUI();
}
}
//初始化套接字
public void initSocket(){
try{
socket=new UDPSocket(initDialog.getLocalHost(),initDialog.getRemoteHost
(),initDialog.getReceivePort(),initDialog.getSendPort());
}catch(SocketException e){
e.printStackTrace();
}
}
//初始化用户界面
public void initGUI(){
setTitle("From:"+initDialog.getLocalHost()+"To:"+initDialog.
getRemoteHost());
setDefaultCloseOperation(EXIT_ON_CLOSE);
setMinimumSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
getContentPane().add(splitPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
initReceivePanel();
initSendPanel();
initButtonPanel();
//窗口在屏幕中间显示
setLocationRelativeTo(null);
setVisible(true);
setResizable(false);
}
//初始化按钮面板
private void initButtonPanel(){
JButton sendButton=new JButton("发送(S)");
sendButton.setMnemonic(KeyEvent.VK_S);
sendButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
try{
send(sendTextArea.getText());
}catch(IOException ioe){
ioe.printStackTrace();
}
}
});
JButton exitButton=new JButton("关闭(X)");
exitButton.setMnemonic(KeyEvent.VK_X);
exitButton.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
socket.close();
System.exit(0);
}
});
buttonPanel.setBorder(BorderFactory.createEtchedBorder());
buttonPanel.add(sendButton);
buttonPanel.add(exitButton);
}
//初始化接收消息面板
private void initReceivePanel(){
receiveTextArea.setEditable(false);
JPanel panel=new JPanel();
panel.setLayout(new BorderLayout());
panel.setBorder(BorderFactory.createEtchedBorder());
JLabel label=new JLabel();
label.setText("接收到的消息:");
panel.add(label, BorderLayout.NORTH);
JScrollPane scrollPane=new JScrollPane();
scrollPane.getViewport().add(receiveTextArea);
DefaultCaret caret=(DefaultCaret)receiveTextArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
panel.add(scrollPane, BorderLayout.CENTER);
panel.setMinimumSize(new Dimension(0,DEFAULT_WIDTH/3));
splitPane.add(panel);
}
//初始化发送消息面板
private void initSendPanel(){
JPanel panel=new JPanel();
panel.setLayout(new BorderLayout());
panel.setBorder(BorderFactory.createEtchedBorder());
JLabel label=new JLabel();
label.setText("待发送的消息:");
panel.add(label, BorderLayout.NORTH);
JScrollPane scrollPane=new JScrollPane();
scrollPane.setWheelScrollingEnabled(true);
DefaultCaret caret=(DefaultCaret)sendTextArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
sendTextArea.addKeyListener(new KeyAdapter(){
public void keyPressed(KeyEvent event){
//Ctrl+Enter组合键
if((event.getKeyCode()==
KeyEvent.VK_ENTER)&&(event.isControlDown())){
try{
send(sendTextArea.getText());
}catch(IOException e){
e.printStackTrace();
}
}
}
});
scrollPane.getViewport().add(sendTextArea);
panel.add(scrollPane, BorderLayout.CENTER);
splitPane.add(panel);
}
/**
*接收消息
*@throws IOException
*/
public void receive()throws IOException{
String message=new String(socket.receive(),CHARSET);
StringBuilder sb=new StringBuilder();
sb.append(receiveTextArea.getText());
sb.append(message);
receiveTextArea.setText(sb.toString());
}
/**
*发送消息
*@param message
*消息
*@throws IOException
*/
public void send(String message)throws IOException{
if(message.isEmpty()){
return;
}
DateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
StringBuilder sendMessage=new StringBuilder();
sendMessage.append(df.format(new Date()));
sendMessage.append("("+initDialog.getUsername()+")");
sendMessage.append("\r\n");
sendMessage.append(message);
sendMessage.append("\r\n");
message=sendMessage.toString();
socket.send(message.getBytes(CHARSET));
StringBuilder receiveMessage=new StringBuilder(receiveTextArea.getText());
receiveMessage.append(sendMessage);
receiveTextArea.setText(receiveMessage.toString());
sendTextArea.setText(null);
}
/*
*(non-Javadoc)
*@see java.lang.Runnable#run()
*/
public void run(){
//循环读
while(true){
try{
receive();
}catch(Exception e){
e.printStackTrace();
}
}
}
/**
*总入口
*@param args
*@throws Exception
*/
public static void main(String[]args)throws Exception{
MainFrame mainFrame=new MainFrame();
Thread t=new Thread(mainFrame);
t.start();
}
}
2.验证服务
执行MainFrame类的main()方法,将获得初始化对话框,如图12-7所示。
这里,作者的主机IP为“192.168.1.2”,欲访问IP为“192.168.1.3”的主机。这里我们使用默认的端口号,将目标主机的值置为“192.168.1.3”,并指定用户昵称“snowolf”。
此外,我们将在IP为“192.168.1.3”的主机上执行MainFrame类的main()方法,进行相关配置,如图12-8所示。
这里,我们需要把目标主机指向IP“192.168.1.2”,将端口互换,并指定用户昵称“zlex”。
图 12-7 初始化对话框1
图 12-8 初始化对话框2
此时,我们可以通过UDP协议进行聊天了,如图12-9所示。“snowolf”和“zlex”互相问候。
图 12-9 UDP聊天窗口2
3.网络监测
在这里,作者将使用网络监测工具Wireshark监测UDP交互数据。作者将Wireshark绑定在本机(IP为192.168.1.2)上,请读者根据实际情况绑定对应网卡。网卡绑定如图12-10所示。
图 12-10 网卡绑定
完成网卡绑定后,在过滤器地址栏中输入过滤信息,如代码清单12-19所示。
代码清单12-19 过滤拦截1
udp&&(udp.port==8001||udp.port==8002)
其中
(udp. port==8001||udp.port==8002)限定UDP端口8001或8002
udp 定UDP协议
执行拦截,我们将拦截到来自“snowolf”的问候:“Hello”,如图12-11所示。
图 12-11 UDP交互消息拦截1
右键单击第一条数据包,在弹出的菜单中选择“Follow UDP Stream”,我们将得到此次聊天的内容,如图12-12所示。
图 12-12 UDP交互内容明文
当然,现在大部分IM工具的协议远比本文描述的纯文本协议更为复杂。但由于协议本身必须公开,通过拦截指定端口UDP协议的数据报,我们完全可以破译交互双方的消息内容。