现在网站都是前后台分离模式,在客户端和服务端的通信过程中,会遇到很多的安全问题,无法确认收到的信息是否是真实有效的,而不是中途被人掉包的。这个时候数字签名就可以站出来,它的作用就是用来征明消息在通信过程中未被掉包,是真实有效的。

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;
        }
    }
}

更多推荐