使用数字签名实现数据库记录防篡改(Java实现)

 更新时间:2020年6月25日 11:21  点击:1359

本文大纲

一、提出问题

二、数字签名

三、实现步骤

四、参考代码

五、后记

六、参考资料

一、提出问题

最近在做一个项目,需要对一个现成的产品的数据库进行操作,增加额外的功能。为此,需要对该产品对数据库有什么操作进行研究(至于怎么监控一个产品的操作会引发什么数据库操作,以后会详细解说)。本来已经对数据库的操作了如指掌的,无意中发现数据库表里的每条记录都会有这样一个字段:

这感觉不妙了,字段名叫signature,顾名思义,就是签名的意思呀。难道数据库表中的每条记录都会有签名?也就是说如果我不能正确生成签名,而直接改记录中的字段,会被程序认为非法篡改了数据?那以后我的产品设计,是否也可采用这种方式来对每条记录做签名,防止数据被非法篡改,例如日志表中的数据?抱着这一发现以及这一连串的问题,我进行了以下的研究。在这里我将研究整理了一下,分享给大家。

二、数字签名

要解决上面的问题,首先就要对最基础的知识进行了解。这里最基础的知识,无疑就是什么是数字签名了。很多同学可能对这个名词并不陌生,但估计大多数人都是对其一知半解,会把散列、非对称加密、数字签名、数字证书的几个概念混为一谈,造成混乱。所以我先对相关概念进行解释,再往下讲。如果很熟悉这方面的同学可以跳过此部分,但对于绝大多数同学来说,不建议这样做。基础没搭好,直接看怎么实现,换了个说法又不知道怎么去做了。要想提高个人能力,做到举一反三很重要。

言归正传,先对跟数字签名有关的密码学知识简单说一下。加密方法分两大类,分别是单钥加密和双钥加密,数字签名涉及到双钥加密。关于双钥加密,主要涉及到以下几个要点[1]:

  • 双钥加密的密钥有两把,一把是公开的公钥,一把是不公开的私钥
  • 公钥和私钥是一一对应的关系,有一把公钥就必然有一把与之对应的、独一无二的私钥,反之亦成立。
  • 所有的(公钥, 私钥)对都是不同的。
  • 用公钥可以解开私钥加密的信息,反之亦成立。
  • 同时生成公钥和私钥应该相对比较容易,但是从公钥推算出私钥,应该是很困难或者是不可能的。
  • 在双钥体系中,公钥用来加密信息,私钥用来数字签名。
  • 还有一点关于数字证书的。因为任何人都可以生成自己的公钥私钥对,所以为了防止有人散布伪造的骗取信任,就需要一个可靠的第三方机构来生成经过认证的公钥、私钥对。简单来说,数字证书是权威的第三方机构颁发的,用来认证某对公钥私钥的证书,经过这个数字证书认证的公钥私钥,就可以明确属于某人或者某机构,是合法的,可信任的。就如同身份证,是证明你身份的一个证件。所以数字证书跟数字签名是两回事,要分清楚。

数字签名,顾名思义,就类似于一种写在纸上的普通的物理签名,不同的是,数字签名是电子信息化的,采用双钥加密的技术实现,是一种用于鉴别数字信息的方法。处理的过程,简单说就是将文件内容进行hash散列,信息发送者对散列后的字符串使用私钥加密,得到的最终字符串就是签名。然后将得到的签名字符串添加到文件信息的后面一同发送出去。接收者获取到文件信息和签名后,使用公钥对签名进行解密,就得到文件内容加密后的hash散列。此时,他可以对获取到的文件内容做hash散列,与签名中的hash散列进行匹对,从而鉴别出最终获取信息的真伪。主要过程如这四幅图所示[2]:

对文件内容进行hash散列,生成摘要

对生成的摘要,使用私钥进行加密,形成签名

将得到的签名,附到文件内容后部,就想到与签名签到文件尾部那样子

使用公钥对签名进行解密,得到摘要,并与获取到的文件内容生成的摘要做对比,以确定是否被篡改

