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非对称加密算法

3.3 RSA公钥和私钥区别,哪个加密哪个解密

公钥和私钥在一些银行系统、第三方支付系统SDK中经常会遇到,刚接触公钥私钥的朋友们估计很难区分两者的区别。

3.3.1 RSA公钥和私钥是什么

首先来说,RSA是一种非对称加密算法,它是由三位数学家(Rivest、Shamir、Adleman)设计出来的。非对称加密是相对于对称加密而言的。对称加密算法是指加密解密使用的是同一个秘钥,而非对称加密是由两个密钥(公钥、私钥)来进行加密解密的,由此可见非对称加密安全性更高。

公钥顾名思义就是公开的密钥会发放给多个持有人,而私钥是私有密码往往只有一个持有人,公私钥特性:

  • 公钥与私钥是成对出现的

  • 私钥文件中包含了公钥数据,所以可以基于私钥导出公钥

  • 密钥越长,越难破解,所以2048位密钥比1024位密钥要更安全

  • 公钥和私钥都是密钥,被公开的那个就是公钥,没有被公开的那个就是私钥

3.3.2 公钥和私钥都可用于加密和解密

公钥和私钥都可以用于加解密操作,用公钥加密的数据只能由对应的私钥解密,反之亦然。虽说两者都可用于加密,但是不同场景使用不同的密钥来加密,规则如下:

  • 私钥用于签名、公钥用于验签

    签名和加密作用不同,签名并不是为了保密,而是为了保证这个签名是由特定的某个人签名的,而不是被其它人伪造的签名,所以私钥的私有性就适合用在签名用途上。

    私钥签名后,只能由对应的公钥解密,公钥又是公开的(很多人可持有),所以这些人拿着公钥来解密,解密成功后就能判断出是持有私钥的人做的签名,验证了身份合法性。

  • 公钥用于加密、私钥用于解密,这才能起到加密作用

    因为公钥是公开的,很多人可以持有公钥。若用私钥加密,那所有持有公钥的人都可以进行解密,这是不安全的!

    若用公钥加密,那只能由私钥解密,而私钥是私有不公开的,只能由特定的私钥持有人解密,保证的数据的安全性。

RSA算法可以总结为四句话:公钥加密、私钥解密、私钥签名、公钥验签。加密是防止信息泄露,而签名是为了防止信息被篡改。

在这里插入图片描述

该内容摘自:https://blog.csdn.net/weixin_37989267/article/details/126641628

4 请求头参数

服务端提供签名私钥 private_key、应用id app_id、商户号 merchant_no,另外,所有请求的接口,都需要添加请求头Headers,8个参数如下:

名称类型说明是否必填
method_nameString请求的方法名称
request_methodString请求方式(get or post)
merchant_noString商户号,一个商户可以有多个应用
msg_idString通讯唯一编号
app_idString应用系统唯一编号
sign_timestampString请求时应用端签名时的时间戳(毫秒单位),接入系统的时间误差不能超过3分钟,示例:1898098042342
api_gw_public_keyString网关公钥key,通过该公钥,才可以请求网关服务
request_signString签名信息,用来验签,通过签名机制,防止应用的请求参数被非法篡改,应用系统必须保证相关私钥不被泄露
  • 请求是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&timestamp=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&timestamp=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 = 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 validity
            params.put(SmileConstants.TIMESTAMP, df.format(new Date(timestamp)));
        } catch (Exception exception) {
            exception.printStackTrace();
        }

        // Core content parameters need to be encrypted
        String 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 field
            params.put(SmileConstants.BIZ_CONTENT_KEY, bizContentStr);
        }

        // Sort by rule
        return 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 legal
        this.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 appear
        String 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 加签代码示例

	@Test
    public void testAddSign() {
        BaseRequest<Map<String, Object>> request = new BaseRequest<>();
        //sign param
        request.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;

    @Autowired
    private ErrorService errorService;
    @Autowired
    private 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
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        // Skip paths that do not require validation
        if (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 params
        BaseRequest<Map<String, Object>> request = new BaseRequest<>();
        //sign param
        request.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&timestamp=20230311
            client.signCheck(request, requestSign);
        } catch (RuntimeException exception) {
            return errorService.authError(resp, GatewayExceptionEnum.SIGN_CHECK_FAIL);
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -100;
    }

}

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!

给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值