20162317袁逸灏 第十六周实验报告:实验五 网络编程与安全
20162317袁逸灏 第十六周实验报告:实验五 网络编程与安全
实验内容
- 客户端与服务器
- 加密
实验要求
- 创建一个类实现中缀表达式转化为后缀表达式
- 结队编程:一人负责客户端,另一人负责服务器
- 实现客户端与服务器的互联
- 使用3DES或AES算法来加密后缀表达式
- 使用DH算法来产生共享密钥
- 学会使用MD5来验证
实验过程
一、客户端的实现与互联
- 我的结队对象是20162315马军。经过商议,决定我担当服务器,他来担当客户端。服务器的原代码如下:
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static final int PORT = 12345;//监听的端口号
public static void main(String[] args) {
System.out.println("服务器启动...\n");
Server server = new Server();
server.init();
}
public void init() {
try {
ServerSocket serverSocket = new ServerSocket(PORT);
while (true) {
// 一旦有堵塞, 则表示服务器与客户端获得了连接
Socket client = serverSocket.accept();
// 处理这次连接
new HandlerThread(client);
}
} catch (Exception e) {
System.out.println("服务器异常: " + e.getMessage());
}
}
private class HandlerThread implements Runnable {
private Socket socket;
public HandlerThread(Socket client) {
socket = client;
new Thread(this).start();
}
public void run() {
try {
// 读取客户端数据
DataInputStream input = new DataInputStream(socket.getInputStream());
String clientInputStr = input.readUTF();//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException
// 处理客户端数据
System.out.println("客户端发过来的内容:" + clientInputStr);
// 向客户端回复信息
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
System.out.print("请输入:\t");
// 发送键盘输入的一行
String s = new BufferedReader(new InputStreamReader(System.in)).readLine();
out.writeUTF(s);
out.close();
input.close();
} catch (Exception e) {
System.out.println("服务器 run 异常: " + e.getMessage());
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
socket = null;
System.out.println("服务端 finally 异常:" + e.getMessage());
}
}
}
}
}
}
但光有代码不行,还要去理解,不然就很难往里添加代码,经过一定的了解,懂得了几条关键的代码:
(1)读取客户端发送过来的消息
DataInputStream input = new DataInputStream(socket.getInputStream());
String clientInputStr = input.readUTF();
input.close();
(2)将信息反馈客户端
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
System.out.print("请输入:\t");
// 发送键盘输入的一行
String s = new BufferedReader(new InputStreamReader(System.in)).readLine();
out.writeUTF(s);
out.close();
一读一写是客户端与服务器联系的关键,要进行实验要求的相关操作基本在这一读一写间完成,只要稍微将其中的变量内容丰富或改动即可。
二、开始实验
(1)通过客户端与服务器传送后缀表达式与计算结果
- 实现了服务器与客户端的互联后,我们将写好的MyDC以及MyBC运用至服务器与客户端中。作为服务器,我要看到的是从客户端中传来的后缀表达式,并进行计算,最后反馈给客户端。于是我的代码中的添加部分为:
MyDC md = new MyDC();
if (md.result.getBottom() != 1) {
s = "运算已完成,答案是:"+md.result.getTop() + "/" + md.result.getBottom();
} else if (md.result.getBottom() == 1) {
s = "运算已完成,答案是:"+md.result.getTop() + " ";
}
最后双方测试的效果为:
(2)使用3DES或AES算法对传送的信息进行加密
- 我们挑选了AES算法作为我们的加密方法,加密与解密的总汇代码为:
package HTTP;
/**
* Created by Funny_One on 2017/6/7.
*/
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Scanner;
/*
* AES对称加密和解密
*/
public class SymmetricEncoder {
/*
* 加密
* 1.构造密钥生成器
* 2.根据ecnodeRules规则初始化密钥生成器
* 3.产生密钥
* 4.创建和初始化密码器
* 5.内容加密
* 6.返回字符串
*/
public static String AESEncode(String encodeRules,String content){
try {
//1.构造密钥生成器,指定为AES算法,不区分大小写
KeyGenerator keygen=KeyGenerator.getInstance("AES");
//2.根据ecnodeRules规则初始化密钥生成器
//生成一个128位的随机源,根据传入的字节数组
keygen.init(128, new SecureRandom(encodeRules.getBytes()));
//3.产生原始对称密钥
SecretKey original_key=keygen.generateKey();
//4.获得原始对称密钥的字节数组
byte [] raw=original_key.getEncoded();
//5.根据字节数组生成AES密钥
SecretKey key=new SecretKeySpec(raw, "AES");
//6.根据指定算法AES自成密码器
Cipher cipher=Cipher.getInstance("AES");
//7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作,第二个参数为使用的KEY
cipher.init(Cipher.ENCRYPT_MODE, key);
//8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
byte [] byte_encode=content.getBytes("utf-8");
//9.根据密码器的初始化方式--加密:将数据加密
byte [] byte_AES=cipher.doFinal(byte_encode);
//10.将加密后的数据转换为字符串
//这里用Base64Encoder中会找不到包
//解决办法:
//在项目的Build path中先移除JRE System Library,再添加库JRE System Library,重新编译后就一切正常了。
String AES_encode=new String(new BASE64Encoder().encode(byte_AES));
//11.将字符串返回
return AES_encode;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//如果有错就返加nulll
return null;
}
/*
* 解密
* 解密过程:
* 1.同加密1-4步
* 2.将加密后的字符串反纺成byte[]数组
* 3.将加密内容解密
*/
public static String AESDncode(String encodeRules,String content){
try {
//1.构造密钥生成器,指定为AES算法,不区分大小写
KeyGenerator keygen=KeyGenerator.getInstance("AES");
//2.根据ecnodeRules规则初始化密钥生成器
//生成一个128位的随机源,根据传入的字节数组
keygen.init(128, new SecureRandom(encodeRules.getBytes()));
//3.产生原始对称密钥
SecretKey original_key=keygen.generateKey();
//4.获得原始对称密钥的字节数组
byte [] raw=original_key.getEncoded();
//5.根据字节数组生成AES密钥
SecretKey key=new SecretKeySpec(raw, "AES");
//6.根据指定算法AES自成密码器
Cipher cipher=Cipher.getInstance("AES");
//7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
cipher.init(Cipher.DECRYPT_MODE, key);
//8.将加密并编码后的内容解码成字节数组
byte [] byte_content= new BASE64Decoder().decodeBuffer(content);
/*
* 解密
*/
byte [] byte_decode=cipher.doFinal(byte_content);
String AES_decode=new String(byte_decode,"utf-8");
return AES_decode;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
//如果有错就返加nulll
return null;
}
public static void main(String[] args) {
SymmetricEncoder se=new SymmetricEncoder();
Scanner scanner=new Scanner(System.in);
/*
* 加密
*/
System.out.println("使用AES对称加密,请输入加密的规则");
String encodeRules=scanner.next();
System.out.println("请输入要加密的内容:");
String content = scanner.next();
System.out.println("根据输入的规则"+encodeRules+"加密后的密文是:"+se.AESEncode(encodeRules, content));
/*
* 解密
*/
System.out.println("使用AES对称解密,请输入加密的规则:(须与加密相同)");
encodeRules=scanner.next();
System.out.println("请输入要解密的内容(密文):");
content = scanner.next();
System.out.println("根据输入的规则"+encodeRules+"解密后的明文是:"+se.AESDncode(encodeRules, content));
}
}
效果为:
根据要求,客户端选择的是加密的部分,而服务器要选择解密的部分。我们将总代码进行分离,分别创建了两个类:Encode 以及 Dncode分别作为加密类与解密类。作为服务器,我的Dncode为:
package HTTP;
import sun.misc.BASE64Decoder;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* Created by Funny_One on 2017/6/7.
*/
public class Dncode {
public static String AESDncode(String encodeRules,String content){
try {
//1.构造密钥生成器,指定为AES算法,不区分大小写
KeyGenerator keygen=KeyGenerator.getInstance("AES");
//2.根据ecnodeRules规则初始化密钥生成器
//生成一个128位的随机源,根据传入的字节数组
keygen.init(128, new SecureRandom(encodeRules.getBytes()));
//3.产生原始对称密钥
SecretKey original_key=keygen.generateKey();
//4.获得原始对称密钥的字节数组
byte [] raw=original_key.getEncoded();
//5.根据字节数组生成AES密钥
SecretKey key=new SecretKeySpec(raw, "AES");
//6.根据指定算法AES自成密码器
Cipher cipher=Cipher.getInstance("AES");
//7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
cipher.init(Cipher.DECRYPT_MODE, key);
//8.将加密并编码后的内容解码成字节数组
byte [] byte_content= new BASE64Decoder().decodeBuffer(content);
/*
* 解密
*/
byte [] byte_decode=cipher.doFinal(byte_content);
String AES_decode=new String(byte_decode,"utf-8");
return AES_decode;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
//如果有错就返加nulll
return null;
}
}
这样就可以在Server中直接调用该类来进行解密,我的代码添加部分为:
Dncode de = new Dncode();
Scanner scanner=new Scanner(System.in);
encodeRules=br.readLine();
System.out.println("根据输入的规则"+encodeRules+"\n"+"解密后的明文是:"+de.AESDncode(encodeRules, clientInputStr1));
实验效果为:
(3)使用DH算法进行密钥共享
- 假设有甲乙两方,DH算法就是首先生成双方的公钥与私钥。甲方通过自己的私钥与乙方的公钥来产生一个共享密码,乙方使用自己的私钥以及甲方的公钥可以生成相同的共享密码,从而确保了网络传输的安全性。
代码来源:java密码学算法中的DH算法。
服务器中新添加的代码为:
// 读取对方的DH公钥
FileInputStream f1=new FileInputStream(args[0]);
ObjectInputStream b1=new ObjectInputStream(f1);
PublicKey pbk=(PublicKey)b1.readObject( );
//读取自己的DH私钥
FileInputStream f2=new FileInputStream(args[1]);
ObjectInputStream b2=new ObjectInputStream(f2);
PrivateKey prk=(PrivateKey)b2.readObject( );
// 执行密钥协定
KeyAgreement ka=KeyAgreement.getInstance("DH");
ka.init(prk);
ka.doPhase(pbk,true);
//生成共享信息
byte[ ] sb=ka.generateSecret();
char[] c = new char[sb.length];
for(int n=0;n<c.length;n++){
c[n]=(char)sb[n];
}
FileWriter fw = new FileWriter("Shared_Key.txt");
for(int n=0;n<sb.length;n++){
fw.append(c[n]);
}
fw.flush();
FileReader fr2 = new FileReader("Shared_Key.txt");
BufferedReader br = new BufferedReader(fr2);
encodeRules=br.readLine();
System.out.println("根据输入的规则"+encodeRules+"\n"+"解密后的明文是:"+de.AESDncode(encodeRules, clientInputStr1));
fr.write(de.AESDncode(encodeRules, clientInputStr1));
fr.flush();
效果为:
(4)使用MD5来比较验证
代码来源::java密码学算法中的MD5。
初始我们在逻辑上遇到了问题,我的服务器是一次性接收来自客户端的信息,并将其进行处理后,将最后得到的后缀表达式放进叫"backEquation.txt"的文件中,然后进行读取,再来计算的。但若要加上MD5,我读进的就不止后缀表达式了,还有后缀表达式对应的MD5值,这样就不能正常。在万般无奈下,我们决定做大胆的尝试,客户端再一次对服务器进行写入,服务器对客户端再进行一次读取。在试验下,成功地将MD5以及后缀表达式分开读入,然后我写了一个条件语句,看我自己获得的后缀MD5值是否与客户端的MD5值匹配,若匹配则进行计算,否则不进行计算。
修改后的最终代码为:
package HTTP;
import javax.crypto.KeyAgreement;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Scanner;
public class Server {
public static final int PORT =7373;//监听的端口号
public static void main(String[] args) throws Exception{
// 读取对方的DH公钥
FileInputStream f1=new FileInputStream(args[0]);
ObjectInputStream b1=new ObjectInputStream(f1);
PublicKey pbk=(PublicKey)b1.readObject( );
//读取自己的DH私钥
FileInputStream f2=new FileInputStream(args[1]);
ObjectInputStream b2=new ObjectInputStream(f2);
PrivateKey prk=(PrivateKey)b2.readObject( );
// 执行密钥协定
KeyAgreement ka=KeyAgreement.getInstance("DH");
ka.init(prk);
ka.doPhase(pbk,true);
//生成共享信息
byte[ ] sb=ka.generateSecret();
char[] c = new char[sb.length];
for(int n=0;n<c.length;n++){
c[n]=(char)sb[n];
}
FileWriter fw = new FileWriter("Shared_Key.txt");
for(int n=0;n<sb.length;n++){
fw.append(c[n]);
}
fw.flush();
System.out.println("服务器启动...\n");
Server server = new Server();
server.init();
}
public void init() {
try {
ServerSocket serverSocket = new ServerSocket(PORT);
while (true) {
// 一旦有堵塞, 则表示服务器与客户端获得了连接
Socket client = serverSocket.accept();
// 处理这次连接
new HandlerThread(client);
}
} catch (Exception e) {
System.out.println("服务器异常: " + e.getMessage());
}
}
private class HandlerThread implements Runnable {
private Socket socket;
public HandlerThread(Socket client) {
socket = client;
new Thread(this).start();
}
public void run() {
try {
Dncode de = new Dncode();
Scanner scanner=new Scanner(System.in);
MyDC md = new MyDC();
FileWriter fr = new FileWriter("backEquation.txt");
String encodeRules;
// 读取客户端数据
DataInputStream input = new DataInputStream(socket.getInputStream());
String clientInputStr1 = input.readUTF();//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException
// 处理客户端数据
System.out.println("客户端发过来的内容:" + clientInputStr1);
// System.out.println("使用AES对称解密,请输入加密的规则:(须与加密相同)");
// encodeRules=scanner.next();
FileReader fr2 = new FileReader("Shared_Key.txt");
BufferedReader br = new BufferedReader(fr2);
encodeRules=br.readLine();
System.out.println("根据输入的规则"+encodeRules+"\n"+"解密后的明文是:"+de.AESDncode(encodeRules, clientInputStr1));
fr.write(de.AESDncode(encodeRules, clientInputStr1));
fr.flush();
//获取明文MD5的值
String x=de.AESDncode(encodeRules, clientInputStr1);
MessageDigest m=MessageDigest.getInstance("MD5");
m.update(x.getBytes("UTF8"));
byte bs[ ]=m.digest( );
String result="";
for (int i=0; i<bs.length; i++){
result+=Integer.toHexString((0x000000ff & bs[i]) |
0xffffff00).substring(6);
}
System.out.println("明文MD5的值为:"+result);
//比较MD5的值
String clientInputStr2 = input.readUTF();
String front = "客户端的MD5的值为:";
System.out.println(front+clientInputStr2);
if(clientInputStr2.equals(result)) {
System.out.println("MD5匹配,运算中...");
System.out.println();
md.calQuestion();
// 向客户端回复信息
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
//System.out.print("请输入:\t");
// 发送键盘输入的一行
String s = "";
if (md.result.getBottom() != 1) {
s = "运算已完成,答案是:"+md.result.getTop() + "/" + md.result.getBottom();
} else if (md.result.getBottom() == 1) {
s = "运算已完成,答案是:"+md.result.getTop() + " ";
}
System.out.println(s);
out.writeUTF(s);
out.close();
input.close();
}else {
// 向客户端回复信息
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
//System.out.print("请输入:\t");
// 发送键盘输入的一行
String s = "你不是正宗的客户端,快滚!";
out.writeUTF(s);
out.close();
input.close();
}
} catch (Exception e) {
System.out.println("服务器 run 异常: " + e.getMessage());
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
socket = null;
System.out.println("服务端 finally 异常:" + e.getMessage());
}
}
}
}
}
}
最后的效果也为:
实验知识点
- 客户端与服务器的实现与互联
- AES或3DES的算法
- DH算法
- MD5的使用
实验感悟
- 这次感悟最深的就是深深地体会到了结队编程的好处,以前的结队项目都怕变量之类不同而没有实现真正意义上的结队编程。胆这次的实验可以说是逼着你结队来编程。
- 但是,结队编程很爽。在编程的过程中,我们的商议讨论激起思维的火花的碰撞,使解决问题的方式多样化。此外,没人编自己的部分,且可以不做成干扰的情况下,编程的的速率快了很多。当程序成功运行的时候,双方都有各自的成就感。
- 但是,仅仅靠一个实验还不够,对于很多需要结队编程的项目我们还是无法实现真正的结队。因此,希望以后能够有更多这类结队性强的实验,以此来培养我们结队编程的能力。