想了解更详细的数字证书相关内容,可以访问此地址:http://www.youdzone.com/signature.html。里面解释得很形象,应该一看就明白的了。

三、实现步骤

看到这里,开篇提出的问题也就呼之欲出了。没错,就是使用数字签名技术,将数据库中的重要字段进行签名,将签名结果作为记录的一列存在记录中。这样当有人入侵数据库,恶意修改字段,程序读数据时拿签名校验一下,就知道数据是否有被修改过了。

在java.security包中,有很多有用的类,用以进行安全机制的开发。对于要创建数字签名,我们主要用到以下的接口或类:

接口名

描述

PrivateKey

A private key

PublicKey

A public key

接口

 类名

描述

Signature

The Signature class is used to provide applications the functionality of a digital signature algorithm.

KeyPair

This class is a simple holder for a key pair (a public key and a private key)

KeyPairGenerator

The KeyPairGenerator class is used to generate pairs of public and private keys.

对于接口和类的描述,我直接引用了Oracle上的J2SE 7的API描述[3],就不翻译成中文了,以防词不达意。大家看英文应该能更精确的明白其意思。

利用上述的接口和类,就可以进行数字签名和验证了,下面分三部分进行基本步骤的描述。

第一部分:生成密钥并存储

  • 生成KeyPairGenerator实例,并调用其genKeyPair()方法生成KeyPair对象。
  • 利用ObjectOutputStream实例,将KeyPair对象写到文件中,从而把密钥保存到文件中。

第二部分:进行数字签名

  • 从密钥文件中读取KeyPair对象。
  • 调用KeyPair对象的getPrivate()和getPublic()方法,分别获取PrivateKey和PublicKey。
  • 利用密钥的指定算法生成Signature实例,然后利用PrivateKey和文件内容,分别调用其initSign()和update()方法,最后调用sign()方法生成数字签名。

第三部分:进行签名验证

  • 从密钥文件中读取KeyPair对象。
  • 调用KeyPair对象的getPrivate()和getPublic()方法,分别获取PrivateKey和PublicKey。
  • 利用密钥的指定算法生成Signature实例,然后利用PublicKey和文件内容,分别调用其initSign()和update()方法,最后利用数字签名调用verify()方法验证签名。

四、参考代码

根据上面的步骤描述,基本可以写出程序来了。下面是参考代码,未必尽善尽美,但是基本功能都体现到了,供你参考。

工程结构:

DataSecurity类:

package com.hzj.security;
import java.io.UnsupportedEncodingException;
import java.nio.charset.CharsetEncoder;
import java.security.KeyPair;
import com.hzj.util.StringHelper;
public class DataSecurity {
 private KeyPair keyPair;
 private static final String KEY_FILE = "/ca.key";
 private DataSignaturer dataSignaturer;
 public DataSecurity() {
 try {
 this.keyPair = KeyPairUtil.loadKeyPair(getClass().getResourceAsStream("/ca.key"));
 this.dataSignaturer = new DataSignaturer(this.keyPair.getPublic(), this.keyPair.getPrivate());
 } catch (RuntimeException e) {
 System.out.println("没有找到KeyPair文件[/ca.key]!");
 }
 }
 /**
 * 验证数字签名
 * @param data
 * @param signs
 * @return
 */
 public boolean verifySign(String data, String signs) {
 if ((data == null) || (signs == null)) {
 System.out.println("参数为Null");
 }
 boolean verifyOk = false;
 try {
 verifyOk = this.dataSignaturer.verifySign(data.getBytes("UTF-8"), StringHelper.decryptBASE64(signs));
 } catch (RuntimeException e) {
 System.out.println("fail!data=" + data + ", sign=" + signs + ", exception:" + e.getMessage());
 } catch (UnsupportedEncodingException e) {
 System.out.println("不支持UTF-8字符集");
 } catch (Exception e) {
 System.out.println("Exception:" + e.getMessage());
 }
 if (!verifyOk) {
 System.out.println("fail!data=" + data + ", sign=" + signs + ", verifyOk=false!");
 }
 return verifyOk;
 }
 /**
 * 生成数字签名
 * @param data
 * @return
 */
 public String sign(String data)
 {
 if (data == null) {
 System.out.println("参数为Null");
 }
 String sign = null;
 try
 {
 sign = StringHelper.encryptBASE64(this.dataSignaturer.sign(data.getBytes("UTF-8")));
 }
 catch (UnsupportedEncodingException e)
 {
 System.out.println("不支持UTF-8字符集");
 }
 catch (Exception e)
 {
 System.out.println(e.getMessage());
 }
 return sign;
 }
 }

