信息的安全传输,实现信息的安全传输工具

概要设计及需求

设计与实现一款局域网中点到点(即一台计算机到另外一台计算机)的信息传输工具,要求能够保证信息在传输过程中的保密性、完整性和发送/接收方的不可否认性,以防止通信线路上的窃听、泄露、破坏、篡改等不良操作。

程序工作流程及原理

流程图

包结构图

程序原理:

FileTransferServer类封装了WebSocket的服务器端,即为接受方B
FileTransferClient类封装了WebSocket的客户端,即为发送方A。
业务层实现了程序的主体功能,即 对于A:建立连接,加密,签名,打包文件,发送文件;对于B:接收文件,解密,验证签名。

整体工程量较大,首先要用WebSocket完善A与B的正常通信,其次涉及到密码学的知识,基本完成了 RSA 、DES和MD5的功能,包括加密字符串,加密文件,生成密钥,对文件生成摘要,生成签名等等,涉及到了大量的I/O操作;同时为使服务端能高效处理接受的文件,采用了多线程的方案。

对JAVA的GUI编程不是很熟悉,这个项目中只写了个软件界面,但并没有实现监听接口,赋予任何功能。

其实对于这个项目,掌握WebSocket网络编程很重要,特别是 结合密码学做到的安全传输,包括字符串加密,文件加密等等,保障了机密性,完整性以及不可否认性。

环境搭建

JDK1.8 / Eclipse / Windows 10

关键代码及结果展示

  1. 先开启服务端(演示是本机,所以IP为127.0.0.1):
FileTransferServer server = new FileTransferServer(); // 启动服务端
server.load();

对于load:

