Java-签名
签名是什么
现实中,由于我们每个人的笔迹近似独一无二,所以一旦我们在文件中签字就无法抵赖说不是自己签的,因为对方可以做笔迹鉴定。
计算机的世界更加错综复杂,A向B发送了一个文件,中途可能别拦截,然后可能被篡改或者替换,B怎么知道收到的文件一定是A发送的原件呢?
为了解决这个问题,聪明的计算机大佬们发明了数字签名这么个东东,数字签名并不是说这个签名是一串数字,而是为了区分传统的手写签名。
A发送文件的同时也把这个文件的签名发送给B,B只要验证文件签名是正确的,就可以认为文件是A发送的而且是原件。
也就是说,签名可以让B很自信的确定收到的文件是否被 篡改 和 替换 。
多处用于金钱交易方面和重大信息传递方面
签名是怎么产生的
我们先来看看签名是怎么生成的,简单来说有如下几步:
生成唯一的非对称加密公钥和私钥对,一般是RSA算法;
对要发送的文件计算摘要,一般是MD5算法;
使用私钥对摘要进行加密,得到签名
为什么要计算摘要呢?主要是出于性能考虑,非对称加密安全性较好,但是不足之处是加密和解密过程复杂,如果要加密的字符很多多性能影响会很大。所以一般没有人会对整个问价进行加密生成签名,虽然这样也能达到签名的目的。
为什么使用私钥加密?大部分情况下,我们使用公钥加密、私钥解密,但是签名却是使用私钥加密,公钥解密。这是因为如果用公钥加密,则必须把私钥发送给要验证签名的一方,不仅麻烦而且很危险;而使用私钥加密,公钥验证方可以很容易得到,不存在泄漏风险,而且由于发送的签名只是摘要,就算被拦截用公钥解密,得到的信息也只是一串字符,没有任何意义。
综上所示,签名的生成方生成签名,并把签名和公钥发送给验证方,验证方根据公钥解密得到摘要,然后对接收到的文件重新计算摘要,两个摘要一样就说明文件没有被替换和篡改。
签名 VS 篡改+替换
为什么签名可以防止篡改呢?因为签名是通告摘要算法(MD5)产生的,任何一位数据的修改都会导致摘要发生变化,所以验证方只要判断签名解密后的摘要和文件计算的摘要一致,就说明文件未被篡改。
为什么签名可以防止文件被整个替换呢?如果攻击者另外找一个假文件,自己生成假的签名呢?别忘了,因为签名生成方产生的公钥和私钥对是唯一的,而且公钥验证方可以很容易获取到,如果签名是伪造的,则解密会出现错误,或者解密得到的摘要和源文件摘要会大不相同。
综上所示,B通过验证签名就可以知道这个文件一定是A发送的,而且一定是A发送的原件,所以就可以无条件的信任这个文件啦~
生成签名和验证签名工具类
在一些项目中,客户端在调用服务的接口时,通常需要设置签名验证,以保证对客户端的认证。在签名过程中一般每个公司都有自己的签名规则和签名算法,广泛使用的是使用非对称加密算法RSA为核心,在客户端使用私钥对参数进行加密生成一个密钥,传给服务端,然后在服务端再对这个这个密钥使用公钥进行解密,解密出来的字符串参数与客户端传过来的参数进行对比,如果一样,验证成功。
总结起来就是:
客户端
首先获取所有的参数,然后对他们进行排序,生成一个字符串
对这个字符串MD5加密,然后转为大写
然后使用私钥对MD5再次加密,生成最终的签名sign
把这个签名sign传给服务端
服务端
获取所有的参数
把参数中签名sign参数去除,然后排序,生成一个字符串
对这个字符串MD5加密,然后转为大写
使用公钥对sign字符串进行解密获取一个String,然后和第三步中获取的字符串相对,如果相等,则验证成功
下面我们就通过以上规则实现客户端的签名和服务端的验证。
import java.io.IOException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class SignatureUtil{
/**得到产生的私钥/公钥对
* @return KeyPair
*/
public static KeyPair getKeypair(){
//产生RSA密钥对(myKeyPair)
KeyPairGenerator myKeyGen = null;
try {
myKeyGen = KeyPairGenerator.getInstance("RSA");
myKeyGen.initialize(512); //最低大小512
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
KeyPair myKeyPair = myKeyGen.generateKeyPair();
return myKeyPair;
}
/**根据私钥和信息生成签名
* @param privateKey
* @param data
* @return 签名的Base64编码
*/
public static String getSignature(PrivateKey privateKey,String data){
Signature sign;
String res = "";
try {
sign = Signature.getInstance("MD5WithRSA");
sign.initSign(privateKey);
sign.update(data.getBytes());
byte[] signSequ = sign.sign();
res = Base64.getEncoder().encodeToString(signSequ);
}catch(NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
}
return res;
}
/**验证签名
* @param publicKey 公钥的Base64编码
* @param sign 签名的Base64编码
* @param data 生成签名的原数据
* @return
*/
public static boolean verify(String publicKey, String sign, String data){
boolean res = true;
try {
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("MD5withRSA");
signature.initVerify(publicK);
signature.update(data.getBytes());
res = signature.verify(Base64.getDecoder().decode(sign));
}catch(NoSuchAlgorithmException | InvalidKeyException | SignatureException | InvalidKeySpecException e) {
e.printStackTrace();
}
return res;
}
//将字符串私密钥还原 PrivateKey 对象
public static PrivateKey loadPrivateKey(String key64) {
byte[] clear = Base64.getDecoder().decode(key64.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(clear);
KeyFactory fact = null;
try {
fact = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
PrivateKey priv = null;
try {
priv = fact.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
Arrays.fill(clear, (byte) 0);
return priv;
}
//将字符串公秘钥还原 PublicKey 对象
public static PublicKey loadPublicKey(String stored)
{
byte[] data = Base64.getDecoder().decode((stored.getBytes()));
X509EncodedKeySpec spec = new X509EncodedKeySpec(data);
PublicKey publicKey=null;
try {
KeyFactory fact = KeyFactory.getInstance("RSA");
try {
publicKey= fact.generatePublic(spec);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return publicKey;
}
final static KeyPair keyPair = getKeypair();
/*(1)生成公钥和私钥对*/
public static Map<String,String> key(){
Map<String,String> map=new HashMap<String,String>(){{
put("publicKey",Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
put("privateKey",Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
}};
return map;
}
/***
* 生成签名
* @Author: HuAnmin
* @email: 3426154361@qq.com
* @Date: 2021/4/6 0:06
* @param: privateKey 私密钥
* @param: data 需要签名的数据
* @return: java.lang.String
* @Description: 方法功能描述....
*/
public static String getSignature(String privateKey,String data){
return getSignature(loadPrivateKey(privateKey),data);
}
public static void main(String[] args) {
System.out.println("公钥:" + key().get("publicKey"));
System.out.println("私钥:" + key().get("privateKey"));
String data = "给我签名吧!";
/*(2)用私钥生成签名*/
String signature = getSignature(key().get("privateKey"), data);
System.out.println("签名是:" +signature );
/*(3)利用公秘钥 验证签名*/
System.out.println("验证签名的结果是:" + verify(key().get("publicKey"),signature,data));
}
}
如果服务器发送给客户端数据后严格点是要加上时间戳的timestamp
客户端请求服务端数据,服务端发送给客户端的数据,如果在指定时间内客户端没有回复数据那么,将数据失效
为了避免多次客户端提交相同请求,服务端返回多次数据, 在请求的参数中加入流水号nonce(防止重复提交),至少为10位。在签名验证成功后,判断是否重复提交; 原理就是结合redis,判断是否已经提交过
就比如一个人手机卡了按下了好几次支付那么如果没有设置nonce 那么这几次订单都将成功多给别人付了好几次的钱,
当接口发送数据到服务端时候如果服务端没有执行那么redis就没有这个nonce,如果执行成功的那么redis就有当前请求的nonce下次…,在遇到新的请求的时候获取请求的nonce和redis中的nonce对比如果相同的那么就能判断是相同的接口请求,不用执行当前请求 比如: 微信支付 , 商品下单 , 商品付款…