DataSignaturer类:

package com.hzj.security;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
public class DataSignaturer {
 private PrivateKey privateKey;
 private PublicKey publicKey;
 public DataSignaturer(PublicKey publicKey, PrivateKey privateKey){
 this.privateKey = privateKey;
 this.publicKey = publicKey;
 }
 /**
 * 进行数字签名
 * @param data
 * @return
 */
 public byte[] sign(byte[] data) {
 if (this.privateKey == null) {
 System.out.println("privateKey is null");
 return null;
 }
 Signature signer = null;
 try {
 signer = Signature.getInstance(this.privateKey.getAlgorithm());
 } catch (NoSuchAlgorithmException e) {
 System.out.println(e.getMessage());
 }
 try {
 signer.initSign(this.privateKey);
 } catch (InvalidKeyException e) {
 System.out.println(e.getMessage());
 }
 try {
 signer.update(data);
 return signer.sign();
 } catch (SignatureException e) {
 System.out.println(e.getMessage());
 return null;
 } catch (NullPointerException e) {
 System.out.println(e.getMessage());
 return null;
 }
 }
 /**
 * 验证数字签名
 * @param data
 * @param signature
 * @return
 */
 public boolean verifySign(byte[] data, byte[] signature) {
 if (this.publicKey == null) {
 System.out.println("publicKey is null");
 return false;
 }
 Signature signer = null;
 try {
 signer = Signature.getInstance(this.publicKey.getAlgorithm());
 } catch (NoSuchAlgorithmException e) {
 System.out.println(e.getMessage());
 return false;
 }
 try {
 signer.initVerify(this.publicKey);
 } catch (InvalidKeyException e) {
 System.out.println(e.getMessage());
 return false;
 }
 try {
 signer.update(data);
 return signer.verify(signature);
 } catch (SignatureException e) {
 System.out.println(e.getMessage());
 return false;
 }
 }
}

KeyPair类:

package com.hzj.security;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
public class KeyPairUtil {
 // 采用的双钥加密算法,既可以用DSA,也可以用RSA
 public static final String KEY_ALGORITHM = "DSA";
 /**
 * 从输入流中获取KeyPair对象
 * @param keyPairStream
 * @return
 */
 public static KeyPair loadKeyPair(InputStream keyPairStream) {
 if (keyPairStream == null) {
 System.out.println("指定的输入流=null!因此无法读取KeyPair!");
 return null;
 }
 try {
 ObjectInputStream ois = new ObjectInputStream(keyPairStream);
 KeyPair keyPair = (KeyPair) ois.readObject();
 ois.close();
 return keyPair;
 } catch (Exception e) {
 System.out.println(e.getMessage());
 }
 return null;
 }
 /**
 * 将整个KeyPair以对象形式存储在OutputStream流中, 当然也可以将PublicKey和PrivateKey作为两个对象分别存到两个OutputStream流中,
 * 从而私钥公钥分开,看需求而定。
 * @param keyPair 公钥私钥对对象
 * @param out 输出流
 * @return
 */
 public static boolean storeKeyPair(KeyPair keyPair, OutputStream out) {
 if ((keyPair == null) || (out == null)) {
 System.out.println("keyPair=" + keyPair + ", out=" + out);
 return false;
 }
 try {
 ObjectOutputStream oos = new ObjectOutputStream(out);
 oos.writeObject(keyPair);
 oos.close();
 return true;
 } catch (FileNotFoundException e) {
 System.out.println(e.getMessage());
 } catch (IOException e) {
 System.out.println(e.getMessage());
 }
 return false;
 }
 /**
 * 生成KeyPair公钥私钥对
 * 
 * @return
 */
 public static KeyPair initKeyPair() throws NoSuchAlgorithmException{
 KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
 keyPairGen.initialize(1024);
 return keyPairGen.genKeyPair();
 }
 /**
 * 生成密钥,并存储
 * @param out
 * @return
 * @throws NoSuchAlgorithmException
 */
 public static boolean initAndStoreKeyPair(OutputStream out) throws NoSuchAlgorithmException {
 return storeKeyPair(initKeyPair(), out);
 }
}

