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 typeslogger.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 classparams.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 dimensionlong timestamp = System.currentTimeMillis();DateFormat df = new SimpleDateFormat(SmileConstants.YYYY_MM_DD);df.setTimeZone(TimeZone.getTimeZone(SmileConstants.DATE_TIMEZONE));//time error is within 3 minutes, gateway control,this only 1d validityparams.put(SmileConstants.TIMESTAMP, df.format(new Date(timestamp)));} catch (Exception exception) {exception.printStackTrace();}// Core content parameters need to be encryptedString bizContentStr = buildBizContentStr(request);if (request.isEncryptFlag()) {if (SecurityStringUtil.areEmpty(encryptType, encryptKey)) {logger.error("request need be encrypted, encrypt type and encrypt key can not be null");throw new RuntimeException("request need be encrypted, encrypt type and encrypt key can not be null");}if (bizContentStr != null) {params.put(SmileConstants.ENCRYPT_TYPE, encryptType);params.put(SmileConstants.BIZ_CONTENT_KEY, AesCryptUtil.encryptContent(bizContentStr, encryptType, encryptKey, charset));}} else {// Do not encrypt, fill in the requestParam fieldparams.put(SmileConstants.BIZ_CONTENT_KEY, bizContentStr);}// Sort by rulereturn this.buildOrderedSignStr(request.getMethodName(), params);}/*** <p>* build biz content str* <p/>** @param request* @return java.lang.String* @Date 2023/3/7 13:43*/protected String buildBizContentStr(BaseRequest<?> request) {if (ObjectUtils.isEmpty(request.getBizContent())) {return null;}if (this.format.equals(SmileConstants.FORMAT_JSON)) {return JSON.toJSONString(request.getBizContent());} else {logger.error("only support json format, current format is not supported, format: {}", this.format);throw new RuntimeException("only support json format, current format is not supported, format: " + this.format);}}/*** <p>* build ordered sign str* <p/>** @param methodName* @param params* @return java.lang.String* @Date 2023/3/7 11:25*/private String buildOrderedSignStr(String methodName, Map<String, String> params) {Map<String, String> sortedMap = new TreeMap();sortedMap.putAll(params);Set<Map.Entry<String, String>> entries = sortedMap.entrySet();boolean hasParam = false;StringBuilder sb = new StringBuilder(methodName);sb.append("?");Iterator var6 = entries.iterator();while (var6.hasNext()) {Map.Entry<String, String> entry = (Map.Entry) var6.next();String name = entry.getKey();String value = entry.getValue();if (SecurityStringUtil.areNotEmpty(name, value)) {if (hasParam) {sb.append("&");} else {hasParam = true;}sb.append(name).append("=").append(value);}}return sb.toString();}/*** <p>* signCheck* <p/>** @param request* @param signStr* @return T* @Date 2023/3/8 13:29*/public <T> T signCheck(BaseRequest<T> request, String signStr) {if (StringUtils.isBlank(signStr)) {throw new RuntimeException("sign check fail, signStr param is null");}//check api public key legalthis.checkApiPublicKeyLegal(request.getApiGwPublicKey());SmileHashMap params = new SmileHashMap();String strToSign = this.prepareParamStr(request, params);logger.info("signCheck strToSign: {}", strToSign);int indexOfSignStart = SmileSignatureUtil.SIGN_PREFIX.length();String sign = signStr.substring(indexOfSignStart);boolean passed = SmileSignatureUtil.verifySign(strToSign, this.signType, this.publicKey, this.charset, sign);if (!passed) {logger.error("sign verify not passed, please check");throw new RuntimeException("sign verify not passed, please check");}if (!request.isEncryptFlag()) {return null;}// The signature is base64 encoded, and no comma will appearString startKey = StringUtils.join(SmileConstants.BIZ_CONTENT_KEY, "=");int indexOfStart = strToSign.lastIndexOf(startKey);int indexOfEnd = strToSign.lastIndexOf("&charset");String bizContentStr = strToSign.substring(indexOfStart, indexOfEnd).replace(startKey, "");String bizContentDecryptResult = AesCryptUtil.decryptContent(bizContentStr, this.encryptType, this.encryptKey, this.charset);return (T) JSON.parse(bizContentDecryptResult);}}
6.1.3 签名工具类
package cn.smilehappiness.security.utils;import cn.smilehappiness.security.constant.SmileConstants;
import cn.smilehappiness.security.utils.crypt.RSAUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.UnsupportedEncodingException;/*** <p>* smile signature util* <p/>** @author* @Date 2023/3/7 14:04*/
public class SmileSignatureUtil {private static final Logger logger = LoggerFactory.getLogger(SmileSignatureUtil.class);public static final String SIGN_PREFIX = "VsMiLe6";private SmileSignatureUtil() {}public static String sign(String content, String signType, String privateKey, String charset) {return sign(content, signType, privateKey, charset, null);}/*** <p>* sign* <p/>** @param content* @param signType* @param privateKey* @param charset* @param password* @return java.lang.String* @Date 2023/3/7 14:15*/public static String sign(String content, String signType, String privateKey, String charset, String password) {try {byte[] contentBytes = content.getBytes(charset);if (signType.equals(SmileConstants.SIGN_TYPE_RSA)) {String signStr = RSAUtil.sign(contentBytes, Base64.decodeBase64(privateKey), SmileConstants.SIGN_SHA1RSA_ALGORITHMS);return StringUtils.join(SIGN_PREFIX, signStr);} else if (signType.equals(SmileConstants.SIGN_TYPE_RSA2)) {String signStr = RSAUtil.sign(contentBytes, Base64.decodeBase64(privateKey), SmileConstants.SIGN_SHA256RSA_ALGORITHMS);return StringUtils.join(SIGN_PREFIX, signStr);}logger.error("not support signType");throw new RuntimeException("not support signType.");} catch (UnsupportedEncodingException e) {logger.error("get content charset exception, content: {} , charset:{} ,error info: {}", content, charset, e);throw new RuntimeException("get content charset exception. content: " + content + " charset: " + charset + e);} catch (Exception e) {logger.error("sign exception.", e);throw new RuntimeException("sign exception." + e);}}/*** <p>* verify sign* <p/>** @param content* @param signType* @param publicKey* @param charset* @param sign* @return boolean* @Date 2023/3/7 14:15*/public static boolean verifySign(String content, String signType, String publicKey, String charset, String sign) {try {byte[] contentBytes = content.getBytes(charset);if (signType.equals(SmileConstants.SIGN_TYPE_RSA)) {return RSAUtil.verify(contentBytes, Base64.decodeBase64(publicKey), sign, SmileConstants.SIGN_SHA1RSA_ALGORITHMS);} else if (signType.equals(SmileConstants.SIGN_TYPE_RSA2)) {return RSAUtil.verify(contentBytes, Base64.decodeBase64(publicKey), sign, SmileConstants.SIGN_SHA256RSA_ALGORITHMS);}logger.error("not support signType.");throw new RuntimeException("not support signType.");} catch (UnsupportedEncodingException e) {logger.error("get content charset exception. content: " + content + " charset: " + charset, e);throw new RuntimeException("get content charset exception. content: " + content + " charset: " + charset + e);} catch (Exception e) {logger.error("sign verify exception.", e);throw new RuntimeException("sign verify exception." + e);}}}
6.2 加签代码示例
@Testpublic void testAddSign() {BaseRequest<Map<String, Object>> request = new BaseRequest<>();//sign paramrequest.setMethodName("getUserInfo");request.setRequestMethod("get");request.setMerchantNo("6666XF20230306001");request.setAppId("10000000006666001");request.setMsgId("666");request.setSignTimestamp(System.currentTimeMillis());request.setApiGwPublicKey(smileSecurityConfig.getApiGwPublicKey());SmileClient client = new SmileClient(smileSecurityConfig.getApiGwPublicKey(), smileSecurityConfig.getAppId(), smileSecurityConfig.getMerchantNo(), SmileConstants.SIGN_TYPE_RSA2, smileSecurityConfig.getPrivateKey());SmileHashMap smileHashMap = client.prepareParams(request);System.out.println(JSON.toJSONString(smileHashMap));}
6.3 验签代码示例
package com.itn.idn.gateway.security;import cn.smilehappiness.security.api.client.SmileClient;
import cn.smilehappiness.security.config.SmileSecurityConfig;
import cn.smilehappiness.security.constant.SmileConstants;
import cn.smilehappiness.security.dto.BaseRequest;
import com.alibaba.fastjson.JSON;
import com.itn.idn.gateway.ErrorService;
import com.itn.idn.gateway.enums.GatewayExceptionEnum;
import com.itn.idn.gateway.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import java.util.Arrays;
import java.util.Map;/*** <p>* api security interceptor* <p/>** @author* @Date 2023/3/10 17:40*/
@Component
@RefreshScope
@ConfigurationProperties("security.config")
public class ApiSecurityInterceptor implements GlobalFilter, Ordered {private final Logger logger = LoggerFactory.getLogger(ApiSecurityInterceptor.class);private final long ALLOW_INTERVAL_TIMESTAMP = 180000L;@Autowiredprivate ErrorService errorService;@Autowiredprivate SmileSecurityConfig smileSecurityConfig;private String[] skipAuthUrls;public String[] getSkipAuthUrls() {return skipAuthUrls;}public void setSkipAuthUrls(String[] skipAuthUrls) {this.skipAuthUrls = skipAuthUrls;}/*** sign verify filter** @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String url = exchange.getRequest().getURI().getPath();// Skip paths that do not require validationif (StringUtils.matches(url, Arrays.asList(skipAuthUrls))) {return chain.filter(exchange);}String methodName = exchange.getRequest().getHeaders().getFirst(SmileConstants.METHOD_NAME);String requestMethod = exchange.getRequest().getHeaders().getFirst(SmileConstants.REQUEST_METHOD);String merchantNo = exchange.getRequest().getHeaders().getFirst(SmileConstants.MERCHANT_NO);String appId = exchange.getRequest().getHeaders().getFirst(SmileConstants.APP_ID);String msgId = exchange.getRequest().getHeaders().getFirst(SmileConstants.MSG_ID);String signTimestamp = exchange.getRequest().getHeaders().getFirst(SmileConstants.SIGN_TIMESTAMP);String apiGwPublicKey = exchange.getRequest().getHeaders().getFirst(SmileConstants.API_GW_PUBLIC_KEY);String requestSign = exchange.getRequest().getHeaders().getFirst(SmileConstants.REQUEST_SIGN);//head need paramsBaseRequest<Map<String, Object>> request = new BaseRequest<>();//sign paramrequest.setMethodName(methodName);request.setRequestMethod(requestMethod);request.setMerchantNo(merchantNo);request.setAppId(appId);request.setMsgId(msgId);ServerHttpResponse resp = exchange.getResponse();if (StringUtils.isBlank(signTimestamp)) {logger.error("signTimestamp params is null");return errorService.authError(resp, GatewayExceptionEnum.SIGN_CHECK_FAIL);}request.setSignTimestamp(Long.parseLong(signTimestamp));long intervalTimestamp = System.currentTimeMillis() - request.getSignTimestamp();logger.info("request url:{},sign and request interval timestamp:{}", url, intervalTimestamp);if (Math.abs(intervalTimestamp) > ALLOW_INTERVAL_TIMESTAMP) {logger.error("intervalTimestamp {} is greater than 3min", intervalTimestamp);return errorService.authError(resp, GatewayExceptionEnum.SIGN_CHECK_FAIL);}request.setApiGwPublicKey(apiGwPublicKey);logger.info("filter sign check params:{}", JSON.toJSONString(request));SmileClient client = new SmileClient(request.getApiGwPublicKey(), request.getAppId(), request.getMerchantNo(), SmileConstants.SIGN_TYPE_RSA2, null, smileSecurityConfig.getPublicKey());try {//signCheck strToSign: 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=20230311client.signCheck(request, requestSign);} catch (RuntimeException exception) {return errorService.authError(resp, GatewayExceptionEnum.SIGN_CHECK_FAIL);}return chain.filter(exchange);}@Overridepublic int getOrder() {return -100;}}
写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!
如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!
给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!
相关文章:

Java应用服务系统安全性,签名和验签浅析
1 前言 随着互联网的普及,分布式服务部署越来越流行,服务之间通信的安全性也是越来越值得关注。这里,笔者把应用与服务之间通信时,进行的的安全性相关,加签与验签,进行了一个简单的记录。 2 安全性痛点 …...

spring中bean的实例化
构造方法实现实例化 无参构造器实例化 我们之前用的就一直是无参构造器实现实例化,虽然没有在类中写构造器,但是每个类都会有一个默认的无参构造器 有参构造器实例化 相比于无参构造器,我们只需要传入参数就可以了 我们可以通过construc…...

磨皮插件portraiture2023最新中文版
Portraiture滤镜是一款 Photoshop,Lightroom 和 Aperture 插件,DobeLighttroom 的 Portraiture 消除了选择性掩蔽和逐像素处理的繁琐的手工劳动,以帮助您在肖像修整方面取得卓越的效果。它是一个强大的,但用户友好的插件照明.这是…...
记录每日LeetCode 2269.找到一个数组的K美丽值 Java实现
题目描述: 一个整数 num 的 k 美丽值定义为 num 中符合以下条件的 子字符串 数目: 子字符串长度为 k 。 子字符串能整除 num 。 给你整数 num 和 k ,请你返回 num 的 k 美丽值。 注意: 允许有 前缀 0 。 0 不能整除任何…...

代码管理--svnadmin工具介绍
1、简介 SVNAdmin2 是一款通过图形界面管理服务端SVN的web程序。正常情况下配置SVN仓库的人员权限需要登录到服务器手动修改 authz 和 passwd 两个文件,当仓库结构和人员权限上了规模后,手动管理就变的非常容易出错,本系统能够识别人员和权限…...

Git的基本使用以及上传到GitHub
GIT的基本使用一、安装并配置GIT二、Git的基本操作三、使用GIT上传至GitHub四、Git分支一、安装并配置GIT 1.安装GIT连接 GIT安装包链接 2.打开GIT 鼠标右键点击Git Bash Here 安装完 Git 之后,第一件事就是设置自己的用户名和邮件地址。因为通过 Git 对项目进行…...

国科大论文latex模板中可能的注意事项
背景 国科大2022年9月发布了毕业论文的LaTeX模板,它是在ucasthesis上修改而来的,但近日使用国科大发布版本时发现有几点不同以及需要注意的地方。本人只会简单使用latex,但并不熟悉latex样式编辑,因此以下介绍与方法仅供参考。仅…...

ABAP 怎样将XML和JSON格式转换为HTML格式显示
ABAP 怎样将XML和JSON格式转换为HTML格式显示 一、将JSON格式转换为HTML格式 BAP接口程序开发中时常会用到JSON格式来传输数据,在监控传输的JSON串内容时,把JSON转换为HTML格式来显示会很便利。下面提供一个简单例子来实现JSON转化为HTML并显示的功能。…...
基础课DP
DP 背包问题01背包问题完全背包问题多重背包问题多重背包问题II分组背包问题线性DP数字三角形最长上升子序列最长上升子序列II最长公共子序列编组距离区间DP石子合并计数类DP整数划分数位统计DP计数问题状态压缩DP蒙德里安的梦想最短Ha路径树形DP没有上司的舞会...

基于支持向量机SVM的风电场NWP数据预测,SVM的详细原理
目录 支持向量机SVM的详细原理 SVM的定义 SVM理论 Libsvm工具箱详解 简介 参数说明 易错及常见问题 SVM应用实例,基于SVM的风电场NWP预测 结果分析 展望 支持向量机SVM的详细原理 SVM的定义 支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型是定…...

webRtc概念
webRtc概念 以下的文档整理来自此链接 文档整理了一系列实现web通用接口的ECMAScript APIs ,这些接口是为了支持浏览器或者一些其他实现了实时交换协议的设备进行媒体信息和程序数据交换。 1、实现点对点通信的规范: NAT穿透实现与远端节点链接比如&a…...

数据结构与算法基础(王卓)(16):KMP算法详解(代码实现)
实现代码的过程中 具体细节、问题: (1):关于写Get_next函数的标题: 现象: PPT上写的是: void get_next(SString T, int &next[]) 然而并不能运行,而当我们去掉了引用符号&…...

九龙证券|盘前直接腰斩,银行巨头紧急“拔网线”!美股银行股又崩了?
见证历史了,又有一家银行巨子倒下? 美股银行股团体暴降 上一交易日暴降超60%的硅谷银行持续面对腥风血雨。盘前,硅谷银行跌幅超50%,随后,公司宣布盘前暂停交易,等待刊发消息。 而最新消息显现,…...
接口优化常用思路
空间换时间 计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有了解决问题的第1招–以空间换时间 合理使用缓存就是一个很好的例子,针对一些频繁使用且不频繁变更的数据&#…...
【SpringCloud】SpringCloud面试题整理
文章目录1、什么是Spring Cloud?2、Spring Cloud和Dubbo的区别3、REST和RPC的区别4、SpringCloud如何实现服务的注册和发现5、什么是服务熔断和服务降级?6、项目中zuul常用的功能7、服务网关的作用8、ribbon和feign区别9、ribbon的负载均衡策略10、简述什…...
一些数据库知识点总结
DB2数据库:从数据库表中第I条记录开始检索J条记录SELECT * FROM (SELECT A.*, ROW_NUMBER() OVER() AS NFROM (SELECT * FROM table_name) AS A)WHERE N > I AND N < J;Oracle数据库:从数据库表中第M条记录开始检索N条记录SELECT * FROM (SELECT R…...
Python unittest 模块
一、Unittest 的几个基本概念 TestCase :要写的具体的测试用例TestSuite: 多个测试用例集合(或测试套件/测试集)TestLoader:用来加载 TestCase 到 TestSuite中的(更通俗一点,就是用来把符合我们…...

Spring - Spring IoC 容器相关面试题总结
文章目录01. Spring IoC 和依赖注入是什么?02. Spring IoC 的优点和缺点分别是什么?03. Spring IoC 有什么作用和功能?04. Spring 依赖注入的方式?05. Spring 构造器注入和 setter 方法注入的区别?06. Spring 依赖注入…...

顺序表来喏!!!
前言:还记得前面的文章:《通讯录的实现》吗?通讯录的完成就借助了顺序表这种数据结构!!!那么今天我们就来介绍我们的顺序表介绍顺序表前,我们来了解一下线性表的概念线性表:线性表&a…...

【H2实践】之 SpringBoot 与 H2 数据交互
一、目标 本文是【H2实践】之认识 H2,【H2实践】之 SpringBoot 整合的后续。前文分别介绍了 H2 及其简单使用,并完成了 H2 与 SpringBoot 的整合。本文将紧接 【H2实践】之 SpringBoot 整合 探索实用 SpringBoot 结合 JPA 通过 web 接口操作 H2 数据库的…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...

蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...

C++:多态机制详解
目录 一. 多态的概念 1.静态多态(编译时多态) 二.动态多态的定义及实现 1.多态的构成条件 2.虚函数 3.虚函数的重写/覆盖 4.虚函数重写的一些其他问题 1).协变 2).析构函数的重写 5.override 和 final关键字 1&#…...