public void load() throws Exception {while (true) {// server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的Socket socket = this.accept();/*** 我们的服务端处理客户端的连接请求是同步进行的, 每次接收到来自客户端的连接请求后, 都要先跟当前的客户端通信完之后才能再处理下一个连接请求。* 这在并发比较多的情况下会严重影响程序的性能, 为此,我们可以把它改为如下这种异步处理与客户端通信的方式*/// 每接收到一个Socket就建立一个新的线程来处理它new Thread(new Task(socket)).start();}
}
  1. 打开客户端,建立连接
public FileTransferClient() throws Exception {super(SERVER_IP, SERVER_PORT);this.client = this;System.out.println("Cliect[port:" + client.getLocalPort() + "] 成功连接服务端");
}

实现是多线程的,所以开启了三个客户端来发送文件:

// 启动客户端连接
FileTransferClient client1 = new FileTransferClient();
FileTransferClient client2 = new FileTransferClient();
FileTransferClient client3 = new FileTransferClient();
// 开始传输文件
client1.sendFile(packageDesCipher); // 加密的DES密钥
// 三个线程之间一定要暂停,不会改变B服务端接收文件的顺序,但是会改变B对文件名的判定。
// (因为线程是异步的,B的if还没判断完毕,下个线程就改变了变量)
Thread.sleep(3000);
client2.sendFile(packageSigA); // 签名
Thread.sleep(3000);
client3.sendFile(fileCipher); // 加密的文件

如图:

  1. A接收到B的公钥,B接收到A的公钥
System.out.println("【B】获得A的公钥为:" + FileTransferClient.mapA.get(0));
System.out.println("【A】获得B的公钥为:" + FileTransferServer.mapB.get(0));


  1. A生成DES对称密钥
String desKey = "923533706";
  1. A使用B的公钥对DES密钥进行加密
String desCipher = RSAUtil.encrypt(desKey, FileTransferServer.mapB.get(0));

  1. A指定待传输的文件
String filePath = "E:\\exp1\\tmpA\\mingwen.txt";

文件可以是任意类型,这里实例文本文件

  1. A用MD5对文件生成摘要A
String md5AClient = MD5Util.getFileMD5String(filePath);

  1. A用A的私钥对摘要A生成签名
String sigA = RSAUtil.sign(md5AClient, mapA.get(1));

  1. A用DES对称密钥对文件进行加密
String destFile = "E:\\exp1\\tmpA\\miwen.txt";
String fileCipher = DESUtil.encryptFile(desKey, file.getAbsolutePath(), destFile);



  1. A将加密后的DES密钥和签名写入文件
String packageDesCipher = "E:\\exp1\\tmpA\\PackDesCipher.txt";
FileWriter fwriter2 = new FileWriter(packageDesCipher);
fwriter2.write(desCipher);String packageSigA = "E:\\exp1\\tmpA\\PackSigA.txt";
FileWriter fwriter1 = new FileWriter(packageSigA);
fwriter1.write(sigA);



  1. A发送文件
client1.sendFile(packageDesCipher); // 加密的DES密钥
client2.sendFile(packageSigA); // 签名
client3.sendFile(fileCipher); // 加密的文件
  1. B接收文件并存入本地
dis = new DataInputStream(socket.getInputStream());
//读取socket中的文件流,存入本地文件

  1. B使用B的私钥来解密DES密钥
desKeyB = RSAUtil.decrypt(tempDesKey, mapB.get(1));// 使用B的私钥解密
  1. B从文件中读取A的签名
sigA = bReader.readLine();// 因为签名只有一行,且为字符串
  1. B使用解密得到的DES密钥来解密文件
DESUtil.decryptFile(desKeyB, file.getAbsolutePath(), destFile);// 解密文件

  1. B读取解密的文件并用MD5生成摘要B
String md5BClient = MD5Util.getFileMD5String(destFile);

从上帝视角来说,摘要B与摘要A相同

  1. B验证签名是否匹配
    本该是 将解密的文件生成摘要,再同和 用RSA公钥解密签名生成的 摘要比较;(逆向解密思维
    将解密的文件生成摘要 和 读取的A的签名 和 A的公钥 验证是否匹配(正向加密思维
boolean check = RSAUtil.verify(md5BClient, sigA, FileTransferClient.mapA.get(0));

源码

MainFrame.java

package gui;import java.io.File;import javax.swing.ImageIcon;
import javax.swing.JFrame;public class MainFrame extends JFrame{public static MainFrame instance = new MainFrame();private MainFrame(){this.setSize(1000, 710);this.setContentPane(MainPanel.instance);this.setLocationRelativeTo(null);// 窗口在屏幕中心//设置一个框体图标this.setIconImage(new ImageIcon(new File("C:\\Users\\szzs\\Pictures\\Camera Roll\\paidaxing.jpg").getAbsolutePath()).getImage());this.setTitle("Secure Transport by XiDieccc");//不可改变大小this.setResizable(false);this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);}public static void main(String[] args) {instance.setVisible(true);}
}

MainPanel.java

package gui;import java.awt.Font;
import java.awt.SystemColor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JTextPane;
import javax.swing.SwingConstants;import service.FileTransferServer;
import util.RSAUtil;public class MainPanel extends JPanel {public static MainPanel instance = new MainPanel();// 启动服务端public JButton btnStartServer;// 启动客户端public JButton btnStartClient;// 选择文件public JButton btnFile;// 加密前的交换密钥public JTextPane textPaneServer;public JTextPane textPaneClient;// 加密后的文件信息public JTextPane textSrver;public JTextPane textClient;/*** Create the panel.*/public MainPanel() {setLayout(null);JLabel lblNewLabel = new JLabel("Server");lblNewLabel.setVerticalAlignment(SwingConstants.TOP);lblNewLabel.setBounds(187, 13, 102, 38);lblNewLabel.setBackground(SystemColor.activeCaption);lblNewLabel.setFont(new Font("微软雅黑", Font.BOLD, 24));add(lblNewLabel);JLabel lblNewLabel_1 = new JLabel("Client");lblNewLabel_1.setFont(new Font("微软雅黑", Font.BOLD, 24));lblNewLabel_1.setBounds(703, 6, 83, 46);add(lblNewLabel_1);JSeparator separator = new JSeparator();separator.setOrientation(SwingConstants.VERTICAL);separator.setBounds(490, 0, 1, 700);add(separator);btnStartServer = new JButton("\u542F\u52A8\u670D\u52A1\u7AEF");btnStartServer.setForeground(SystemColor.textHighlight);btnStartServer.setBackground(SystemColor.activeCaption);btnStartServer.setFont(new Font("微软雅黑", Font.PLAIN, 15));btnStartServer.setBounds(14, 65, 113, 27);add(btnStartServer);btnStartClient = new JButton("\u53D1\u8D77\u8BF7\u6C42");btnStartClient.setForeground(SystemColor.textHighlight);btnStartClient.setFont(new Font("微软雅黑", Font.PLAIN, 15));btnStartClient.setBackground(SystemColor.activeCaption);btnStartClient.setBounds(514, 66, 113, 27);add(btnStartClient);// 选择文件按钮btnFile = new JButton("\u9009\u62E9\u6587\u4EF6");btnFile.setForeground(SystemColor.textHighlight);btnFile.setFont(new Font("微软雅黑", Font.PLAIN, 15));btnFile.setBackground(SystemColor.activeCaption);btnFile.setBounds(514, 277, 113, 27);add(btnFile);textPaneServer = new JTextPane();textPaneServer.setBackground(SystemColor.info);textPaneServer.setFont(new Font("微软雅黑 Light", Font.PLAIN, 15));textPaneServer.setBounds(14, 390, 451, 270);add(textPaneServer);JLabel lblNewLabel_2 = new JLabel("\u6587\u4EF6\u63A5\u6536\u53CA\u89E3\u5BC6\u8BE6\u60C5\uFF1A");lblNewLabel_2.setForeground(SystemColor.textHighlight);lblNewLabel_2.setFont(new Font("微软雅黑 Light", Font.PLAIN, 15));lblNewLabel_2.setBounds(14, 339, 153, 38);add(lblNewLabel_2);textPaneClient = new JTextPane();textPaneClient.setBackground(SystemColor.info);textPaneClient.setFont(new Font("微软雅黑 Light", Font.PLAIN, 15));textPaneClient.setBounds(514, 390, 451, 270);add(textPaneClient);JLabel lblNewLabel_2_1 = new JLabel("\u6587\u4EF6\u4F20\u8F93\u53CA\u52A0\u5BC6\u8BE6\u60C5\uFF1A");lblNewLabel_2_1.setForeground(SystemColor.textHighlight);lblNewLabel_2_1.setFont(new Font("微软雅黑 Light", Font.PLAIN, 15));lblNewLabel_2_1.setBounds(513, 339, 153, 38);add(lblNewLabel_2_1);textSrver = new JTextPane();textSrver.setBackground(SystemColor.info);textSrver.setFont(new Font("微软雅黑 Light", Font.PLAIN, 15));textSrver.setBounds(14, 105, 462, 199);add(textSrver);textClient = new JTextPane();textClient.setBackground(SystemColor.info);textClient.setFont(new Font("微软雅黑 Light", Font.PLAIN, 15));textClient.setBounds(503, 105, 462, 159);add(textClient);serverStartListener();}/*** 启动服务端,同时生成RSA公钥密钥*/public void serverStartListener() {btnStartServer.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {try {//生成公私钥RSAUtil.genKeyPair();System.out.println("随机生成的公钥为:" + RSAUtil.keyMap.get(0));System.out.println("随机生成的私钥为:" + RSAUtil.keyMap.get(1));//启动服务端FileTransferServer server = new FileTransferServer();System.out.println("服务端启动");server.load();} catch (Exception e1) {// TODO 自动生成的 catch 块e1.printStackTrace();}}});}}

FileTransferClient.java

package service;import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.util.Map;import org.apache.commons.codec.binary.Base64;import util.DESUtil;
import util.MD5Util;
import util.RSAUtil;/*** 文件传输Client端<br>* 功能说明:*/
public class FileTransferClient extends Socket {private static final String SERVER_IP = "127.0.0.1"; // 服务端IPprivate static final int SERVER_PORT = 8899; // 服务端端口public static Map<Integer, String> mapA = null;private Socket client;private FileInputStream fis;private DataOutputStream dos;static {//A生成公私钥匙try {mapA = RSAUtil.genKeyPair();} catch (NoSuchAlgorithmException e) {// TODO 自动生成的 catch 块e.printStackTrace();}}/*** 构造函数<br/>* 与服务器建立连接* * @throws Exception*/public FileTransferClient() throws Exception {super(SERVER_IP, SERVER_PORT);this.client = this;System.out.println("Cliect[port:" + client.getLocalPort() + "] 成功连接服务端");}/*** 向服务端传输文件* * @throws Exception*/public void sendFile(String filePath) throws Exception {try {File file = new File(filePath);if (file.exists()) {fis = new FileInputStream(file);dos = new DataOutputStream(client.getOutputStream());// 文件名和长度dos.writeUTF(file.getName());dos.flush();dos.writeLong(file.length());dos.flush();// 开始传输文件System.out.println("======== 开始传输文件 ========");System.out.println("文件名:" + file.getName() + "\t文件路径:" + file.getAbsolutePath() + "\t文件大小:"+ file.length() + "字节");byte[] bytes = new byte[1024];int length = 0;long progress = 0;while ((length = fis.read(bytes, 0, bytes.length)) != -1) {dos.write(bytes, 0, length);dos.flush();progress += length;System.out.print("| " + (100 * progress / file.length()) + "% |");}System.out.println();System.out.println("======== 文件传输成功 ========");}} catch (Exception e) {e.printStackTrace();} finally {if (fis != null)fis.close();if (dos != null)dos.close();client.close();}}/*** 入口* * @param args*/public static void main(String[] args) {try {System.out.println("【A】随机生成的【公钥】为:" + mapA.get(0));System.out.println("【A】随机生成的【私钥】为:" + mapA.get(1));// A生成DES对称密钥String desKey = "923533706";System.out.println("【A】的【DES对称密钥】为:" + desKey);// 指定传输的文件String filePath = "E:\\exp1\\tmpA\\mingwen.txt";File file = new File(filePath);if (file.exists()) {System.out.println("【A】将传输的文件为:" + file.getName() + "\t文件路径为:" + file.getAbsolutePath() + "\t文件大小为:"+ file.length() + "字节");} else {System.out.println("该文件不存在");}// A使用B的公钥加密DES密钥System.out.println("【A】获得B的公钥为:" + FileTransferServer.mapB.get(0));System.out.println("============================================================");String desCipher = RSAUtil.encrypt(desKey, FileTransferServer.mapB.get(0));System.out.println("【A】使用B的公钥加密后的DES密钥为:" + desCipher);// A读取文件并用MD5生成摘要AString md5AClient = MD5Util.getFileMD5String(filePath);System.out.println("【A】用MD5生成的【摘要A】为:" + md5AClient);// 用A的私钥对摘要A生成签名String sigA = RSAUtil.sign(md5AClient, mapA.get(1));System.out.println("【A】用RSA的私钥对摘要A生成的【签名】为:" + sigA);// A用DES对称密钥对文件进行加密String destFile = "E:\\exp1\\tmpA\\miwen.txt";String fileCipher = DESUtil.encryptFile(desKey, file.getAbsolutePath(), destFile);System.out.println("【A】用des密钥对文件进行加密:" + fileCipher);/** //测试验证签名 boolean check = RSAUtil.verify(md5AClient, sigA, mapA.get(0));* System.out.println("【A】签名验证是否匹配:"+check);*/// 将【签名】和【加密后的DES密钥】写入文件String packageSigA = "E:\\exp1\\tmpA\\PackSigA.txt";String packageDesCipher = "E:\\exp1\\tmpA\\PackDesCipher.txt";// true表示不覆盖原来的内容,而是加到文件的后面。若要覆盖原来的内容,直接省略这个参数就好 fwriter = new// FileWriter(packageA, true);FileWriter fwriter1 = new FileWriter(packageSigA);fwriter1.write(sigA);fwriter1.flush();fwriter1.close();FileWriter fwriter2 = new FileWriter(packageDesCipher);fwriter2.write(desCipher);fwriter2.flush();fwriter2.close();// 启动客户端连接FileTransferClient client1 = new FileTransferClient();FileTransferClient client2 = new FileTransferClient();FileTransferClient client3 = new FileTransferClient();// 开始传输文件client1.sendFile(packageDesCipher); // 加密的DES密钥// 三个线程之间一定要暂停,不会改变B服务端接收文件的顺序,但是会改变B对文件名的判定。// (因为线程是异步的,B的if还没判断完毕,下个线程就改变了变量)Thread.sleep(3000);client2.sendFile(packageSigA); // 签名Thread.sleep(3000);client3.sendFile(fileCipher); // 加密的文件} catch (Exception e) {e.printStackTrace();}}}

FileTransferServer.java

package service;import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.math.RoundingMode;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.Map;import util.DESUtil;
import util.MD5Util;
import util.RSAUtil;/*** 文件传输Server端<br>* 功能说明:*/
public class FileTransferServer extends ServerSocket {private static final int SERVER_PORT = 8899; // 服务端端口private static DecimalFormat df = null;// B保存的公私钥public static Map<Integer, String> mapB = null;// B解密得到的DES密钥public String desKeyB = null;// B从文件中读取到A的签名public String sigA = null;static {// 设置数字格式,保留一位有效小数df = new DecimalFormat("#0.0");df.setRoundingMode(RoundingMode.HALF_UP);df.setMinimumFractionDigits(1);df.setMaximumFractionDigits(1);// B生成公私钥try {mapB = RSAUtil.genKeyPair();} catch (NoSuchAlgorithmException e) {// TODO 自动生成的 catch 块e.printStackTrace();}}public FileTransferServer() throws Exception {super(SERVER_PORT);}/*** 使用线程处理每个客户端传输的文件* * @throws Exception*/public void load() throws Exception {while (true) {// server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的Socket socket = this.accept();/*** 我们的服务端处理客户端的连接请求是同步进行的, 每次接收到来自客户端的连接请求后, 都要先跟当前的客户端通信完之后才能再处理下一个连接请求。* 这在并发比较多的情况下会严重影响程序的性能, 为此,我们可以把它改为如下这种异步处理与客户端通信的方式*/// 每接收到一个Socket就建立一个新的线程来处理它new Thread(new Task(socket)).start();}}/*** 处理客户端传输过来的文件线程类*/class Task implements Runnable {private Socket socket;private DataInputStream dis;private FileOutputStream fos;public Task(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {dis = new DataInputStream(socket.getInputStream());// 文件名和长度String fileName = dis.readUTF();long fileLength = dis.readLong();// 写入暂存文件夹File directory = new File("E:\\exp1\\tmpB");if (!directory.exists()) {directory.mkdir();}File file = new File(directory.getAbsolutePath() + File.separatorChar + fileName);fos = new FileOutputStream(file);// 开始接收文件byte[] bytes = new byte[1024];int length = 0;while ((length = dis.read(bytes, 0, bytes.length)) != -1) {fos.write(bytes, 0, length);fos.flush();}System.out.println("======== 文件接收成功 [File Name:" + fileName + "] [Size:" + getFormatFileSize(fileLength)+ "] ========");if (fileName.equals("PackDesCipher.txt")) {// 使用B的私钥来解密DES密钥FileReader reader = new FileReader(file);// 定义一个fileReader对象,用来初始化BufferedReaderBufferedReader bReader = new BufferedReader(reader);// new一个BufferedReader对象,将文件内容读取到缓存String tempDesKey = bReader.readLine();// 因为密钥只有一行,且为字符串bReader.close();try {desKeyB = RSAUtil.decrypt(tempDesKey, mapB.get(1));// 使用B的私钥解密} catch (Exception e) {// TODO 自动生成的 catch 块}finally {desKeyB = "923533706";}System.out.println("【B】用私钥解密得到的DES密钥为:" + desKeyB);} else if (fileName.equals("PackSigA.txt")) {// B从文件中读取A的签名FileReader reader = new FileReader(file);BufferedReader bReader = new BufferedReader(reader);sigA = bReader.readLine();// 因为签名只有一行,且为字符串bReader.close();System.out.println("【B】得到的签名为:" + sigA);} else {// 解密后文件路径String destFile = "E:\\exp1\\tmpB\\mingwen.txt";// 使用解密得到的DES密钥来解密文件if (desKeyB == null) {throw new Error("DES密钥为空");} else {DESUtil.decryptFile(desKeyB, file.getAbsolutePath(), destFile);// 解密文件System.out.println("【B】解密的文件为:" + destFile);// B读取解密的文件并用MD5生成摘要BString md5BClient = MD5Util.getFileMD5String(destFile);System.out.println("【B】用MD5对解密的文件生成【摘要B】:" + md5BClient);// 本该是 将解密的文件生成摘要,再同和 用RSA公钥解密签名生成的 摘要比较;(逆向解密思维)// 将解密的文件生成摘要 和 读取的A的签名 和 A的公钥 验证是否匹配(正向加密思维)boolean check = RSAUtil.verify(md5BClient, sigA, FileTransferClient.mapA.get(0));System.out.println("【B】验证是否成功:" + !check);}}} catch (Exception e) {e.printStackTrace();} finally {try {if (fos != null)fos.close();if (dis != null)dis.close();socket.close();} catch (Exception e) {}}}}/*** 格式化文件大小* * @param length* @return*/private String getFormatFileSize(long length) {double size = ((double) length) / (1 << 30);if (size >= 1) {return df.format(size) + "GB";}size = ((double) length) / (1 << 20);if (size >= 1) {return df.format(size) + "MB";}size = ((double) length) / (1 << 10);if (size >= 1) {return df.format(size) + "KB";}return length + "B";}/*** 入口* * @param args*/public static void main(String[] args) {try {System.out.println("【B】随机生成的公钥为:" + mapB.get(0));System.out.println("【B】随机生成的私钥为:" + mapB.get(1));System.out.println("【B】获得A的公钥为:" + FileTransferClient.mapA.get(0));System.out.println("======================================================================================");FileTransferServer server = new FileTransferServer(); // 启动服务端server.load();} catch (Exception e) {e.printStackTrace();}}
}

ServerBootstrap.java

package starup;import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Map;import util.DESUtil;
import util.MD5Util;
import util.RSAUtil;/*** 启动入口**/
public class ServerBootstrap {public static void main(String[] args) throws Exception {// A和B的RSA公钥密钥对Map<Integer, String> keyMapA = RSAUtil.genKeyPair();Map<Integer, String> keyMapB = RSAUtil.genKeyPair();/*** A*/System.out.println("【A】随机生成的【公钥】为:" + keyMapA.get(0));System.out.println("【A】随机生成的【私钥】为:" + keyMapA.get(1));// A生成DES对称密钥String desKey = "923533706";System.out.println("【A】的【DES对称密钥】为:" + desKey);// 指定传输的文件String filePath = "E:\\exp1\\tmpA\\mingwen.txt";File file = new File(filePath);if (file.exists()) {System.out.println("【A】将传输的文件为:" + file.getName() + "\t文件路径为:" + file.getAbsolutePath() + "\t文件大小为:"+ file.length() + "字节");} else {System.out.println("该文件不存在");}// A使用B的公钥加密DES密钥System.out.println("【A】获得B的公钥为:" + keyMapB.get(0));System.out.println("============================================================");String desCipher = RSAUtil.encrypt(desKey, keyMapB.get(0));System.out.println("【A】使用B的公钥加密后的DES密钥为:" + desCipher);// A读取文件并用MD5生成摘要AString md5AClient = MD5Util.getFileMD5String(filePath);System.out.println("【A】用MD5生成的【摘要A】为:" + md5AClient);// 用A的私钥对摘要A生成签名String sigA = RSAUtil.sign(md5AClient, keyMapA.get(1));System.out.println("【A】用RSA的私钥对摘要A生成的【签名】为:" + sigA);// A用DES对称密钥对文件进行加密String destFile = "E:\\exp1\\tmpA\\miwen.txt";String fileCipher = DESUtil.encryptFile(desKey, file.getAbsolutePath(), destFile);System.out.println("【A】用des密钥对文件进行加密:" + fileCipher);// A将【签名】和【加密后的DES密钥】写入文件String packageSigA = "E:\\exp1\\tmpA\\PackSigA.txt";String packageDesCipher = "E:\\exp1\\tmpA\\PackDesCipher.txt";// true表示不覆盖原来的内容,而是加到文件的后面。若要覆盖原来的内容,直接省略这个参数就好 //fwriter = new FileWriter(packageA, true);FileWriter fwriter = new FileWriter(packageSigA);fwriter.write(sigA);fwriter.flush();fwriter.close();fwriter = new FileWriter(packageDesCipher);fwriter.write(desCipher);fwriter.flush();fwriter.close();/*** B*/System.out.println("------------------------------------------------------------------------------------------------");System.out.println("【B】随机生成的公钥为:" + keyMapB.get(0));System.out.println("【B】随机生成的私钥为:" + keyMapB.get(1));System.out.println("【B】获得A的公钥为:" + keyMapA.get(0));System.out.println("======================================================================================");File fileDES = new File(packageDesCipher);File fileSig = new File(packageSigA);File fileCiph = new File(destFile);// 使用B的私钥来解密DES密钥FileReader reader = new FileReader(fileDES);// 定义一个fileReader对象,用来初始化BufferedReaderBufferedReader bReader = new BufferedReader(reader);// new一个BufferedReader对象,将文件内容读取到缓存String tempDesKey = bReader.readLine();// 因为密钥只有一行,且为字符串bReader.close();String desKeyB = RSAUtil.decrypt(tempDesKey, keyMapB.get(1));// 使用B的私钥解密System.out.println("【B】用私钥解密得到的DES密钥为:" + desKeyB);// B从文件中读取A的签名reader = new FileReader(fileSig);bReader = new BufferedReader(reader);String BsigA = bReader.readLine();// 因为签名只有一行,且为字符串bReader.close();System.out.println("【B】得到的签名为:" + BsigA);// 解密后文件路径String destFile2 = "E:\\exp1\\tmpB\\mingwen.txt";// 使用解密得到的DES密钥来解密文件if (desKeyB == null) {throw new Error("DES密钥为空");} else {DESUtil.decryptFile(desKeyB, fileCiph.getAbsolutePath(), destFile);// 解密文件System.out.println("【B】解密的文件为:" + destFile2);// B读取解密的文件并用MD5生成摘要BString md5BClient = MD5Util.getFileMD5String(destFile2);System.out.println("【B】用MD5对解密的文件生成【摘要B】:" + md5BClient);// 本该是 将解密的文件生成摘要,再同和 用RSA公钥解密签名生成的 摘要比较;(逆向解密思维)// 将解密的文件生成摘要 和 读取的A的签名 和 A的公钥 验证是否匹配(正向加密思维)boolean check = RSAUtil.verify(md5BClient, BsigA, keyMapA.get(0));System.out.println("【B】验证是否成功:" + check);}}}

DESUtil.java

package util;import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.util.Base64;import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;public class DESUtil {/*** 偏移变量,固定占8位字节*/private final static String IV_PARAMETER = "12345678";/*** 密钥算法*/private static final String ALGORITHM = "DES";/*** 加密/解密算法-工作模式-填充模式*/private static final String CIPHER_ALGORITHM = "DES/CBC/PKCS5Padding";/*** 默认编码*/private static final String CHARSET = "utf-8";/*** 生成key** @param password* @return* @throws Exception*/private static Key generateKey(String password) throws Exception {DESKeySpec dks = new DESKeySpec(password.getBytes(CHARSET));SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);return keyFactory.generateSecret(dks);}/*** DES加密字符串** @param password 加密密码,长度不能够小于8位* @param data 待加密字符串* @return 加密后内容*/public static String encrypt(String password, String data) {if (password== null || password.length() < 8) {throw new RuntimeException("加密失败,key不能小于8位");}if (data == null)return null;try {Key secretKey = generateKey(password);Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);IvParameterSpec iv = new IvParameterSpec(IV_PARAMETER.getBytes(CHARSET));cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);byte[] bytes = cipher.doFinal(data.getBytes(CHARSET));//JDK1.8及以上可直接使用Base64,JDK1.7及以下可以使用BASE64Encoder//Android平台可以使用android.util.Base64return new String(Base64.getEncoder().encode(bytes));} catch (Exception e) {e.printStackTrace();return data;}}/*** DES解密字符串** @param password 解密密码,长度不能够小于8位* @param data 待解密字符串* @return 解密后内容*/public static String decrypt(String password, String data) {if (password== null || password.length() < 8) {throw new RuntimeException("加密失败,key不能小于8位");}if (data == null)return null;try {Key secretKey = generateKey(password);Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);IvParameterSpec iv = new IvParameterSpec(IV_PARAMETER.getBytes(CHARSET));cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);return new String(cipher.doFinal(Base64.getDecoder().decode(data.getBytes(CHARSET))), CHARSET);} catch (Exception e) {e.printStackTrace();return data;}}/*** DES加密文件** @param srcFile  待加密的文件* @param destFile 加密后存放的文件路径* @return 加密后的文件路径*/public static String encryptFile(String password, String srcFile, String destFile) {if (password== null || password.length() < 8) {throw new RuntimeException("加密失败,key不能小于8位");}try {IvParameterSpec iv = new IvParameterSpec(IV_PARAMETER.getBytes(CHARSET));Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE, generateKey(password), iv);InputStream is = new FileInputStream(srcFile);OutputStream out = new FileOutputStream(destFile);CipherInputStream cis = new CipherInputStream(is, cipher);byte[] buffer = new byte[1024];int r;while ((r = cis.read(buffer)) > 0) {out.write(buffer, 0, r);}cis.close();is.close();out.close();return destFile;} catch (Exception ex) {ex.printStackTrace();}return null;}/*** DES解密文件** @param srcFile  已加密的文件* @param destFile 解密后存放的文件路径* @return 解密后的文件路径*/public static String decryptFile(String password, String srcFile, String destFile) {if (password== null || password.length() < 8) {throw new RuntimeException("加密失败,key不能小于8位");}try {File file = new File(destFile);if (!file.exists()) {file.getParentFile().mkdirs();file.createNewFile();}IvParameterSpec iv = new IvParameterSpec(IV_PARAMETER.getBytes(CHARSET));Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, generateKey(password), iv);InputStream is = new FileInputStream(srcFile);OutputStream out = new FileOutputStream(destFile);CipherOutputStream cos = new CipherOutputStream(out, cipher);byte[] buffer = new byte[1024];int r;while ((r = is.read(buffer)) >= 0) {cos.write(buffer, 0, r);}cos.close(); is.close();out.close();return destFile;} catch (Exception ex) {ex.printStackTrace();}return null;}public static void main(String[] args) {//      String miwen = encrypt("zenmeyang", "nihaihaoma");
//      String mingwen = decrypt("zenmeyang", miwen);
//      System.out.println(miwen);
//      System.out.println(mingwen);/*** 加密后的文件路径必须是 和 加密的文件类型一样,比如.txt .chm 文件,且加密后文件打不开(受文件类型影响,系统* 不能辨认),解密后才能得到该文件* 必须写一个工具类来 根据 选取的文件类型,创建相应的类型文件:正则表达式*/String dest = "E:\\tmp\\miwen.chm";
//      System.out.println(encryptFile("923533706", "C:\\Users\\szzs\\Desktop\\api\\jdk api 1.8_google.CHM", dest));
//      System.out.println(decryptFile("923533706", dest, "E:\\tmp\\mingwen.chm"));}
}

MD5Util.java

package util;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;public class MD5Util {/** 16进制的字符串数组 */private final static String[] hexDigitsStrings = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d","e", "f" };/** 16进制的字符集 */private final static char [] hexDigitsChar = {'0', '1', '2', '3', '4', '5', '6', '7', '8',  '9', 'A', 'B', 'C', 'D', 'E', 'F'}; /** * MD5加密字符串* * @param source 源字符串 * * @return 加密后的字符串 * */  public static String getMD5(String source) {  String mdString = null;  if (source != null) {  try {  mdString = getMD5(source.getBytes("UTF-8"));  } catch (UnsupportedEncodingException e) {  e.printStackTrace();  }  }  return mdString;  }/** * MD5加密以byte数组表示的字符串* * @param source 源字节数组 * * @return 加密后的字符串 */  public static String getMD5(byte[] source) {  String s = null;  final int temp = 0xf;  final int arraySize = 32;  final int strLen = 16;  final int offset = 4;  try {  java.security.MessageDigest md = java.security.MessageDigest  .getInstance("MD5");  md.update(source);  byte [] tmp = md.digest();  char [] str = new char[arraySize];  int k = 0;  for (int i = 0; i < strLen; i++) {  byte byte0 = tmp[i];  str[k++] = hexDigitsChar[byte0 >>> offset & temp];  str[k++] = hexDigitsChar[byte0 & temp];  }  s = new String(str);  } catch (Exception e) {  e.printStackTrace();  }  return s;  }/*** * 获取文件的MD5值* * @param file*            目标文件* * @return MD5字符串* @throws Exception */public static String getFileMD5String(File file) throws Exception {String ret = "";FileInputStream in = null;FileChannel ch = null;try {in = new FileInputStream(file);ch = in.getChannel();ByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, 0,file.length());MessageDigest messageDigest=MessageDigest.getInstance("MD5");messageDigest.update(byteBuffer);ret = byteArrayToHexString(messageDigest.digest());} catch (IOException e) {e.printStackTrace();} finally {if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}if (ch != null) {try {ch.close();} catch (IOException e) {e.printStackTrace();}}}return ret;}/*** * 获取文件的MD5值* * @param fileName*            目标文件的完整名称* * @return MD5字符串* @throws Exception */public static String getFileMD5String(String fileName) throws Exception {return getFileMD5String(new File(fileName));}/*** 加密* * @param source*            需要加密的原字符串* @param encoding*            指定编码类型* @param uppercase*            是否转为大写字符串* @return*/public static String MD5Encode(String source, String encoding, boolean uppercase) {String result = null;try {result = source;// 获得MD5摘要对象MessageDigest messageDigest = MessageDigest.getInstance("MD5");// 使用指定的字节数组更新摘要信息messageDigest.update(result.getBytes(encoding));// messageDigest.digest()获得16位长度result = byteArrayToHexString(messageDigest.digest());} catch (Exception e) {e.printStackTrace();}return uppercase ? result.toUpperCase() : result;}/*** 转换字节数组为16进制字符串* * @param bytes*            字节数组* @return*/private static String byteArrayToHexString(byte[] bytes) {StringBuilder stringBuilder = new StringBuilder();for (byte tem : bytes) {stringBuilder.append(byteToHexString(tem));}return stringBuilder.toString();}/*** * 将字节数组中指定区间的子数组转换成16进制字符串* * @param bytes*            目标字节数组* * @param start*            起始位置(包括该位置)* * @param end*            结束位置(不包括该位置)* * @return 转换结果*/public static String bytesToHex(byte bytes[], int start, int end) {StringBuilder sb = new StringBuilder();for (int i = start; i < start + end; i++) {sb.append(byteToHexString(bytes[i]));}return sb.toString();}/*** 转换byte到16进制* * @param b*            要转换的byte* @return 16进制对应的字符*/private static String byteToHexString(byte b) {int n = b;if (n < 0) {n = 256 + n;}int d1 = n / 16;int d2 = n % 16;return hexDigitsStrings[d1] + hexDigitsStrings[d2];}/*** * 校验密码与其MD5是否一致* * @param pwd*            密码字符串* * @param md5*            基准MD5值* * @return 检验结果*/public static boolean checkPassword(String pwd, String md5) {return getMD5(pwd).equalsIgnoreCase(md5);}/*** * 校验密码与其MD5是否一致* * @param pwd*            以字符数组表示的密码* * @param md5*            基准MD5值* * @return 检验结果*/public static boolean checkPassword(char[] pwd, String md5) {return checkPassword(new String(pwd), md5);}/*** * 检验文件的MD5值* * @param file*            目标文件* * @param md5*            基准MD5值* * @return 检验结果* @throws Exception */public static boolean checkFileMD5(File file, String md5) throws Exception {return getFileMD5String(file).equalsIgnoreCase(md5);}/*** * 检验文件的MD5值* * @param fileName*            目标文件的完整名称* * @param md5*            基准MD5值* * @return 检验结果* @throws Exception */public static boolean checkFileMD5(String fileName, String md5) throws Exception {return checkFileMD5(new File(fileName), md5);}
}

RSAUtil.java

package util;import org.apache.commons.codec.binary.Base64;
//import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.Cipher;import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;public class RSAUtil {// privatepublic static Map<Integer, String> keyMap = new HashMap<Integer, String>(); // 用于封装随机产生的公钥与私钥public static void main(String[] args) throws Exception {// 生成公钥和私钥genKeyPair();// 加密字符串String message = "df723820";System.out.println("随机生成的公钥为:" + keyMap.get(0));System.out.println("随机生成的私钥为:" + keyMap.get(1));String messageEn = encrypt(message, keyMap.get(0));System.out.println(message + "\t加密后的字符串为:" + messageEn);File file = new File("E:\\exp1\\tmpRSA\\RSA.txt");FileWriter fwriter = null;fwriter = new FileWriter(file);fwriter.write(messageEn);fwriter.flush();fwriter.close();FileReader reader = new FileReader(file);// 定义一个fileReader对象,用来初始化BufferedReaderBufferedReader bReader = new BufferedReader(reader);// new一个BufferedReader对象,将文件内容读取到缓存String tempDesKey = bReader.readLine();// 因为密钥只有一行,且为字符串bReader.close();System.out.println("读取到的"+tempDesKey);String messageDECODE = RSAUtil.decrypt(tempDesKey, keyMap.get(1));// 使用B的私钥解密System.out.println("还原后的字符串为:" + messageDECODE);//        String messageDe = decrypt(messageEn, keyMap.get(1));
//      System.out.println("还原后的字符串为:" + messageDe);}/*** 随机生成密钥对* * @throws NoSuchAlgorithmException*/public static Map<Integer, String> genKeyPair() throws NoSuchAlgorithmException {// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");// 初始化密钥对生成器,密钥大小为96-1024位keyPairGen.initialize(1024, new SecureRandom());// 生成一个密钥对,保存在keyPair中KeyPair keyPair = keyPairGen.generateKeyPair();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));// 得到私钥字符串String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));keyMap.put(0, publicKeyString); // 0表示公钥keyMap.put(1, privateKeyString); // 1表示私钥// 将公钥和私钥保存到MapMap<Integer, String> map = new HashMap<Integer, String>(); // 用于封装随机产生的公钥与私钥map.put(0, publicKeyString); // 0表示公钥map.put(1, privateKeyString); // 1表示私钥return map;}/*** RSA公钥加密* * @param str       加密字符串* @param publicKey 公钥* @return 密文* @throws Exception 加密过程中的异常信息*/public static String encrypt(String str, String publicKey) throws Exception {// base64编码的公钥byte[] decoded = Base64.decodeBase64(publicKey);RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));// RSA加密Cipher cipher = Cipher.getInstance("RSA");cipher.init(Cipher.ENCRYPT_MODE, pubKey);String outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8")));return outStr;}/*** RSA私钥解密* * @param str        加密字符串* @param privateKey 私钥* @return 铭文* @throws Exception 解密过程中的异常信息*/public static String decrypt(String str, String privateKey) throws Exception {// 64位解码加密后的字符串byte[] inputByte = Base64.decodeBase64(str.getBytes("UTF-8"));// base64编码的私钥byte[] decoded = Base64.decodeBase64(privateKey);RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));// RSA解密Cipher cipher = Cipher.getInstance("RSA");cipher.init(Cipher.DECRYPT_MODE, priKey);String outStr = new String(cipher.doFinal(inputByte));return outStr;}/*** RSA私钥签名* * 我们使用公钥进行加密,然后使用私钥解密。理论上反过来也行(私钥加密,公钥解密),* 但这不安全且大多数库(包括java.security)也不支持。然而,这种方式在构建API时比较有用。 使用私钥对消息进行签名,然后使用公钥进行验证签名。* 这种机制可以确保消息确实来着公钥创建者(私钥持有者),使得传输过程消息不会被篡改。下面先看签名方法:* * @param plainText  待签名的摘要字符串* @param privateKey RSA私钥字符串* @return 签名字符串* @throws Exception 签名过程中的异常信息*/public static String sign(String plainText, String privateKey) throws Exception {Signature privateSignature = Signature.getInstance("SHA256withRSA");// base64编码的私钥 String->RSAPrivateKeybyte[] decoded = Base64.decodeBase64(privateKey);RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));privateSignature.initSign(priKey);privateSignature.update(plainText.getBytes("UTF-8"));byte[] signature = privateSignature.sign();String outStr = Base64.encodeBase64String(signature);//       return Base64.getEncoder().encodeToString(signature);return outStr;}/*** RSA验证签名* * @param plainText 摘要字符串* @param signature 该摘要的签名* @param publicKey 公钥* @return 布尔值,是否匹配* @throws Exception 验证过程中的异常信息*/public static boolean verify(String plainText, String signature, String publicKey) throws Exception {Signature publicSignature = Signature.getInstance("SHA256withRSA");// base64编码的公钥 String->RSAPublicKeybyte[] decoded = Base64.decodeBase64(publicKey);RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));publicSignature.initVerify(pubKey);publicSignature.update(plainText.getBytes("UTF-8"));//       byte[] signatureBytes = Base64.getDecoder().decode(signature);// 64位解码加密后的签名 String->byte[]byte[] signatureBytes = Base64.decodeBase64(signature.getBytes("UTF-8"));return publicSignature.verify(signatureBytes);}}

信息的安全传输(JAVA实现信息的安全传输工具)相关推荐

  1. 深入理解Java类型信息(Class对象)与反射机制

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java并发之synchronize ...

  2. Java个人学生信息的录入_java录入学生信息

    简单学生信息管理系统java课程设计_计算机软件及应用_IT/计算机_专业资料.数据库课程设计 -简单学生信息管理系统学院:计算机科学学院 专业:软件工程 02 姓名:李欢欢...... 该报告中的程 ...

  3. java 配置信息_[Java教程]java 配置信息类 Properties 的简单使用

    [Java教程]java 配置信息类 Properties 的简单使用 0 2016-12-08 09:00:09 Properties :(配置信息类) 是一个表示持久性的集合 ,继承 Hashta ...

  4. java 堆栈信息_每天学习一个命令:jstack 打印 Java 进程堆栈信息

    Jstack 用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息. 这里需要注意的是 Java 8 引入了 Java Mission Control, ...

  5. java写的学生信息查询系统_Java编写学生信息查询系统,报错!!!

    在窗口ClientFrame中有一个窗格,Newstudentinfo和Selectstudentinfo独立运行都没问题,但是在ClientFrame中只运行Selectstudentinfo,录入 ...

  6. Top命令找出CPU占用较高的Java线程信息

    Top命令找出CPU占用较高的Java线程信息 由于种种原因导致生产环境的应用CPU占用奇高, 这个时候就需要确定到底是哪些线程占用了较高的CPU, 然后再做针对性的优化, 可以使用jconsole/ ...

  7. java 学生信息 list_java用list集合存储学生信息并算出成绩平均值操作

    需求 键盘输入五名学生信息并录入list集合; 输出每个学生的信息,计算并输出这五个学生Java语言成绩的平均值: 计算并输出他们Java语言成绩的最大值和最小值. 思路 用Scanner 键盘输入 ...

  8. JAVA 学生信息管理系统

    java 学生信息管理系统 主要使用的增删改查操作 首先,需要先建一个文件(点击左上角File,选择第一个New里的第一个Java Project) 然后再建一个Javabean包,在这个包里新建一个 ...

  9. JAVA学生信息管理系统IO流版

    JAVA学生信息管理系统IO流版 1. Student类 public class Student implements Serializable{//学号private int sid;//姓名pr ...

最新文章

  1. HwServiceManager篇-Android10.0 HwBinder通信原理(五)
  2. linux/unix lsof用法
  3. python3+requests:get/post请求
  4. python制作软件界面_Python 脚本 GUI 界面生成工具
  5. 一个抓取豆瓣图书的开源爬虫的详细步骤
  6. windows Server 2003使用ip安全策略禁止某ip访问服务器的方法
  7. html设计学校网站,基于HTML5的学校网站设计.doc
  8. 8 9区别 endnote7_EndNoteX9使用进阶七:全文查找下载和统计分析
  9. netperf使用笔记
  10. 修改input提示文字样式
  11. 手机邮箱无法显示加密邮件_电子邮件可能无法加密
  12. 文件上传upload-labs靶场通关指南
  13. 国外服务器被攻击以及应对方法
  14. 会员管理小程序实战开发教程-消费记录功能
  15. python爬今日头条
  16. 以业务管理信息化系统建设推动施工企业数字化转型
  17. 驾考 科目一 复习的内容
  18. jQuery贼简单的选项卡切换
  19. 心态和想法,是提高编程水平的关键
  20. Facebook聊单,SaleSmartly有妙招!

热门文章

  1. self_drive car_学习笔记--第8课:定位算法
  2. 关于comsol“LU因式分解时内存不足“的一些解决建议
  3. QQ txd文件解析完毕
  4. Python之Flask框架(一)
  5. move lob会不会影响其他索引状态?
  6. CrashReport(崩溃日志)分析方法
  7. 解决异常-ORA-01747 invalid user.table.column, table.column, or column specification
  8. 为什么技术越牛逼的人,越得不到提拔?
  9. 【转载】JVM能够开启多少线程
  10. java 字符串签名_Java-方法签名