写在前面

安全测试需要, 为防止后台响应数据返给前台过程中被篡改前台再拿被篡改后的数据进行接下来的操作影响正常业务, 决定采用RSA对响应数据进行签名和验签, 于是有了这篇<RSA后台签名前台验签的应用>.

我这里所谓的返给前台的数据只是想加密用户验证通过与否的字段success是true还是false, 前台拿这个success作为判断依据进行下一步的操作, 是进一步向后台发起请求还是直接弹出错误消息.照测试结果看这是个逻辑漏洞, 即使后台返回的是false, 在返回前台的过程中响应包被劫获, 将false改为true, 这样的操作也是能做到的(BurpSuit). 所以后台响应数据尽量不要再二次使用. 那既然能篡改, 如何防止流氓篡改呢?

说一下整体思路: 首先生成密钥对, 私钥存放在后台用于签名, 公钥存放在前台用于验签. 所谓签名就是指拿明文+私钥生成的签名结果, 返回数据给前台时将明文+签名结果一并返给前台, 前台用公钥+接收到的明文+签名结果进行验签, 这样即使响应包被劫获, 篡改明文后, 验证签名时也不会验证通过, 从而达到响应数据防篡改的目的.

接下来说一下具体步骤.

正文(具体步骤)

1.在线生成密钥对

采用在线工具生成密钥对, 私钥密码可填可不填, 网址:http://web.chacuo.net/netrsakeypair 截图中的密钥对是写博客时重新生成的, 和代码中的不一样不要见怪~

生成密钥对备用.

2.后台签名

