使用数字签名实现数据库记录防篡改(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],就不翻译成中文了,以防词不达意。大家看英文应该能更精确的明白其意思。

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

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

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

第二部分:进行数字签名

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

第三部分:进行签名验证

  1. 从密钥文件中读取KeyPair对象。
  2. 调用KeyPair对象的getPrivate()和getPublic()方法,分别获取PrivateKey和PublicKey。
  3. 利用密钥的指定算法生成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进行编码,就可以得到一串可见的字符串,方便存储和重新调用。

五、后记

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

六、参考资料:

[1] http://www.ruanyifeng.com/blog/2006/12/notes_on_cryptography.html

[2] http://www.youdzone.com/signature.html

[3] http://docs.oracle.com/javase/7/docs/api/java/security/package-summary.html

时间: 2024-10-10 08:18:17

使用数字签名实现数据库记录防篡改(Java实现)的相关文章

如何实现网页防篡改

最近想和朋友搞一个防篡改的东西,我自己罗列了一些,咨询下各位篡改网页的途径和相应的方法,小弟不才,不胜赐教. 一.网页篡改的途径(1)SQL注入后获取Webshell:黑客通过web应用程序的漏洞,通过SQL语句提交非法的语句到数据,通过系统以及第三方软件的漏洞获取web的控制权限或者服务器权限:(2)XSS漏洞引入恶意HTML界面:被动的跨站攻击可以在合法的地方引入非法的HTML或者JS代码,从而让访问者“正常”的改变页面内容:例如:校内网蠕虫:(3)控制了Web服务器:攻击者可能通过服务器或

使用hibernate更新数据库记录的信息的相关学习记录

截选代码(可能遗漏标点符号): package name.sql; import java.util.List; import name.session.HibernateSessionFactory; import org.hibernate.Query;import org.hibernate.Session;import org.hibernate.Transaction; public class SqlHelper { //增 public void save(Object obj){

WebApi系列~安全校验中的防篡改和防复用

回到目录 web api越来越火,因为它的跨平台,因为它的简单,因为它支持xml,json等流行的数据协议,我们在开发基于面向服务的API时,有个问题一直在困扰着我们,那就是数据的安全,请求的安全,一般所说的安全也无非就是请求的防篡改和请求的防复用,例如,你向API发一个查询用户账户的请求,在这个过程中,你可能要传递用户ID,用户所在项目ID等,而现在拦截工具如此盛行,很容易就可以把它的请求拦截,然后篡改,再转发,这样你的API就是不安全的,而对于订单,账户模块这种糟糕的API设计更是致命的,可

数据库记录安全解决方案

数据库记录安全解决方案 http://netkiller.github.io/journal/mysql.security.html Mr. Neo Chen (netkiller), 陈景峰(BG7NYT) 中国广东省深圳市龙华新区民治街道溪山美地 518131 +86 13113668890 +86 755 29812080 <[email protected]> 版权 © 2014 http://netkiller.github.io 版权声明 转载请与作者联系,转载时请务必标明文章原始

基于Volley,Gson封装支持JWT无状态安全验证和数据防篡改的GsonRequest网络请求类

这段时间做新的Android项目的客户端和和REST API通讯框架架构设计,使用了很多新技术,最终的方案也相当简洁优雅,客户端只需要传Java对象,服务器端返回json字符串,自动解析成Java对象, 无状态安全验证基于JWT实现,JWT规范的细节可以参考我前面的文章.JWT的token和数据防篡改签名统一放在HTTP Header中,这样就实现了对请求内容和返回结果的无侵入性,服务器端也可以在全局过滤器中统一处理安全验证. Android客户端使用了Volley网络请求框架和Gson解析库,

JavaScript中的防篡改对象

由于JavaScript共享的特性,任何对象都可以被放在同一环境下运行的代码修改. 例如: var person = {name:"caibin'} person.age = 21; 即使第一行定义了完整的person对象,那么第二行代码仍然可以对其添加属性,删除属性等. 我们有三个方法可以防止你做出这些行为. 一.不可扩展对象: 先来看person本身的扩展性: Object.isExtensible(person); // true 接下来执行: Object.preventExtensio

aip接口中对url参数md5加密防篡改的原理

目前网上所有开放api的网站中,数据的调用都是采用同一种方式,即: http:www.xxx.com/aa=1&bb=2...,原后对这些参数按字典顺序排序后进行md5加密,将md5加密串与接口方提供的 key接在参数后面提交,如http:www.xxx.com/aa=1&bb=2&sg=md5(...)& key=3432423,服务器端把这些参数接收后以同样的方式生成md5与提交的sg参数核对是否一致,以达到防止篡改与验证合法性的目的. 我现在的疑问是,既然参数可以被篡

BAE百度云平台的mysql数据库的施用(Java)

BAE百度云平台的mysql数据库的使用(Java) BAE的数据库使用的mysql,还有phpmyadmin,怎么说呢,太像那种php空间了. 不过都是免费的哈~~ 第一个问题就是连接数据的问题. 做了一个简单的聊天室项目,打算放上去试试. BAE的数据库连接的用户名和密码需要从request请求中获取. 只有数据库名是我们可以直接拿来用的. 定义一个JdbcUtil类.用来获取连接. 为了节省代码,直接写成 共有静态变量了. public final class JdbcUtil { pri

Atitit.并发测试解决方案(2) -----获取随机数据库记录 随机抽取数据 随机排序 原理and实现

Atitit.并发测试解决方案(2) -----获取随机数据库记录 随机抽取数据 随机排序 1. 应用场景 1 2. 随机抽取数据原理 1 3. 常用的实现方法:::数据库随机函数 1 4. Mssql 的实现 NEWID() 跟rand()  1 5. newid()与rand()的区别 2 6. NEWID() 2 7. 参考 2 1. 应用场景 并发测试 2. 随机抽取数据原理 原理是 循环所有的ID/记录,附加随机函数字段,然后排序as 这个字段.. 3. 常用的实现方法:::数据库随机