Java应用服务系统安全性,签名和验签浅析
Posted 緈諨の約錠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java应用服务系统安全性,签名和验签浅析相关的知识,希望对你有一定的参考价值。
1 前言
随着互联网的普及,分布式服务部署越来越流行,服务之间通信的安全性也是越来越值得关注。这里,笔者把应用与服务之间通信时,进行的的安全性相关,加签
与验签
,进行了一个简单的记录。
2 安全性痛点
- 网关服务接口,暴漏在公网,被非法调用?
- 增加了token安全验证,被抓包等其他手段拦截了token,token验证无效?
- 参数被非法获取,非法调用系统应用的接口?
- 接口参数被非法获取后,同一个接口被重复多次非法调用?
3 技术选型
3.1 对称加密与非对称加密对比
-
对称加密
优点:加密速度快
缺点:密钥管理分配困难,安全性较低 -
非对称加密
优点:安全性较高
缺点:加密速度慢
对称加密技术加密和解密使用的都是同一个密钥
,因此密钥的管理非常困难,在分发密钥的过程中,如果一方密钥被截获,那后面的通信就是不安全的
。
而非对称加密技术就很好的解决了这一问题,非对称加密技术使用公钥加密,私钥加密
。通信前把公钥发布出去,私钥只有自己保留,即便你的公钥被攻击者拿到,没有私钥,就无法进行解密。
那有了非对称加密技术,对称加密是不是就被淘汰了?当然不是,因为非对称加密技术加解密比较慢,不适合对大量数据的加解密。
3.2 MD5是对称加密还是非对称加密?
- 对称算法有哪些?
对称密码算法又叫传统密码算法,也就是加密密钥能够从解密密钥中推算出来,反过来也成立。在大多数对称算法中,加密解密密钥是相同的。常见的对称算法有:DES、IDEA、AES、SM1和SM4。
- 非对称算法有哪些?
非对称密钥也叫公开密钥加密,它是用两个数学相关的密钥对信息进行编码。在此系统中,其中一个密钥叫公开密钥,可随意发给期望同密钥持有者进行安全通信的人。公开密钥用于对信息加密。第二个密钥是私有密钥,属于密钥持有者,此人要仔细保存私有密钥。密钥持有者用私有密钥对收到的信息进行解密。常见的非对称算法有:RSA、ECC、SM2。
这个问题有人吐槽过,面试官竟然问MD5是对称加密还是非对称加密?其实,MD5不是加密算法,md5实际上既不是对称算法,也不是非对称加密算法。它是消息摘要(安全散列)算法。
-
对称加密和非对称加密有哪些优缺点?
对称加密优点: 速度快,对称性加密通常在消息发送方需要加密大量数据时使用,具有算法公开、计算量小、加密速度快、加密效率高的特点。对称加密算法的优点在于加解密的高速度和使用长密钥时的解密性。
对称加密的缺点:
密钥的管理和分发非常困难,不够安全
。在数据传送前,发送方和接收方必须商定好密钥,并且双方都要保存好密钥,如果一方的密钥被泄露,那么加密信息也就不安全了,安全性得不到保证
。非对称加密优点:
安全性更高,公钥是公开的,秘钥是自己保存的,不需要将私钥给别人
。非对称加密缺点:
加密和解密花费时间长、速度慢,只适合对少量数据进行加密
-
MD5优缺点
MD5的优点:
计算速度快,加密速度快,不需要密钥
;可以检查文件的完整性,一旦文件被更改,MD5值会改变;防止被篡改,传输中一旦被篡改,计算出的MD5值也会改变;防止看到明文,公司存放密码存放的是MD5值。MD5的缺点:
作为散列算法,经过证实,仍然会存在两种不同数据会发生碰撞
;MD5的安全性。将用户的密码直接MD5后存储在数据库中是不安全的。很多人使用的密码是常见的组合,威胁者将这些密码的常见组合进行单向哈希,得到一个摘要组合,然后与数据库中的摘要进行比对即可获得对应的密码。
综上所述,md5是消息摘要算法,既不是对称算法也不是非对称算法。大部分情况下使用对称加密具有不错的安全性,如果需要分布式进行密钥分发,那么就考虑使用非对称加密;如果不需要可逆计算,则考虑散列算法(md5)。
参考资料:https://www.eolink.com/news/post/28567.html
具体的加密算法可以参考另一篇博文:浅析对称加密与非对称加密算法
经过分析,最终采用AES加密算法
对参数进行加密(有需要的话),加签
和验签
采用RSA非对称加密算法
。
4 请求头参数
服务端提供签名私钥 private_key
、应用id app_id
、商户号 merchant_no
,另外,所有请求的接口,都需要添加请求头Headers
,8个参数如下:
名称 | 类型 | 说明 | 是否必填 |
---|---|---|---|
method_name | String | 请求的方法名称 | 是 |
request_method | String | 请求方式(get or post) | 是 |
merchant_no | String | 商户号,一个商户可以有多个应用 | 是 |
msg_id | String | 通讯唯一编号 | 是 |
app_id | String | 应用系统唯一编号 | 是 |
sign_timestamp | String | 请求时应用端签名时的时间戳(毫秒单位),接入系统的时间误差不能超过3分钟,示例:1898098042342 | 是 |
api_gw_public_key | String | 网关公钥key,通过该公钥,才可以请求网关服务 | 是 |
request_sign | String | 签名信息,用来验签,通过签名机制,防止应用的请求参数被非法篡改,应用系统必须保证相关私钥不被泄露 | 是 |
- 请求是head头参数示例:
注:postman自定义参数,在Pre-request Script 输入: pm.environment.set('sign_timestamp',new Date().getTime());
5 签名以及验签机制
-
原理
请求接口的参数中添加sign签名+时间戳(毫秒单位)
具体实现原理:应用端生成RSA私钥加密签名串,和当前时间戳跟随请求的参数一起发送到后台,后台获取签名进行RSA公钥解密,然后获取系统当前时间戳和前端发送过来的时间戳做比较,如果两者相差超过180s,则认定为非法操作。这种方式既能保证防止请求重放,又能有效节省服务器资源注:应用端调用服务端接口时的公钥,必须为
PKCS8格式的RSA 2048密钥,该密钥还必须
经过Base64转换,服务端提供的API公钥也遵从同样标准。
-
本文章涉及到的安全机制梳理如下:
* 原有的token校验,无法做到安全性,因为token一般过期时间都很长,可以token多次访问 * 原来可以直接简单的http调用,现在必须经过一系列加签过程才可以访问服务资源 * 加签时timestamp以日为单位,跨日时可能会验签失败,这里可以根据实际业务情况调整 * api网关秘钥校验,只有有效的key才可以访问网关服务 * 应用端使用后端分配的私钥(定期修改增加安全性)进行加签,后端进行公钥验签,非对称加解密,增加了更高级别的安全性 * 服务端分配商户号和应用编号,保证唯一性 * 应用端加签和服务端验签,`时间戳不允许超过3min(可动态调整)`,保证即使参数被盗,3min过期后仍然无法访问 * 即使参数被非法获取,也仅仅是那个接口极短时间内有风险,大大提高的系统的防攻击特性
5.1 签名
-
系统采取的签名算法
SHA256WithRSA
签名算法,使用RSA非对称加密算法
,可以采用应用端 + 服务端双重签名的方式,来保证系统的安全性。Unlike symmetric encryption algorithms, asymmetric encryption algorithms require two keys: public key and private key. The public key and private key are a pair. If the data is encrypted with the public key, only the corresponding private key can be decrypted; If the data is encrypted with a private key, it can only be decrypted with the corresponding public key. Because encryption and decryption use two different keys, this algorithm is called asymmetric encryption algorithm
-
签名私钥
private_key
、应用idapp_id
、商户号merchant_no
由服务端提供
5.2 加签名思路
-
参数数据是
在应用端,使用私钥加密
的,这里在服务端只能用相应公钥解密
,即可验签是否有效 -
首先将method_name、request_method、merchant_no、msg_id、app_id、sign_timestamp拼接成一个字符串:
getUserInfo?method_name=getUserInfo&request_method=get&merchant_no=6666XF20230306001&msg_id=666&app_id=10000000006666001&charset=UTF-8&format=json&sign_type=RSA2×tamp=20230311
-
把拼接后的字符串,按照
ASCII
排序getUserInfo?app_id=10000000006666001&charset=UTF-8&format=json&merchant_no=6666XF20230306001&method_name=getUserInfo&msg_id=666&request_method=get&sign_type=RSA2×tamp=20230311
注:ASCII码的值从⼩到⼤为数字、⼤写英⽂字母、⼩写英⽂字母。48~57为0到9⼗个阿拉伯数字,65~90为26个⼤写英⽂字母,97~122号为26个⼩写英⽂字母。
-
对私钥进行Base64加密
-
对以上字符串进行
SHA256WithRSA
签名算法,运算之后得到:VN2O+0Kgf9SSQf36BhUo6EqvMcKPlRHEm+6TWBqqQxCXmW5a88NYnVafItEBvWBajY8nR+8w9zhpNrCZZ2dHyg0umPZSDi6cDQL/zVX15fwvZlMRnccNHnMJ4QfDuybEN4NGWUDsOWXiEvlAzTA3/QYrRWivXpyrKS9xlA/CqTOvZIdwlcyJEokGHP55aMrJCfJuVdmKI6oqkPMpNvwHe/fQHi0krhkOJw7aa97WJ0tptqdpmnANz/lvCEvFBmvUIMVFtjpEutPTRSAL1miDZeKHdfPlUIMN0G/qoTdSyFF4yh0Nvk0rSvGd0/tVSxDUBE5sLhbjM5K+gLpo2iOQGQ==
-
最后,将VsMiLe6字符串,添加到sign开头处,再次增加安全性,最后得到:
VsMiLe6VN2O+0Kgf9SSQf36BhUo6EqvMcKPlRHEm+6TWBqqQxCXmW5a88NYnVafItEBvWBajY8nR+8w9zhpNrCZZ2dHyg0umPZSDi6cDQL/zVX15fwvZlMRnccNHnMJ4QfDuybEN4NGWUDsOWXiEvlAzTA3/QYrRWivXpyrKS9xlA/CqTOvZIdwlcyJEokGHP55aMrJCfJuVdmKI6oqkPMpNvwHe/fQHi0krhkOJw7aa97WJ0tptqdpmnANz/lvCEvFBmvUIMVFtjpEutPTRSAL1miDZeKHdfPlUIMN0G/qoTdSyFF4yh0Nvk0rSvGd0/tVSxDUBE5sLhbjM5K+gLpo2iOQGQ==
5.3 验签思路
使用原有的请求入参,以及传递来的sign进行公钥解密,如果解密成功,就说明验签通过,否则验签失败。
注:如果使用公钥加密,则需要私钥进行解密,是双向的。
6 功能核心代码实现
6.1 SDK核心代码
6.1.1 SmileConstants常量类
/**
* icbc.com.cn Inc.
* Copyright (c) 2004-2016 All Rights Reserved.
*/
package cn.smilehappiness.security.constant;
/**
* <p>
* SmileConstants
* <p/>
*
* @author
* @Date 2023/3/6 19:30
*/
public class SmileConstants
public static final String SIGN_TYPE = "sign_type";
public static final String SIGN_TYPE_RSA = "RSA";
public static final String SIGN_TYPE_RSA2 = "RSA2";
public static final String SIGN_TYPE_SM2 = "SM2";
public static final String SIGN_TYPE_CA = "CA";
public static final String SIGN_TYPE_SM = "SM";
public static final String SIGN_TYPE_EM = "EM";
public static final String SIGN_TYPE_EM_SM = "EM-SM";
public static final String SIGN_SHA1RSA_ALGORITHMS = "SHA1WithRSA";
public static final String SIGN_SHA256RSA_ALGORITHMS = "SHA256WithRSA";
public static final String ENCRYPT_TYPE_AES = "AES";
public static final String METHOD_NAME = "method_name";
public static final String REQUEST_METHOD = "request_method";
public static final String MERCHANT_NO = "merchant_no";
public static final String APP_ID = "app_id";
public static final String SIGN_TIMESTAMP = "sign_timestamp";
public static final String API_GW_PUBLIC_KEY = "api_gw_public_key";
public static final String REQUEST_SIGN = "request_sign";
public static final String FORMAT = "format";
public static final String TIMESTAMP = "timestamp";
public static final String SIGN = "sign";
public static final String APP_AUTH_TOKEN = "app_auth_token";
public static final String CHARSET = "charset";
public static final String NOTIFY_URL = "notify_url";
public static final String RETURN_URL = "return_url";
public static final String ENCRYPT_TYPE = "encrypt_type";
public static final String BIZ_CONTENT_KEY = "biz_content";
/**
* Default time format
**/
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String YYYY_MM_DD = "yyyyMMdd";
/**
* Date Default time zone
**/
public static final String DATE_TIMEZONE = "GMT+8";
/**
* UTF-8 character set
**/
public static final String CHARSET_UTF8 = "UTF-8";
/**
* GBK character set
**/
public static final String CHARSET_GBK = "GBK";
/**
* JSON Format
*/
public static final String FORMAT_JSON = "json";
/**
* XML Format
*/
public static final String FORMAT_XML = "xml";
public static final String CA = "ca";
public static final String PASSWORD = "password";
public static final String RESPONSE_BIZ_CONTENT = "response_biz_content";
/**
* Message unique number
**/
public static final String MSG_ID = "msg_id";
/**
* sdk The version number in the headerkey
*/
public static final String VERSION_HEADER_NAME = "APIGW-VERSION";
/**
* sdk Region number, for overseas institutions
*/
public static final String ZONE_NO = "Zone-No";
/**
* Request type
*/
public static final String REQUEST_Type = "Request-Type";
/**
* For em-type signatures, send a request to CICC Cryptography
*/
public static final String EM_CFCA = "CFCA";
/**
* For em-type signature, send a request to the NC client
*/
public static final String EM_NC = "NC";
/**
* Refined information
*/
public static final String REFINE_INFO = "Apirefined-Info";
6.1.2 SmileClient类
package cn.smilehappiness.security.api.client;
import cn.smilehappiness.security.constant.SmileConstants;
import cn.smilehappiness.security.dto.BaseRequest;
import cn.smilehappiness.security.utils.AesCryptUtil;
import cn.smilehappiness.security.utils.SecurityStringUtil;
import cn.smilehappiness.security.utils.SmileHashMap;
import cn.smilehappiness.security.utils.SmileSignatureUtil;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* <p>
* smile client base deal
* <p/>
*
* @author
* @Date 2023/3/7 11:08
*/
public class SmileClient
private static final Logger logger = LoggerFactory.getLogger(SmileClient.class);
protected String apiPublicKey;
protected String merchantNo;
protected String appId;
protected String signType = SmileConstants.SIGN_TYPE_RSA;
protected String privateKey;
protected String publicKey;
protected String charset = SmileConstants.CHARSET_UTF8;
protected String format = SmileConstants.FORMAT_JSON;
protected String encryptType;
protected String encryptKey;
public SmileClient()
public SmileClient(String apiPublicKeyParam, String appId, String merchantNo, String signType, String privateKey, String publicKey, String charset, String format, String encryptType, String encryptKey)
this.apiPublicKey = apiPublicKeyParam;
this.appId = appId;
this.merchantNo = merchantNo;
this.signType = signType;
this.privateKey = privateKey;
this.publicKey = publicKey;
this.charset = charset;
this.format = format;
this.encryptType = encryptType;
this.encryptKey = encryptKey;
public SmileClient(String apiPublicKeyParam, String appId, String merchantNo, String privateKey)
this(apiPublicKeyParam, appId, merchantNo, SmileConstants.SIGN_TYPE_RSA, privateKey, null, SmileConstants.CHARSET_UTF8, SmileConstants.FORMAT_JSON, null, null);
/**
* <p>
* rsa,rsa2,four params
* <p/>
*
* @param apiPublicKeyParam
* @param appId
* @param signType
* @param privateKey
* @return
* @Date 2023/3/7 11:08
*/
public SmileClient(String apiPublicKeyParam, String appId, String merchantNo, String signType, String privateKey)
this(apiPublicKeyParam, appId, merchantNo, signType, privateKey, null, SmileConstants.CHARSET_UTF8, SmileConstants.FORMAT_JSON, null, null);
/**
* <p>
* check sign,five params
* <p/>
*
* @param apiPublicKeyParam
* @param appId
* @param signType
* @param privateKey
* @return
* @Date 2023/3/8 15:40
*/
public SmileClient(String apiPublicKeyParam, String appId, String merchantNo, String signType, String privateKey, String publicKey)
this(apiPublicKeyParam, appId, merchantNo, signType, privateKey, publicKey, SmileConstants.CHARSET_UTF8, SmileConstants.FORMAT_JSON, null, null);
/**
* <p>
* six params -AES encrypt
* <p/>
*
* @param apiPublicKeyParam
* @param appId
* @param signType
* @param privateKey
* @param encryptType
* @param encryptKey
* @return
* @Date 2023/3/8 10:19
*/
public SmileClient(String apiPublicKeyParam, String appId, String merchantNo, String signType, String privateKey, String encryptType, String encryptKey)
this(apiPublicKeyParam, appId, merchantNo, signType, privateKey, null, SmileConstants.CHARSET_UTF8, SmileConstants.FORMAT_JSON, encryptType, encryptKey);
public SmileClient(String apiPublicKeyParam, String appId, String merchantNo, String signType, String privateKey, String publicKey, String encryptType, String encryptKey)
this(apiPublicKeyParam, appId, merchantNo, signType, privateKey, publicKey, SmileConstants.CHARSET_UTF8, SmileConstants.FORMAT_JSON, encryptType, encryptKey);
private boolean checkApiPublicKeyLegal(String apiPublicKeyParam)
if (StringUtils.isBlank(apiPublicKeyParam) || !apiPublicKey.equals(apiPublicKeyParam))
throw new RuntimeException("apiPublicKey param [" + apiPublicKeyParam + "] is unLegal");
return true;
/**
* <p>
* prepare params
* <p/>
*
* @param request
* @return cn.smilehappiness.security.utils.IcbcHashMap
* @Date 2023/3/7 13:56
*/
public SmileHashMap prepareParams(BaseRequest<?> request)
SmileHashMap params = new SmileHashMap();
String strToSign = this.prepareParamStr(request, params);
logger.info("addSign strToSign: ", strToSign);
if (signType.equals(SmileConstants.SIGN_TYPE_RSA) || signType.equals(SmileConstants.SIGN_TYPE_RSA2))
String signedStr = SmileSignatureUtil.sign(strToSign, signType, privateKey, charset);
params.put(SmileConstants.SIGN, signedStr);
else
// Other signature types
logger.error("signType is not supported", signType);
throw new RuntimeException("signType " + signType + " is not supported");
return params;
/**
* <p>
* prepare param str
* <p/>
*
* @param request
* @param params
* @return java.lang.String
* @Date 2023/3/8 16:34
*/
private String prepareParamStr(BaseRequest<?> request, SmileHashMap params)
Map<String, String> extraParams = request.getExtraParameters();
if (extraParams != null)
params.putAll(extraParams);
//appId Is the public variable of the class
params.put(SmileConstants.METHOD_NAME, request.getMethodName());
params.put(SmileConstants.REQUEST_METHOD, StringUtils.lowerCase(request.getRequestMethod()));
params.put(SmileConstants.MERCHANT_NO, merchantNo);
params.put(SmileConstants.APP_ID, appId);
params.put(SmileConstants.MSG_ID, request.getMsgId());
params.put(SmileConstants.SIGN_TYPE, signType);
params.put(SmileConstants.CHARSET, charset);
params.put(SmileConstants.FORMAT, format);
try
// Get the timestamp, where you can achieve a higher level of control over the time dimension
long timestamp 以上是关于Java应用服务系统安全性,签名和验签浅析的主要内容,如果未能解决你的问题,请参考以下文章
用Java实现RSA加解密及签名和验签——.pem文件格式秘钥