Controller层java代码

    private AjaxJson getAjaxJson(HttpServletRequest req, HttpServletResponse res) {
AjaxJson j = new AjaxJson();
String passresStr = GetRSAStr.getResStr(true);// pass明文
j.setRsaStr(passresStr);// 明文
try {
j.setSign(RSAEnDeUtils.sign(passresStr, RSAEnDeUtils.getPrivateKey(RSAEnDeUtils.getPrivateKey())));// pass签名
} catch (Exception e) {
e.printStackTrace();
}
//============================client判断开始============================
String sessionCounterStr = sysConfigService.queryConfValueByConfId(SysParamConfig.forSecurityTest.SESSION_COUNTER.getParam());
BigInteger counterParam = new BigInteger(sessionCounterStr);
int clientCount = clientManager.getAllClient().size();
BigInteger clients = new BigInteger(String.valueOf(clientCount));
if (clients.compareTo(counterParam) >= 0) {
j.setSuccess(false);
j.setRsaStr(GetRSAStr.getResStr(false));// notpass明文
j.setMsg("系统已达最大会话数!");
return
j;
}
......

简单说一下这段代码逻辑, 这是校验登录用户用户名密码的其中一段代码.(关键代码已加粗)

思路就是进入该方法时, 把将要返回给前台的结果ajaxJson中首先设置两个参数, 一个属性名为rsaStr, 另一个属性名为sign.

其中rsaStr用于存放随机生成的uuid, sign用于存放由该uuid和第1步的私钥生成的签名结果.

当程序中遇到用户校验不通过时生成另一个uuid替换上面的rsaStr的值(sign并不替换), 由ajaxJson一并返回给前台.

后台关键代码(两个工具类)

签名用到的工具类RSAEnDeUtils.java和生成uuid的工具类GetRSAStr.java如下:

RSAEnDeUtils.java

package org.jeecgframework.web.system.util;

import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.UUID; public class RSAEnDeUtils { // 私钥
private static final String PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMlEZXt7J32l4s84ioWDeiKidaqmauNWKTbDInNaq/yK3fIC+j+jg5HjTJutk8ernbqTqeC+oc4I0m+Gs3vBc1QQhP49fIu7B9Y/TgjgQMFLcGfctxMwCcZWgiRrR/k7qWjcjRi09bCfKFxCGsda5OJ60YLQI3C54jXUm6rw1XafAgMBAAECgYAii9PjcwsfPQcGTI0yR5QCN+J8jR4RsWtXk/zo0eptaaSY8rvjinx94Qb4Pb386s8jBE+HXRFG3SrJq9RI7LaPrGjU3qbURTExr9qRo9//eR9VahCKyftryRkeXGqBcOreDgbiTb6wYzUL9OdgSV4to4hz7oIBmnal3+oy5grpIQJBAOgQwoMgAQfjfDSeBcXRklLestWvHRxLu3mpgcvcqHWmeH6HdSxBidJlu0U14QkruvxOZAeW0Y4iu20LY0JKZY8CQQDeBneXmEJr1Pnd/GAUo61i9xpKJOmGmFaiM78DE+JYFdnim+wdye1z/u7GPuD6HmcQC3kb7zpSRVSdOWsnxvXxAkEAhJBWXMsia5wybmg6ifcebAJVDCW9LlXAoU4IHClPfe17dWPxtjc2AJ8ma/HMPA3kAY7SK1enG1eR00enCs4u1wJBAJitY9H4Xzyd0VGIul2XDKVwfUCdT4VB/tk9sk2gf9bI9/Mv+9ekQ0iv92yWUslM3NyYtyixgq6OhJg1ou1QkVECQB3Vu4KvKafP5ejMPe3XplyDI20HJbHlAWH5NGZ67oRWLsVnKAIyLxZRhF4LPXew3gC9BVFCw8zj1geO42oOAso="; public static String getPrivateKey() {
return PRIVATE_KEY;
} /**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117; /**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128; /**
* 获取密钥对
*
* @return 密钥对
*/
public static KeyPair getKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(1024);
return generator.generateKeyPair();
} /**
* 获取私钥
*
* @param privateKey 私钥字符串
* @return
*/
public static PrivateKey getPrivateKey(String privateKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodedKey = Base64.decodeBase64(privateKey.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
return keyFactory.generatePrivate(keySpec);
} /**
* 获取公钥
*
* @param publicKey 公钥字符串
* @return
*/
public static PublicKey getPublicKey(String publicKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodedKey = Base64.decodeBase64(publicKey.getBytes());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
return keyFactory.generatePublic(keySpec);
} /**
* RSA加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return
*/
public static String encrypt(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
int inputLen = data.getBytes().length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data.getBytes(), offset, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data.getBytes(), offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
// 获取加密内容使用base64进行编码,并以UTF-8为标准转化成字符串
// 加密后的字符串
return new String(Base64.encodeBase64String(encryptedData));
} /**
* RSA解密
*
* @param data 待解密数据
* @param privateKey 私钥
* @return
*/
public static String decrypt(String data, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] dataBytes = Base64.decodeBase64(data);
int inputLen = dataBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offset > 0) {
if (inputLen - offset > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(dataBytes, offset, inputLen - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
// 解密后的内容
return new String(decryptedData, "UTF-8");
} /**
* 签名
*
* @param data 待签名数据
* @param privateKey 私钥
* @return 签名
*/
public static String sign(String data, PrivateKey privateKey) throws Exception {
byte[] keyBytes = privateKey.getEncoded();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey key = keyFactory.generatePrivate(keySpec);
Signature signature = Signature.getInstance("MD5withRSA");
signature.initSign(key);
signature.update(data.getBytes());
return new String(Base64.encodeBase64(signature.sign()));
} /**
* 验签
*
* @param srcData 原始字符串
* @param publicKey 公钥
* @param sign 签名
* @return 是否验签通过
*/
public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception {
byte[] keyBytes = publicKey.getEncoded();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("MD5withRSA");
signature.initVerify(key);
signature.update(srcData.getBytes());
return signature.verify(Base64.decodeBase64(sign.getBytes()));
} public static void main(String[] args) {
try {
// 生成密钥对
// KeyPair keyPair = getKeyPair();
// String privateKey = new String(Base64.encodeBase64(keyPair.getPrivate().getEncoded()));
// String publicKey = new String(Base64.encodeBase64(keyPair.getPublic().getEncoded()));
String privateKey = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMlEZXt7J32l4s84ioWDeiKidaqmauNWKTbDInNaq/yK3fIC+j+jg5HjTJutk8ernbqTqeC+oc4I0m+Gs3vBc1QQhP49fIu7B9Y/TgjgQMFLcGfctxMwCcZWgiRrR/k7qWjcjRi09bCfKFxCGsda5OJ60YLQI3C54jXUm6rw1XafAgMBAAECgYAii9PjcwsfPQcGTI0yR5QCN+J8jR4RsWtXk/zo0eptaaSY8rvjinx94Qb4Pb386s8jBE+HXRFG3SrJq9RI7LaPrGjU3qbURTExr9qRo9//eR9VahCKyftryRkeXGqBcOreDgbiTb6wYzUL9OdgSV4to4hz7oIBmnal3+oy5grpIQJBAOgQwoMgAQfjfDSeBcXRklLestWvHRxLu3mpgcvcqHWmeH6HdSxBidJlu0U14QkruvxOZAeW0Y4iu20LY0JKZY8CQQDeBneXmEJr1Pnd/GAUo61i9xpKJOmGmFaiM78DE+JYFdnim+wdye1z/u7GPuD6HmcQC3kb7zpSRVSdOWsnxvXxAkEAhJBWXMsia5wybmg6ifcebAJVDCW9LlXAoU4IHClPfe17dWPxtjc2AJ8ma/HMPA3kAY7SK1enG1eR00enCs4u1wJBAJitY9H4Xzyd0VGIul2XDKVwfUCdT4VB/tk9sk2gf9bI9/Mv+9ekQ0iv92yWUslM3NyYtyixgq6OhJg1ou1QkVECQB3Vu4KvKafP5ejMPe3XplyDI20HJbHlAWH5NGZ67oRWLsVnKAIyLxZRhF4LPXew3gC9BVFCw8zj1geO42oOAso=";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJRGV7eyd9peLPOIqFg3oionWqpmrjVik2wyJzWqv8it3yAvo/o4OR40ybrZPHq526k6ngvqHOCNJvhrN7wXNUEIT+PXyLuwfWP04I4EDBS3Bn3LcTMAnGVoIka0f5O6lo3I0YtPWwnyhcQhrHWuTietGC0CNwueI11Juq8NV2nwIDAQAB";
System.out.println("私钥:" + privateKey);
System.out.println("公钥:" + publicKey);
// RSA加密
// String data = "123456";
UUID uuid = UUID.randomUUID();
String data = uuid.toString();//.toString().replace("-","")
String encryptData = encrypt(data, getPublicKey(publicKey));
System.out.println("加密后内容:" + encryptData);
// RSA解密
String decryptData = decrypt(encryptData, getPrivateKey(privateKey));
System.out.println("解密后内容:" + decryptData); // RSA签名
String sign = sign(data, getPrivateKey(privateKey));
System.out.println("签名:" + sign);
// RSA验签
boolean result = verify(data, getPublicKey(publicKey), sign);
System.out.print("验签结果:" + result); } catch (Exception e) {
e.printStackTrace();
System.out.print("加解密异常");
}
}
}

GetRSAStr.java(忽略备注释的代码吧, 那是RSA之前想到的方案, 不可行)

package org.jeecgframework.web.system.util;

import java.io.UnsupportedEncodingException;
import java.util.UUID; public class GetRSAStr {
public static String getResStr(boolean b) {
String resStr = "";
if (b) {
UUID uuid = UUID.randomUUID();
String passuuidStr = uuid.toString().replace("-", "");
resStr = passuuidStr;
} else {
UUID uuid = UUID.randomUUID();
String notpassuuidStr = uuid.toString().replace("-", "");
resStr = notpassuuidStr;
} /*java.util.Base64.Encoder encoder = java.util.Base64.getEncoder();
final byte[] textByte;
try {
textByte = resStr.getBytes("UTF-8");
String encodedText = encoder.encodeToString(textByte);
resStr = new StringBuilder(encodedText).reverse().toString();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}*/ return resStr;
} }

3.前台验签

jsp中js部分

      $.ajax({
async: false,
cache: false,
type: 'POST',
url: checkurl,// 请求的action路径
data: formData,
error: function () {// 请求失败处理函数
},
success: function (data) {
var d = $.parseJSON(data);
var success = d.success; // 验证签名start===========
var rsaStr = d.rsaStr;// 明文
var sign = d.sign;// 签名
const rsaverify = RSA_VERIFY_SIGN(publicKey, rsaStr, sign);// 验签结果
// 验证签名end=========== if (rsaverify) {
window.location.href = actionurl;
} else {
showErrorMsg(d.msg);
......

这段就是登陆方法的其中一段代码, 忽略这坨翔吧, 看一下思路, 公钥也即是publicKey是定义在js文件中的全局变量, rsaStr和sign都是由后台响应数据data中获取的, 在采用rsa之前if else判断条件是用的var success = d.success;中success的结果, 由于该结果有可能被篡改, 才采用了现在的验签结果rsaverify作为判断依据.

前台关键代码(js文件)

验签用到的js文件如下, rsaverify.js和jsrsasign-all-min.js这两个文件, 在jsp中引入即可.

  <%--逻辑漏洞修复,需引入下面js文件--%>
<script src="<%=basePath%>/plug-in/ace/js/rsaverify.js"></script>
<script src="<%=basePath%>/plug-in/ace/js/jsrsasign-all-min.js"></script>

rsaverify.js

const ALGORITHM = 'MD5withRSA';

/**
* 私钥签名
* rsa 用 MD5withRSA 算法签名
* @param privateKey 私钥
* @param src 明文
* @return {*}
* @constructor
*/
const RSA_SIGN = (privateKey, src) => {
const signature = new KJUR.crypto.Signature({'alg': ALGORITHM});
const priKey = KEYUTIL.getKey(privateKey); // 因为后端提供的是pck#8的密钥对,所以这里使用 KEYUTIL.getKey来解析密钥
signature.init(priKey); // 初始化实例
signature.updateString(src); // 传入待签明文
const a = signature.sign(); // 签名, 得到16进制字符结果
return hex2b64(a) // 转换成base64
}; /**
* 公钥验签
* @param publicKey 公钥
* @param src 明文
* @param data 经过私钥签名并且转换成base64的结果
* @return {Boolean} 是否验签成功
* @constructor
*/
const RSA_VERIFY_SIGN = (publicKey, src, data) => {
const signature = new KJUR.crypto.Signature({'alg': ALGORITHM, 'prvkeypem': publicKey});
signature.updateString(src); // 传入待签明文
return signature.verify(b64tohex(data))
}; const publicKey = '-----BEGIN PUBLIC KEY-----\n' +
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJRGV7eyd9peLPOIqFg3oionWq\n' +
'pmrjVik2wyJzWqv8it3yAvo/o4OR40ybrZPHq526k6ngvqHOCNJvhrN7wXNUEIT+\n' +
'PXyLuwfWP04I4EDBS3Bn3LcTMAnGVoIka0f5O6lo3I0YtPWwnyhcQhrHWuTietGC\n' +
'0CNwueI11Juq8NV2nwIDAQAB\n' +
'-----END PUBLIC KEY-----';

jsrsasign-all-min.js库可以从GitHub上下载, 地址:https://github.com/kjur/jsrsasign, 也可以直接拷贝下面代码.只不过我为了便于观看代码把min代码格式化了一下(算了, 格式化后一万两千多行还是自行下载吧).

RSA前台加密后台解密的应用

其他的也没什么可说的了, 网上关于前台加密后台解密的很多, 后台签名前台验签的寥寥无几, 查看的资料很多不一一列举了, 在这里感谢一下https://www.jianshu.com/p/32ab410c71c3

关于后台加密前台解密的, 如果有需要可以参考我的另一片文章:RSA前台加密后台解密的应用

传输以及存储的保密性和完整性(补充)

关于登录过程中涉及到的传输保密性及完整性以及存储保密性和完整性描述:

前后台用到的关键文件可参考我另一篇博客, 这里只作描述, 不作展开, 详细参考:关于密码传输和密码存储的保密性和完整性.

感谢

RSA后台签名前台验签的应用(前台采用jsrsasign库)的更多相关文章

  1. RSA/RSA2 进行签名和验签

    package com.byttersoft.hibernate.erp.szmy.util; import java.io.ByteArrayInputStream; import java.io. ...

  2. .NET RSA解密、签名、验签

    using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Sec ...

  3. IdentityServer4之JWT签名(RSA加密证书)及验签

    一.前言 在IdentityServer4中有两种令牌,一个是JWT和Reference Token,在IDS4中默认用的是JWT,那么这两者有什么区别呢? 二.JWT与Reference Token ...

  4. erlang的RSA签名与验签

    1.RSA介绍 RSA是目前最有影响力的公钥加密算法,该算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但那时想要对 其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥,即公钥,而 ...

  5. Delphi支付宝支付【支持SHA1WithRSA(RSA)和SHA256WithRSA(RSA2)签名与验签】

    作者QQ:(648437169) 点击下载➨Delphi支付宝支付             支付宝支付api文档 [Delphi支付宝支付]支持条码支付.扫码支付.交易查询.交易退款.退款查询.交易撤 ...

  6. Delphi RSA签名与验签【支持SHA1WithRSA(RSA1)、SHA256WithRSA(RSA2)和MD5WithRSA签名与验签】

    作者QQ:(648437169) 点击下载➨ RSA签名与验签 [delphi RSA签名与验签]支持3种方式签名与验签(SHA1WithRSA(RSA1).SHA256WithRSA(RSA2)和M ...

  7. 密码基础知识(2)以RSA为例说明加密、解密、签名、验签

    密码基础知识(1)https://www.cnblogs.com/xdyixia/p/11528572.html 一.RSA加密简介 RSA加密是一种非对称加密.是由一对密钥来进行加解密的过程,分别称 ...

  8. PHP SHA1withRSA加密生成签名及验签

    最近公司对接XX第三方支付平台的代付业务,由于对方公司只有JAVA的demo,所以只能根据文档自己整合PHP的签名加密,网上找过几个方法,踩到各种各样的坑,还好最后算是搞定了,话不多说,代码分享出来. ...

  9. 中行P1签名及验签

    分享中国银行快捷.NET P1签名和验签方法代码中ReturnValue为自定义类型请无视 #region 验证签名 /// <summary> /// 验证签名 /// </sum ...

随机推荐

  1. JavaScript的函数call和apply的区别、以及bind方法

    1.call和apply的定义和区别 call和apply的作用一样,唯一不同的是:接受的参数不同. apply:方法能够劫持另一个对象的方法,继承另一个对象的属性. Funciton.apply(o ...

  2. XenCenter安装VM

    XenServer是服务器"虚拟化系统".系统设置为Linux_x86-64即可安装XenServer 和VMware ESX/ESXi有点不同的是,XenServer 不能在Xe ...

  3. TP5给request对象动态绑定属性

    在tp5中可以给Request请求对象绑定属性,方便全局调用.比如我们可以在公共控制器中绑定当前登录的用户模型到请求对象. 1. 首先在公共控制器中绑定: <?php namespace app ...

  4. z = z*z + c的分型图如何画

    使用python的图形库. 环境:conda+jupyter notebook 代码如下: import numpy as np from PIL import Image from numba im ...

  5. win黑窗口命令

    rd 删除 文件夹, 语法: rd [/S] [/Q] [drive:]path 参数: drive 盘符 path 文件路径 /S   递归删除文件夹( 删除前, 要确认是否进行删除) /Q  关闭 ...

  6. Spring Boot 集成 Kafka

    相关文章 网址 Spring Boot系列文章(一):SpringBoot Kafka 整合使用 http://www.54tianzhisheng.cn/2018/01/05/SpringBoot- ...

  7. 20180523模拟赛T4——Number

    [题目描述] 最近

  8. Spring中AOP方式实现多数据源切换

    作者:suroot spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性.而这样的方案就会不同于常见的单一数据实例的方案,这就要 ...

  9. web前端如何性能优化提高加载速度

    前端优化有以下几种途径: 一.减少HTTP请求数量和次数: 二.使用CDN: 三.添加Expires头: 四.压缩组件: 五.将样式表放在头部: 六.将脚本放在底部: 七.避免CSS表达式: 八.使用 ...

  10. cc2530中单片机的通用I/O接口

    cc2530中有21个输入/输出引脚. 这些引脚可以设置为通用I/O或者设置为外设I/O.(其实这里的外设还是不太懂到底指什么,网上说输入设备,但是通用I/O也可以输入啊,为什么要弄外设I/O?) 其 ...