StringHelper类:

package com.hzj.util;
import sun.misc.BASE64Encoder;
import sun.misc.BASE64Decoder;
public class StringHelper {
 /** 
 * BASE64Encoder 加密 
 * @param data 要加密的数据 
 * @return 加密后的字符串 
 */ 
 public static String encryptBASE64(byte[] data) { 
 BASE64Encoder encoder = new BASE64Encoder(); 
 String encode = encoder.encode(data); 
 return encode; 
 } 
 /** 
 * BASE64Decoder 解密 
 * @param data 要解密的字符串 
 * @return 解密后的byte[] 
 * @throws Exception 
 */ 
 public static byte[] decryptBASE64(String data) throws Exception { 
 BASE64Decoder decoder = new BASE64Decoder(); 
 byte[] buffer = decoder.decodeBuffer(data); 
 return buffer; 
 } 
}

Program类:

package com.hzj.main;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.security.NoSuchAlgorithmException;
import com.hzj.security.DataSecurity;
import com.hzj.security.KeyPairUtil;
public class Program {
 public static void main(String[] args) {
 // 1.生成证书
// File file = new File("ca.key");
// try {
// FileOutputStream fileOutputStream = new FileOutputStream(file);
// KeyPairUtil.initAndStoreKeyPair(fileOutputStream);
// } catch (FileNotFoundException | NoSuchAlgorithmException e) {
// e.printStackTrace();
// }
 // 2.生成数字签名
// DataSecurity dataSecurity = new DataSecurity();
// String sign = dataSecurity.sign("大家好");
// System.out.println("sign:" + sign);
 //3.验证数字签名
 DataSecurity dataSecurity = new DataSecurity();
 boolean result = dataSecurity.verifySign("大家好", "MCwCFCDs3sBw/fXK9flndl0M5lAUiPYFAhR9vyNNc91UiUBxFwK3GzLLjWgTkQ==");
 System.out.println("result:" + result);
 }
}

这里需要注意的是,为什么要对数字签名进行进行Base64编码呢?这是因为生成的数字签名是byte[]型的,无论对应哪一种字符集来转化成String,都会有乱码出现。所以,采用Base64进行编码,就可以得到一串可见的字符串,方便存储和重新调用。

五、后记

写到这里,本文的内容就基本上完结了。有人看到这里就会问,这不是说数据库记录防篡改嘛,一直都在讲数字签名,究竟怎么个防篡改?前文已经对数字签名的一些基本原理,使用的场景,开发的步骤、代码等进行了描述,开篇是也描述了项目中遇到的数据库中的问题。将这些信息综合起来,应该就知道怎么将数字签名应用到数据库记录中来作为数据库防篡改的工具了。知道工具怎么用是基础,会用工具来完成自己想做的事情,就是进阶了。祝你步步高升!

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持猪先飞!

[!--infotagslink--]

