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

本文大纲

一、提出问题

二、数字签名

三、实现步骤

四、参考代码

五、后记

六、参考资料

一、提出问题

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

这感觉不妙了,字段名叫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进行编码,就可以得到一串可见的字符串,方便存储和重新调用。

五、后记

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

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

(0)

相关推荐

  • Java实现获得MySQL数据库中所有表的记录总数可行方法

    在MySQL中,可以通过SELECT COUNT(*) FROM table_name查询某个表中有多少条记录.如果想知道某个数据库中所有别的记录总数应该怎么做呢?本文给出两种可行的Java程序,解决该问题. 1. 首先确定数据库中有多少个表,然后对每个表执行SELECT COUNT(*) FROM table_name 复制代码 代码如下: import java.sql.Connection; import java.sql.DriverManager; import java.sql.Pr

  • MySQL数据库遭到攻击篡改(使用备份和binlog进行数据恢复)

    本文主要描述了MySQL遭到攻击篡改数据,利用从库的备份和主库的binlog进行不完全恢复. 欢迎转载,请注明作者.出处. 作者:张正 QQ:176036317 如有疑问,欢迎联系. 一.发现问题 今天是2014-09-26,开发大清早就说昨晚数据库遭到了攻击.数据库中某文章表的文章内容字段遭到篡改,全部改成了同一篇文章. 通过查看日制 发现 数据是在 2014-09-25 21:53:57 遭到篡改. 所有的内容全部被改成了如下: 复制代码 代码如下: subject: 桂林阳朔自助游    

  • SQL获取第一条记录的方法(sqlserver、oracle、mysql数据库)

    Sqlserver 获取每组中的第一条记录 在日常生活方面,我们经常需要记录一些操作,类似于日志的操作,最后的记录才是有效数据,而且可能它们属于不同的方面.功能下面,从数据库的术语来说,就是查找出每组中的一条数据.下面我们要实现的就是在sqlserver中实现从每组中取出第一条数据. 例子 我们要从上面获得的有效数据为: 对应的sql语句如下所示: select * from t1 t where id = (select top 1 id from t1 where grp = t.grp o

  • 用SQL语句查询数据库中某一字段下相同值的记录方法

    今天接到一任务,有一张学生信息表(Excel表),里面有一万多条记录,现在要把这张表导入到数据库中,并设置学生学号为主键,但是现在这张表中的学生学号有重复的记录,我必须先找出这些重复的记录,然后再进行筛选,经过研究问题终于得到解决.      以上问题实际上就是查询数据库表中某一字段值重复的记录,这里省略如何将Excel表导入到数据库步骤,只讨论用SQL查询数据库中某一字段下相同值的记录方法. 现在假设数据库表名为student,里面有字段Sno(学号),ID(身份证),这里提供两种查询的方法:

  • 1亿条记录的MongoDB数据库随机查询性能测试

    mongdb性能压力测试,随机查询,数据量1亿条记录 操作系统centos6.4x64位 从测试结果看,当mongodb将数据全部载入到内存后,查询速度根据文档的大小,性能瓶颈通常会是在网络流量和CPU的处理性能(该次测试中当数据全部在内存后,纯粹的查询速度可以稳定在10W/S左右,系统load可以维持在1以下,由于此时CPU已经被使用到极限了,当并发再大时load值会直线飙升,性能急剧下降). 压力生成服务器与Mongodb服务器基本配置 cpu型号:Intel(R) Xeon(R) CPU

  • SQL Server数据库按百分比查询出表中的记录数

    SQL Server数据库查询时,能否按百分比查询出记录的条数呢?答案是肯定的.本文我们就介绍这一实现方法. 实现该功能的代码如下: create procedure pro_topPercent ( @ipercent [int] =0 --默认不返回 ) as begin select top (@ipercent ) percent * from books end 或 create procedure pro_topPercent ( @ipercent [int] =0 ) as be

  • 数据库查询排除重复记录的方法

    今天由于工作需要,需要在数据库中找出某一字段下不同的记录值,很简单的问题被我想的太复杂,很是郁闷,原因是SQL的一条命令忘了,现问题已解决,赶快拿来做备忘. 其实这里只需要用到SQL中的DISTINCT命令即可,非常简单,语法如下: 复制代码 代码如下: SELECT DISTINCT 列名称 FROM 表名称 举例说明: 假设现有一个数据库表:htmer: 复制代码 代码如下: field001 记录1 记录2 记录1 记录3 现在这张表中有四条记录,但有一条记录是重复的,如果我要去掉该重复记

  • 用php制作简单分页(从数据库读取记录)的方法详解

    PHP新手,一直想做一下分页都给忘了,今天有幸被提醒所以网上搜了一下.有些写的看不懂也没怎么去看.最后找到一个比较简单的. 大致的思想就是: 1.设置每页要显示的最大记录数. 2.计算出页面总数 3.当前页面跟总页面数比较改变连接的状态 4.用limit控制从数据库中读取记录 下面是代码: 复制代码 代码如下: $conn = mysql_connect('localhost','root','');            mysql_select_db('db_BookStore',$conn

  • PHP判断数据库中的记录是否存在的方法

    本文实例讲述了PHP判断数据库中的记录是否存在的方法.分享给大家供大家参考. 具体实现代码如下: 复制代码 代码如下: <?php    $sql="select * from checklist where game_id=$gid and task='$task' and status='$status'";  $result=mysql_query($sql);    $row = mysql_fetch_array($result, MYSQL_ASSOC);      

  • MySQL数据库查看数据表占用空间大小和记录数的方法

    如果想知道MySQL数据库中每个表占用的空间.表记录的行数的话,可以打开MySQL的 information_schema 数据库.在该库中有一个 TABLES 表,这个表主要字段分别是: TABLE_SCHEMA : 数据库名 TABLE_NAME:表名 ENGINE:所使用的存储引擎 TABLES_ROWS:记录数 DATA_LENGTH:数据大小 INDEX_LENGTH:索引大小 其他字段请参考MySQL的手册,这几个字段对我们来说最有用. 一个表占用空间的大小,相当于是 数据大小 +

  • Java实现从数据库导出大量数据记录并保存到文件的方法

    本文实例讲述了Java实现从数据库导出大量数据记录并保存到文件的方法.分享给大家供大家参考,具体如下: 数据库脚本: -- Table "t_test" DDL CREATE TABLE `t_test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) DEFAULT NULL, `createTime` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=I

随机推荐