现在网站都是前后台分离模式,在客户端和服务端的通信过程中,会遇到很多的安全问题,无法确认收到的信息是否是真实有效的,而不是中途被人掉包的。这个时候数字签名就可以站出来,它的作用就是用来征明消息在通信过程中未被掉包,是真实有效的。
1977年,由三位数学家(Rivest、Shamir 和 Adleman )联合发表了RSA算法,算法名称的来源是三位科学家的首字母的和。
RSA加密是一种非对称加密。是由一对密钥来进行加解密的过程,分别称为公钥和私钥。可以在不直接传递密钥的情况下,完成解密。RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。并且随着密钥长度的不断增加,以当下的计算机运算水平,是不可能在有限的时间下,暴力破解出密钥,而攻破该算法的。这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。RSA是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知的所有密码攻击,已被ISO推荐为公钥数据加密标准。
加密、签名区别
加密和签名都是为了安全性考虑,但略有不同。常有人问加密和签名是用私钥还是公钥?其实都是对加密和签名的作用有所混淆。简单的说,加密是为了防止信息被泄露,而签名是为了防止信息被篡改。本文主要讲解签名过程。
RSA签名(私钥签名、公钥验签)的过程如下:
- 用户注册生成一对密钥(公钥和私钥),私钥不公开,用户自己保留。公钥保存在服务端保管。
- 用户自己的私钥对消息加签,形成签名,并将加签的消息和消息本身一起传递给服务器。
- 服务器收到消息后,在获取用户的公钥进行验签,如果验签出来的内容与消息本身一致,证明消息是该用户的没有被篡改。
业务流程图如下
生成RSA公钥私钥
本文为了使用简单不重复造轮子使用hutool 工具包作为生成工具和验签工具
Hutool针对java.security.Signature做了简化包装,包装类为:Sign,用于生成签名和签名验证。链接
maven引入hutool工具包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
后端生成公钥私钥代码
public static void main(String[] args) {
RSA rsa= SecureUtil.rsa();
String priKey= rsa.getPrivateKeyBase64();
System.out.println("public static String priKeyText=\""+priKey+"\";");
String pubKey= rsa.getPublicKeyBase64();
System.out.println("public static String pubKeyText=\""+pubKey+"\";");
}
public static String priKeyText="MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAM2yllVX45houpxHKUkRUIm7XjobsQxF6R6Uo++PPRi8yEplO/jUWGdFKsl7KA75umWeaLcCmhmkZMDCtjYRGl7yD/H9+9wsvCUA5AONHqLPBWpeKUiEz5QZb3lM4p0ZqDHYYg+pnRecNVDi3gRSpGbEnfPGJXNmTsHf/g7jyVJzAgMBAAECgYAJBguYRNnEJtwA3RJPlUXeNO0GSY6zxiFa6RRj/vmkKyvyL4y49r/GD/+3hQKV3ZiyuodHeALAmzicSC5sIsXwTibDk+xeO2MV2MqYj5u+s1iNUfVhYaKKZUVxZaTuhVnwjYutgGPq4UWV2Lk6jfIoKQgfbJ4JtwJSgKImp+ZuQQJBAPfH8KYPjomc6x46384t97S6WunUyL2LnAEpWZIpTjporVdndMt7qrqYwq2a7wcMcBevUw4Py83GXIyEgBER9GECQQDUhUqQpgi9RcLYYZs5WP/z+2Vsyb03jmgg1iMEJ8vdKLsiIIiEyFuXCmyzQHnmHBg6ZaNp3P5R6wKrewu/HndTAkEA1U/HmO6jo0z4DNpaekGQkoIsaknJV4StFULn3bC5rAcZ07k5D0SzwlhooNRVGxhWF83UDrMCnXA7lK0tI4T1gQJBAKRcykSBBPVNhNpQ8voxaRNLdS/j/uF+0+3Bb6eBF9xd6g0E2FDJnHx+ZX9k+GINjHdtsCrp7g9xEMmLCpSvfV0CQCP3V8nvi8OTd8h+454XuvL45mBKuiN4PjVcfMjmlNQawSWx/FRgna4F7d44K/Zo8fTW/rgCNZp4fXLluIi8lEY=";
public static String pubKeyText="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNspZVV+OYaLqcRylJEVCJu146G7EMRekelKPvjz0YvMhKZTv41FhnRSrJeygO+bplnmi3ApoZpGTAwrY2ERpe8g/x/fvcLLwlAOQDjR6izwVqXilIhM+UGW95TOKdGagx2GIPqZ0XnDVQ4t4EUqRmxJ3zxiVzZk7B3/4O48lScwIDAQAB";
主要流程在前端对请求内容使用私钥进行签名,把签名内容写入到请求头header在Java后台拦截件获取签名内容使用公钥进行验签操作,若是一致则认为请求参数没有被篡改,若是不一致则认为请求参数已经被篡改,拦截请求。
前端签名
引入依赖
//主要包
npm install jsencrypt
//辅助包提供了各种各样的加密算法。
npm install crypto-js
前端签名
#引入模块
import {JSEncrypt} from "jsencrypt";
/** 签名方法 */
function rsaSign(plainText){
let priKey= "填入上方生成的priKey";
//这里必须使用这个标准格式所有做了一个拼接
var priK = "-----BEGIN PRIVATE KEY-----"+priKey+"-----END PRIVATE KEY-----";
var rsa = new jsrsasign.RSAKey();
rsa = jsrsasign.KEYUTIL.getKey(priK);
//后端算法与前端需要一致
let sig = new jsrsasign.KJUR.crypto.Signature({"alg": "SHA256withRSA"});
// 初始化
sig.init(rsa)
console.log("data:"+plainText);
// 传入待加密字符串
sig.updateString(plainText)
// 生成密文
let sign = jsrsasign.hextob64(sig.sign());
console.log("sign:"+sign);
// 对加密后内容进行URI编码
sign = encodeURIComponent(sign);
console.log("sign encode:"+sign);
return sign;
}
效验签名
//需要把对应的拦截器注册到WebMvcConfigurer
registry.addInterceptor(signInterceptor).addPathPatterns("/**");
/**
* 签名验签拦截器
*/
@Component
@Slf4j
public class SignInterceptor implements HandlerInterceptor {
private final static String priKeyText="MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALkYLvaYdloINgYnT5nrUMFd/3ghrUeV65CbueEwBkmwydPJOQkulumNCx2JsqZKLtOME1BCzzfVvAkZZzMmEXXUdweZee0NFiTrFKzCQaLVN7XhHZyb+6nFsAf31jwCm2LU3Y7yqni2dZPMNhL2W7UM6JtewyWKbqJwnrhpV1hlAgMBAAECgYBWKP67hY8aK9ZSGPyB7rshuArStgJ+XzhPkV3+iCd1KBlbP8EQGCLhxukUr+N8au3PRdY7t03UdObZ6XxTn/Xh88FGpvNtP8Xt2NXPmPdHnr8yS8ro3xoyjnFpnV8ILoVXB3wcH7sMyJlLbFOfNopd34UnOHq8i0G4UMufDfF/5QJBAOTxaw/4lU00ng/2Gqv/3IQ5/LlfoTCnyGd+wni+UXuwMhZm9EgwfjZFStNmm7To2MDtgaRLcllGbzNKkleEHScCQQDO+CTUiqFomWvyQfiITpvYwCeDHZkNxSKW/rIAJkcbIjnKoTyBjLIX4aO6tnrSGMYtfofWKPIE9Oov5PiEB22TAkAlyLIisKPzWfu8JqHAQTnjmK5c6atwnEA21HTD+KT6BNo/WD54q4go7MasnQKVmNgs/wWmV81HbphrnqMIToWBAkAW26UFrEHjUX5whQSz2SqxJ5e1jWFH+gu17W8vQQr9XixPyrrp++X3aT4x8tFhvsa0Y5MQ/fisv3fVzk38Fu3tAkEAqYWdIuBUtweHI5GU+eOy/B3/WJQYyhTQ7TIXJvzHlxy/YsCrN3FShkvbMda1johv6Js0rvTP8PFi8JfmcjlBkw==";
private final static String pubKeyText="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5GC72mHZaCDYGJ0+Z61DBXf94Ia1HleuQm7nhMAZJsMnTyTkJLpbpjQsdibKmSi7TjBNQQs831bwJGWczJhF11HcHmXntDRYk6xSswkGi1Te14R2cm/upxbAH99Y8Apti1N2O8qp4tnWTzDYS9lu1DOibXsMlim6icJ64aVdYZQIDAQAB";
private Sign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA,priKeyText,pubKeyText);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果不是映射到方法直接通过
if (handler instanceof HandlerMethod) {
if (!HttpMethod.GET.name().equals(request.getMethod())) {
String signStr = request.getHeader("sign");
if (Objects.isNull(signStr)) {
AjaxResult ajaxResult = AjaxResult.error("签名错误:签名为null");
ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
return false;
}
signStr=URLUtil.decode(signStr);
String body = HttpHelper.getBodyString(request);
boolean verify= sign.verify(body.getBytes(), Base64.decode(signStr));
if (!verify) {
AjaxResult ajaxResult = AjaxResult.error("签名错误:验签错误");
ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
return false;
}
}
return true;
} else {
return true;
}
}
}