相关文章

  • PHP 数据库缓存Memcache操作类

    操作类就是把一些常用的一系列的数据库或相关操作写在一个类中,这样调用时我们只要调用类文件,如果要执行相关操作就直接调用类文件中的方法函数就可以实现了,下面整理了...2016-11-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • Intellij IDEA连接Navicat数据库的方法

    这篇文章主要介绍了Intellij IDEA连接Navicat数据库的方法,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借价值,需要的朋友可以参考下...2021-03-25
  • 在数据库里将毫秒转换成date格式的方法

    在开发过程中,我们经常会将日期时间的毫秒数存放到数据库,但是它对应的时间看起来就十分不方便,我们可以使用一些函数将毫秒转换成date格式。 一、 在MySQL中,有内置的函数from_unixtime()来做相应的转换,使用如下: 复制...2014-05-31
  • C#操作本地文件及保存文件到数据库的基本方法总结

    C#使用System.IO中的文件操作方法在Windows系统中处理本地文件相当顺手,这里我们还总结了在Oracle中保存文件的方法,嗯,接下来就来看看整理的C#操作本地文件及保存文件到数据库的基本方法总结...2020-06-25
  • 如何解决局域网内mysql数据库连接慢

    通过内网连另外一台机器的mysql服务, 确发现速度N慢! 等了大约几十秒才等到提示输入密码。 但是ping mysql所在服务器却很快! 想到很久之前有过类似的经验, telnet等一些服务在连接请求的时候,会做一些反向域名解析(如果...2015-10-21
  • MySQL快速复制数据库数据表的方法

    某些时候,例如为了搭建一个测试环境,或者克隆一个网站,需要复制一个已存在的mysql数据库。使用以下方法,可以非常简单地实现。假设已经存在的数据库名字叫db1,想要复制一份,命名为newdb。步骤如下:1. 首先创建新的数据库newd...2015-10-21
  • mysqldump命令导入导出数据库方法与实例汇总

    mysqldump命令的用法1、导出所有库系统命令行mysqldump -uusername -ppassword --all-databases > all.sql 2、导入所有库mysql命令行mysql>source all.sql; 3、导出某些库系统命令行mysqldump -uusername -ppassword...2015-10-21
  • Mysql数据库错误代码中文详细说明

    1005:创建表失败1006:创建数据库失败1007:数据库已存在,创建数据库失败1008:数据库不存在,删除数据库失败1009:不能删除数据库文件导致删除数据库失败1010:不能删除数据目录导致删除数据库失败1011:删除数据库...2013-09-23
  • c#异步读取数据库与异步更新ui的代码实现

    这篇文章主要介绍了c#从数据库里取得数据并异步更新ui的方法,大家参考使用吧...2020-06-25
  • node.js如何操作MySQL数据库

    这篇文章主要介绍了node.js如何操作MySQL数据库,帮助大家更好的进行web开发,感兴趣的朋友可以了解下...2020-10-29
  • Yii2.0高级框架数据库增删改查的一些操作

    yii2.0框架是PHP开发的一个比较高效率的框架,集合了作者的大量心血,下面通过用户为例给大家详解yii2使用中的一些基本的增删改查操作。 User::find()->all(); //返回所有用户数据; User::findOne($id); //返回 主键...2015-11-24
  • MYSQL数据库使用UTF-8中文编码乱码的解决办法

    1.用phpmyadmin创建数据库和数据表 创建数据库的时候,请将“整理”设置为:“utf8_general_ci” 或执行语句: 复制代码 代码如下:CREATE DATABASE `dbname` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 创...2015-10-21
  • springBoot 项目排除数据库启动方式

    这篇文章主要介绍了springBoot 项目排除数据库启动方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-10
  • Linux 下使用shell脚本定时维护数据库的案例

    这篇文章主要介绍了Linux 下使用shell脚本定时维护数据库,本文通过案例分析给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-07-11
  • PHP连接公司内部服务器的MYSQL数据库的简单实例

    “主机,用户名,密码”得到连接、“数据库,sql,连接”得到结果,最后是结果的处理显示。当然,数据库连接是扩展库为我们完成的,我们能做的仅仅是处理结果而已。...2013-09-29
  • C#连接加密的Sqlite数据库的方法

    对数据加密分两种,一种是对数据库本身进行加密,另一种是对数据表中的数据进行加密,下面通过本文给大家介绍C#连接加密的Sqlite数据库的方法,感兴趣的朋友一起看看吧...2020-06-25
  • Java连接数据库oracle中文乱码解决方案

    这篇文章主要介绍了Java连接数据库oracle中文乱码解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-05-16
  • 深入分析C#连接Oracle数据库的连接字符串详解

    本篇文章是对C#连接Oracle数据库的连接字符串进行了详细的分析介绍,需要的朋友参考下...2020-06-25