提供三方API接口、调用第三方接口API接口、模拟API接口(一)通过signature签名验证,避免参数恶意修改

为什么要设计安全的api接口

运行在外网服务器的接口暴露在整个互联网中,可能会受到各种攻击,例如恶意爬取服务器数据、恶意篡改请求数据等,因此需要一个机制去保证api接口是相对安全的。

本项目api接口安全设计

本项目api接口的安全性主要是为了请求参数不会被篡改和防止接口被多次调用而产生脏数据,实现方案主要围绕令牌(token)、时间戳(timestamp)、签名(signature)三个机制展开设计。

模拟前端签名与后端验证签名

RSA密钥对生成

KeyPairGenerator生成RSA密钥对

自定义Generator密钥生成器:

package com.atguigu.signcenter.util;

import org.apache.tomcat.util.codec.binary.Base64;

import java.io.UnsupportedEncodingException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

/**
 * 自定义Generator密钥生成器:
 * 描述:密钥生成器
 *
 * RSA密钥对生成
 * KeyPairGenerator生成RSA密钥对
 *
 *
 * @author: jd
 * @create: 2024-07-30
 */
public class Generator {

    public static final  String ALGORITHM_RSA ="RSA";

    private static final String RSA_CHARSET = "UTF-8";

    public static void main(String[] args) throws Exception  {
        // 初始化密钥对生成器,生成RSA密钥对
        KeyPairGenerator keyPairGen  = KeyPairGenerator.getInstance(ALGORITHM_RSA);
        // // 可以指定密钥大小,如1024位
        keyPairGen.initialize(1024);
        // 生成密钥对
        KeyPair keyPair = keyPairGen.generateKeyPair();
        //从密钥对中获取公钥
        RSAPublicKey rsaPublicKey = (RSAPublicKey)keyPair.getPublic();
        // 获取公钥的ASN.1 DER编码  
        byte[] keyBs  = rsaPublicKey.getEncoded();
        String publicKey = encodeBase64(keyBs);
        System.out.println("生成的公钥:\r\n" + publicKey);
        //从密钥对中获取私钥
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
        // 获取私钥的ASN.1 DER编码
        keyBs = rsaPrivateKey.getEncoded();
        String privateKey = encodeBase64(keyBs);
        System.out.println("生成的私钥:\r\n" + privateKey);
    }


    /**
     * 描述:byte数组转String
     * @param source  入参byte数组
     * @return
     */
    public static String encodeBase64(byte[] source) throws UnsupportedEncodingException {
        return new String(Base64.encodeBase64(source), RSA_CHARSET);
    }

    /**
     * 描述:String转byte数组
     * @param target 入参字符串
     * @return
     * @throws Exception
     */
    public static byte[] decodeBase64(String target) throws Exception {
        return Base64.decodeBase64(target.getBytes(RSA_CHARSET));
    }

}

测试效果:
在这里插入图片描述

生成的公钥:
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDSv50KbmfIibYRZoFnVLdtjp/sjDAAHrAwWu9lLk6zSfx9QhEqGFbeLD6tDuS0vcBoXRCrXphOvDbMGjEWPRGw0bz2Q65zfwLTyk//XBgezDTNBK3VYYeMu9/Skgdm9lZDsvXFHPBypTfJf8/Co7sKZRXtwS69pT0UK9uMWN0+3wIDAQAB
生成的私钥:
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANK/nQpuZ8iJthFmgWdUt22On+yMMAAesDBa72UuTrNJ/H1CESoYVt4sPq0O5LS9wGhdEKtemE68NswaMRY9EbDRvPZDrnN/AtPKT/9cGB7MNM0ErdVhh4y739KSB2b2VkOy9cUc8HKlN8l/z8KjuwplFe3BLr2lPRQr24xY3T7fAgMBAAECgYBSVqS/f5Uyx4MH11m1imbD+hZHcBoM4fCKY+zmRipfaAeq1JYqMSo3UWgHays15nD3FW0+1w2ArY5nPdBx1NhE3D+icwKbbtvIpgfMRGU0EYBkm2pNdGaQDI3UYyLeMOjytletH85Dj0kue+4YHSOVDPGj2sNAHtaKTL57d65Q8QJBAPd5YALQgNAQLNCjhaDZYbdeySOrtp7uP0kAs37NywFgf9L14FMi0orTr1FduexYs2cB9tPpMJp43nzDppohNCcCQQDaAlUrbjxBHf0cPhWHcMOiEFUqPCFQB/JJDQsCZVFA8ZwXAUTOAi51tK3ID0XcsJzVZiaIlI6CyVE8WRKVrLqJAkEAqhNQWJ7S6Cs1oW3AOHstHMiXk1w/dZpnA9Tnhw4HpjqbnnA8auZTq/UvV8wCKtwK74/6AkkQjhjjTvtnVCXdoQJBAJqGrsHzCAiL7h23r+Dpv/E+rG8cYextRYIcGaKgGCD1YNM5lgCDsVTDNa6pjLZqBTCJkGSdEAqKEee5px+qaCkCQHIrF3nC9CoaJMrywRYOF3sKNt9loHWHOg5wtzt0Ccdfy3MKpuJjMlWqQe8Licz/FcaQFMr9PVtk93cHBtIDANo=
使用keytool生成RSA密钥对

keytool口令:生成一个名称为jwt.jks、别名为jwt的RSA密钥证书

keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

注意:如果提示,keytool 工具未找到,则直接到JDK的bin目录下cmd执行即可。
在这里插入图片描述
产生了秘钥文件:
在这里插入图片描述
然后把产生的秘钥文件放到类路径下面,
在这里插入图片描述
KeyPair配置类:
KeyPairConfig.java

package com.atguigu.signcenter.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import java.security.KeyPair;

/**
 * @author: jd
 * @create: 2024-07-30
 */
@Configuration
public class KeyPairConfig {

    @Bean
    public KeyPair keyPair() {
        //从classpath下的证书中获取秘钥对
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    }
}

SecurityUtilTestService服务类

package com.atguigu.signcenter.service;


import java.io.UnsupportedEncodingException;

public interface SecurityUtilTestService {
    public void test() throws UnsupportedEncodingException;
}

SecurityUtilTestServiceImpl.java 服务实现类

package com.atguigu.signcenter.service.serviceImpl;

import com.atguigu.signcenter.service.SecurityUtilTestService;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.UnsupportedEncodingException;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

/**
 *https://blog.csdn.net/weixin_47560078/article/details/118222785
 * 使用keytool生成RSA密钥对
 * keytool口令:生成一个名称为jwt.jks、别名为jwt的RSA密钥证书
 * @author: jd
 * @create: 2024-07-30
 */

@Slf4j
@Service
public class SecurityUtilTestServiceImpl implements SecurityUtilTestService {

    @Autowired
    public KeyPair keyPair;

    public void test() throws UnsupportedEncodingException {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        log.info("公钥信息 => \n {} \n 公钥:{}", publicKey.toString(), encodeBase64(publicKey.getEncoded()));

        RSAPrivateKey priKey = (RSAPrivateKey) keyPair.getPrivate();
        log.info("私钥信息 => \n {} \n 私钥:{}", priKey.toString(), encodeBase64(priKey.getEncoded()));

    }


    public String  encodeBase64(byte[] source) throws UnsupportedEncodingException {
        return  new String(Base64.encodeBase64(source),"UTF-8");
    }


}

测试效果:
在这里插入图片描述

2024-08-02 09:28:00.380  INFO 15240 --- [nio-8025-exec-1] c.a.s.s.s.SecurityUtilTestServiceImpl    : 公钥信息 => 
 Sun RSA public key, 2048 bits
  modulus: 23829896032654266449237055885186070100267258718676980642255177035533633961682767587031637650539693543712242029204450662903733370684503258275060048893522899536661642394654212548304797355318568454907620090143269620466272360481207010741679722568484516052908816917363309070994255480818549791827817786183403586871515551029544108830017390208760682982163893096953077182893148288822722804881185915432409621143786029919505816246686776327742032964509838775989821139201389029065260189892568467570697405413232162062535688873178341418936979958788897096099384096388668260002427751117248616155956906183130250777246881881675374119023
  public exponent: 65537 
 公钥:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvMTmPTwH1lX5wnfQndyU2s0945VNpagEFGDZp+Qsh30377r5tB7baUv4WW4AkDtGzE1Tog8vZStZGnZzxOzcfi65Lfwir7BaXm1BZX2VzPHcj6pi/rzNoQBfCVu4sb2OuOzU2znpQdDJsp4oBZYLOOc201vUyj//2CZXljIFS22OLdFcsyabAe+WVK8eLuWrNeV4wIScP2oGOBED5amvNlojhlrYP//Rq2HZxZJAQWZFKzbqMqcY9x+z74DZK4ZE8ttSfVjMNQ6ZzI76MGIWXzIh83O+eLJv/wC9LpDMdlkGnbRVWUSt14IFLIJiMGIG7PsJIaCH9Ri8qpL+clHIbwIDAQAB
2024-08-02 09:28:00.381  INFO 15240 --- [nio-8025-exec-1] c.a.s.s.s.SecurityUtilTestServiceImpl    : 私钥信息 => 
 sun.security.rsa.RSAPrivateCrtKeyImpl@ffdad0f9 
 私钥:MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8xOY9PAfWVfnCd9Cd3JTazT3jlU2lqAQUYNmn5CyHfTfvuvm0HttpS/hZbgCQO0bMTVOiDy9lK1kadnPE7Nx+Lrkt/CKvsFpebUFlfZXM8dyPqmL+vM2hAF8JW7ixvY647NTbOelB0MmynigFlgs45zbTW9TKP//YJleWMgVLbY4t0VyzJpsB75ZUrx4u5as15XjAhJw/agY4EQPlqa82WiOGWtg//9GrYdnFkkBBZkUrNuoypxj3H7PvgNkrhkTy21J9WMw1DpnMjvowYhZfMiHzc754sm//AL0ukMx2WQadtFVZRK3XggUsgmIwYgbs+wkhoIf1GLyqkv5yUchvAgMBAAECggEAIWFcKXXlGOTJLrdLP68S74IdsJHlUibc2dGqi4LQ0QC2lIDmyRAv0nXpq77FALxKr7P41w6CXgyFTCWoISmVcAbJHjWY2KKByHLffpfvlncqfoktykgOwKq1I42BooSXqWHP3hhmhAnssNZA04QN5fkU+9kgTsd0cX+OO3QK9aRTIOrogcdor1a+uytnpNQKBRA6W+e7MJo67HFWn1DkacKdYaty+jI4B1+1eSuHQoiGSR92Is0ITnu1ToVZq8ljknI87sUeSr2745ZNa9dvCixnqu3MDo8MpriIbvIs/ou/EIkOkWIqxk3ZcJlnSLAp3KExzZrEn6oNgnt0f5o1SQKBgQDeCvijwzUfgPFT6+BmqcG7aizd8c9muwIJq5SZ092WbkeYLJ1MTE/M/FdY+gf60+RGerIoqs/MS5Wq22Wkm2zoNExlx1J74j3T/hLJi83k2sPcLZ8ERylF2gxHCC0mSq8XYBP95JnhP3GhJAb0AfZAX33aNlTmhI+x0dGK/E3LEwKBgQDZo0EhzhTQ6LfKgdq856V0BhPCrUMAiclvgww9hZTKJizDDhYRmozplqMxJDsoYM+UIrT4+HOgn1hhyVyQrL+UuU4avwlFvttRV2qP5DN23cCmO2fCEl1VZsdfYwM6Wxi5W3HtrfPCOTI3UDCBBaIVIY5ysgaYN/K3d7GApbx8tQKBgQC6Fr3NGZPTBFC8wam+wLSxqklR9Q+aDE6n5hnTVgGWynPMME/gGvCiXjWiR2IasCzXeIsJlCY1FH+pwtgLvYrnjLUec49IbhLdMUlzimyTSVjulXl4r18CwLybZ1nXhq35TDdVCJqCcZZ8s2H0bBSCXMVv9SrwStbW837HGa7k6wKBgDEfL+w5X2lnnVQPyxcoEagGVx9xi9XrQ5PvuCuKovKdeyzrWP/QKk3uuha1XsTWVQPScZZgPXjbfk9T65ib5QHkcUSIKcecNFQ/rsWbdCI4GutqLRkYnAhSkl1tM6VQOlxbz3Md+A62aich6lC0vMiYMlag5+wjdJ0EXwPyZIChAoGBALY2A7hlCaI8tbg2QVnbMBBmLCUWneEV3wVVehDSMA3Dwq6jD/MrCHHSdFIk0pOsH+YkKyifrhjS58M/irrwv1fsbs/NHdjngBHYGswP10cYyCNSI9LvsN47/pVxDPr/9nUfSB2wNWUTaRLflPpokT8tmMc4AIkAmri6+VWXnlgO

进行实际的http请求的签名模块模拟签名与验证

模块pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>sign-center</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sign-center</name>
    <description>api接口的安全设计</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>1.8</java.version>
        <!--        <spring-cloud.version>2021.0.4</spring-cloud.version>-->
        <spring-cloud.version>2021.0.1</spring-cloud.version>
    </properties>
    <dependencies>
        <!--        redisson依赖-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.15.5</version>
        </dependency>
        <!--redis链接客户端-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <!--集成redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.7.RELEASE</version>
        </dependency>

        <!--joda time  ? 这个还有些问题,这个类库是做什么的-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>com.atguigu.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <artifactId>servlet-api</artifactId>
                    <groupId>javax.servlet</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--什么作用? -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.4.2</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

在这里插入图片描述

主类:

package com.atguigu.signcenter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class SignCenterApplication {

    public static void main(String[] args) {
        SpringApplication.run(SignCenterApplication.class, args);
    }

}

封装的安全工具类:SecurityUtil

package com.atguigu.signcenter.util;

import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * 描述:安全工具类
 * ————————————————
 * <p>
 * 原文链接:https://blog.csdn.net/weixin_47560078/article/details/118222785
 *
 * @author: jd
 * @create: 2024-07-31
 */
public class SecurityUtil {

    //加密算法
    private static final String ALGORITHM_RSA = "RSA";
    //字符编码指定的字符集
    private static final String RSA_CHARSET = "UTF-8";

    /**
     * 描述:将字符串通过RSA算法公钥加密
     *
     * @param content 需要加密的内容
     * @param pubKey  公钥
     * @return 加密后字符串
     * @throws Exception
     */
    private static String EncryptByRSAPubKey(String content, String pubKey) throws Exception {
        try {
            PublicKey publicKey = SecurityUtil.getRSAPubKey(pubKey);
            Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            cipher.update(content.getBytes(RSA_CHARSET));
            return SecurityUtil.encodeBase64(cipher.doFinal());
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }

    }

    /**
     * 描述:将字符串通过RSA算法公钥解密
     *
     * @param content 需要解密的内容
     * @param pubKey  公钥
     * @return 解密后字符串
     * @throws Exception
     */
    public static String DecryptByRSAPubKey(String content, String pubKey) throws Exception {
        try {
            PublicKey publicKey = SecurityUtil.getRSAPubKey(pubKey);
            Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            cipher.update(SecurityUtil.decodeBase64(content));
            return new String(cipher.doFinal(), RSA_CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }
    }


    /**
     * 描述:将字符串通过RSA算法私钥加密
     *
     * @param content 需要加密的内容
     * @param priKey  私钥
     * @return 加密后字符串
     * @throws Exception
     */
    public static String EncryptByRSAPriKey(String content, String priKey) throws Exception {
        try {
            PrivateKey privateKey = SecurityUtil.getRSAPriKey(priKey);
            Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);
            cipher.update(content.getBytes(RSA_CHARSET));
            return SecurityUtil.encodeBase64(cipher.doFinal());
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();
        }

    }


    /**
     * 描述:将字符串通过RSA算法私钥解密
     *
     * @param content 需要解密的内容
     * @param priKey  私钥
     * @return 解密后字符串
     * @throws Exception
     */
    public static String DecryptByRSAPriKey(String content, String priKey) throws Exception {
        try {
            PrivateKey privateKey = SecurityUtil.getRSAPriKey(priKey);
            Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            cipher.update(SecurityUtil.decodeBase64(content));
            return new String(cipher.doFinal(), RSA_CHARSET);

        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();

        }

    }


    /**
     * 从秘钥文件中获取密钥对
     *
     * @return
     */
    private static KeyPair getKeyPair(){
        //从classpath下的证书中获取秘钥对
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    }


    /**
     * 从jwt.jks文件中获取公钥字符串
     *
     * @return
     * @throws Exception
     */
    public static String getPublicKey() throws Exception {
        // 获取密钥对
        KeyPair keyPair = getKeyPair();
        // 获取私钥信息
        PublicKey publicKey  = keyPair.getPublic();
        //byte 转 String
        return encodeBase64(publicKey.getEncoded());
    }

    /**
     * 从jwt.jks文件中获取私钥字符串
     *
     * @return
     * @throws Exception
     */
    public static String getPrivateKey() throws Exception {
        // 获取密钥对
        KeyPair keyPair = SecurityUtil.getKeyPair();
        // 获取私钥信息
        PrivateKey privateKey = keyPair.getPrivate();
        // byte 转 String
        return SecurityUtil.encodeBase64(privateKey.getEncoded());
    }




    /**
     * 描述:获取RSA私钥
     *
     * @param priKey 私钥
     * @return PrivateKey
     * @throws Exception
     */
    private static PrivateKey getRSAPriKey(String priKey) throws Exception {
        try {
            PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(SecurityUtil.decodeBase64(priKey));
            KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
            return keyFactory.generatePrivate(privateKeySpec);

        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();

        }
    }


    /**
     * 描述:获取RSA公钥
     *
     * @param pubKey 公钥
     * @return
     */
    private static PublicKey getRSAPubKey(String pubKey) throws Exception {
        try {
            X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(SecurityUtil.decodeBase64(pubKey));
            KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
            return keyFactory.generatePublic(publicKeySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }



    /**
     * base64编码
     *
     * @param source
     * @return
     * @throws Exception
     */
    public static String encodeBase64(byte[] source) throws Exception {
        return new String(Base64.encodeBase64(source), RSA_CHARSET);

    }


    /**
     * Base64解码
     *
     * @param target 需要解码的目标字符串
     * @return
     */
    private static byte[] decodeBase64(String target) throws Exception {
        return Base64.decodeBase64(target.getBytes(RSA_CHARSET));
    }


    public static void main(String[] args) throws Exception {
        //获取公钥
        String pubKey = getPublicKey();
        //获取私钥
        String priKey = getPrivateKey();
        //需要加密的参数
        String content = "age=18&name=yushanma";
        //字符串加密
        String s = EncryptByRSAPubKey(content, pubKey);
        System.out.println("s参数通过公钥加密后:" + s);
        System.out.println("加密后的字符串通过私钥解密后:" + DecryptByRSAPriKey(s, priKey));


        content = "age=18&name=yushanma";
        s = EncryptByRSAPriKey(content, priKey);
        System.out.println("s参数通过私钥加密后:" + s);
        System.out.println("加密后的字符串通过公钥解密后:" + DecryptByRSAPubKey(s, pubKey));

        //有个疑问,是私钥和公钥都可以用来加密 吗,私钥和公钥是什么关系??
        //TODO

    }


}

main方法


    public static void main(String[] args) throws Exception {
        //获取公钥
        String pubKey = getPublicKey();
        //获取私钥
        String priKey = getPrivateKey();
        //需要加密的参数
        String content = "age=18&name=yushanma";
        //字符串加密
        String s = EncryptByRSAPubKey(content, pubKey);
        System.out.println("s参数通过公钥加密后:" + s);
        System.out.println("加密后的字符串通过私钥解密后:" + DecryptByRSAPriKey(s, priKey));


        content = "age=18&name=yushanma";
        s = EncryptByRSAPriKey(content, priKey);
        System.out.println("s参数通过私钥加密后:" + s);
        System.out.println("加密后的字符串通过公钥解密后:" + DecryptByRSAPubKey(s, pubKey));

        //有个疑问,是私钥和公钥都可以用来加密 吗,私钥和公钥是什么关系??
        //TODO

    }

测试结果
在这里插入图片描述

s参数通过公钥加密后:RT9yWv4CL3LtG2RYKxV35KWkFNHA/tCFQEp+En0AjFJYelscLYupJL+1Fssblp0OC/HGUqd0bx5Ji3OdZln5wI8HDMChfAxj5k4pb9NfAlXPvCKYZynFL4n/gnxLijJCWTTC0j+XazYrn8qxMmPUvh8uqwIhTGnm5F1nNlLFoxOzaGyWz3wkjy36RVFr609g1mHWRu1PoqLSWb6+qyiWhL0qr4tCNYsgx1Oe+JVptSJ67OW747o+Vk+oAzs22LKlSv+1gMtGvSqYLh2rxdk+vdN36PeSmQgsuW3dRKdpQEsyfR7jmmZnQ99Zjog5Hc6CKQjlYFQjgZjnGpOM2kTGDw==
加密后的字符串通过私钥解密后:age=18&name=yushanma
s参数通过私钥加密后:YOW/GiySigBsY4RyM/Q25Qyg3ypohvelONxHuSd+8NbRoRu9mBxVOgCq4MXEKOqf92mBcuAEdIxq4J8ARwZz0ba7846bQnpuRiLxpndZXs/ViZVH/gC93s5GpkmOhsx1nM4kNMap39FNASSn04Gxs8MJkYZLibZ4HKCaq1IZrolIawzW1Stm2nKn+B3CHpyYvMxKCDn7WdOtJaAdDf9MLOjvbZDM3GmIkQAoNctWrExjLoIsF689lAQuDRoIPLNNsYMtD4Zrx4KqPFAWnLsAMI17Mc6igQxNejiGNOw4S2WjZew44Mo+EKxutQCR5mFUVAXs94fsFH7AVwtJhuHW3A==
加密后的字符串通过公钥解密后:age=18&name=yushanma

封装签名工具类: ApiUtil

package com.atguigu.signcenter.util;

import com.alibaba.fastjson.JSONObject;
import com.mysql.cj.util.StringUtils;
import com.rabbitmq.tools.json.JSONUtil;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.DigestException;
import java.util.*;

/**
 * 封装签名工具类
 * https://blog.csdn.net/weixin_47560078/article/details/118222785
 * @author: jd
 * @create: 2024-07-31
 */
@Slf4j
public class ApiUtil {


    /**
     * 根据传的参数Map,对参数进行排序并拼接成A=1&b=2....
     * @param data
     * @return
     */
    private static String getSortedContent(Map<String, String> data){
        StringBuffer content = new StringBuffer();
        List<String> keys = new ArrayList<>(data.keySet());
        Collections.sort(keys);
        int index = 0;
        for (String key : keys) {
            String value = data.get(key);
            content.append((index == 0 ? "" : "&")).append(key).append("=").append(value);
            index++;
        }
        return content.toString();

    }

    /**
     * 参数加密,吸纳使用MD5加密,然后使用自定义的私钥加密方法进行加密,最终进行URL编码,并返回。
     * @param data
     * @return
     * @throws Exception
     */
    public static String getSignature(Map<String, String> data) throws Exception {
        // 第一次加密:使用MD5加密   对排序好的字符串进行加密,然后转小写
        String summary  = DigestUtils.md5Hex(getSortedContent(data));
        log.info("======>md5加密后的参数 summary:" + summary);
        //第二次加密,使用自定义的加密方法加密[自定义的加密方法,通过私钥加密]
        String encryptByRSAPriSummary = SecurityUtil.EncryptByRSAPriKey(summary, SecurityUtil.getPrivateKey());
        //对加密后的字符串进行Url编码,并返回
        return URLEncoder.encode(encryptByRSAPriSummary,"utf-8");
    }


    /**
     *  验证签名
     * @param params  所有参数
     * @param sign 数字签名
     * @param signType 加密类型
     * @return
     * @throws Exception
     */
    public static boolean verifySign(Map<String, String> params, String sign, String signType) throws Exception {
        //如果签名直接为空,则返回验证失败
        if(StringUtil.isEmpty(URLDecoder.decode(sign, "utf-8"))){
            return false;
        }
        //暂不支持非RSA的签名
        if(StringUtil.isEmpty(signType)&&!"RSA".equals(signType)){
            log.info("暂不支持其他加密方法的签名验证");
            return false;
        }
        //走过参数校验之后,开始正式的签名校验
        //参与签名的数据
        String data = getSortedContent(params);
        log.info("=====>原始data:" + data);
        String summary  = DigestUtils.md5Hex(data).toLowerCase();
        log.info("=====>data进行md5Hex加密后 summary:" + summary);

        String summaryDecode = null;
        try {
            //对签名通过公钥解密,因为加密的时候先加密后编码,所以这里需要先解码,后解密,又因为使用自定义加密方法之前,是先经过了MD5加密,所以对比的时候,也要对原始串进行下MD5,
            //(紧接着上一行)然后再和解密的字符串去对比。
            summaryDecode = SecurityUtil.DecryptByRSAPubKey(URLDecoder.decode(sign,"utf-8"),SecurityUtil.getPublicKey());
        } catch (Exception e) {
            throw new RuntimeException("do_digest_error", e);
        }

        return summary.equals(summaryDecode);

    }

    public static void main(String[] args) throws Exception {
        Map<String,String> data = new HashMap<>();
        data.put("name","zhaijh");
        data.put("age","20");
        log.info("原始参数:{}",data);
        String signature = getSignature(data);
        log.info("参数签名:{}",signature);
        String signType = "RSA";
        log.info("验证结果:{}",verifySign(data,signature,signType));

    }

}

测试结果
在这里插入图片描述

09:01:07.417 [main] INFO com.atguigu.signcenter.util.ApiUtil - 原始参数:{name=zhaijh, age=20}
09:01:07.432 [main] INFO com.atguigu.signcenter.util.ApiUtil - ======>md5加密后的参数 summary:bea0310dc7f03ad5d036d906dbe513f4
09:01:08.642 [main] INFO com.atguigu.signcenter.util.ApiUtil - 参数签名:j0CTBZ0NpiZFycua7otGYXr7KHNgXsBc0DBojHzyKbTKtSrutZv%2FblSFqFi5lCM6stSkxWd8OQQRcgPwPopsy1HYZ6cU%2Fi%2Bpwc0JAH2DH49jmTGgJVatCGFZ0NtcNK972XIncpSOaeWNx6fvBffi71jbxg7BdWM89anLL%2BQJ1GeF5i3OL4wBzFoyC7FdiGdxG9qShSdORBcy2LMyCa3sHJDTayoPmOhNLAqgvrAWeTfF%2BkUZAPqysHVGX8KpmfcNY0vf1y%2BSmWXYJ5x%2Faw1GVCIy39vcXppqy1ofqZQTo0d6drk62J1cjmDzl0Pj7c4J0h13mCw4yl4dn3KRkJ3jhg%3D%3D
09:01:08.644 [main] INFO com.atguigu.signcenter.util.ApiUtil - =====>原始data:age=20&name=zhaijh
09:01:08.644 [main] INFO com.atguigu.signcenter.util.ApiUtil - =====>data进行md5Hex加密后 summary:bea0310dc7f03ad5d036d906dbe513f4
09:01:08.645 [main] INFO com.atguigu.signcenter.util.ApiUtil - 验证结果:true

控制器签名接口与校验接口: SignController

package com.atguigu.signcenter.controller;


import com.atguigu.signcenter.util.ApiUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * 控制器签名接口与校验接口:
 * @author: jd
 * @create: 2024-08-01
 */
@Slf4j
@RestController
public class SignController {



    /**
     * 模拟前端请求时发送的 参数签名
     * @param data 实际的业务入参数据
     * @return
     */
    @GetMapping("/sign/getSign")
    public Map<String,Object> getSign(@RequestParam Map<String, String> data) throws Exception {
        log.info("入参:{}",data);
        //返回信息,map结构
        HashMap<String, Object> result  = new HashMap<>();
        result.put("code",0); //状态码
        result.put("msg","success"); //返回信息
        String signature = ApiUtil.getSignature(data);//获得入参参  通过私钥的签名内容
        result.put("data",signature);
        result.put("verify",ApiUtil.verifySign(data,signature,"RSA"));
        return result;
    }

    /**
     * 模拟后端进行请求中“参数签名”的验证
     * @param request Http请求
     * @param data  实际的业务入参数据
     * @return
     * @throws Exception
     */
    @GetMapping("/sign/verifySign")
    public boolean verifySign(HttpServletRequest request,@RequestParam Map<String, String> data) throws Exception {
        String sign = request.getHeader("sign");
        String sign_type = request.getHeader("sign_type");
        boolean result = ApiUtil.verifySign(data, sign, sign_type);
        log.info("参数签名验证结果:{}",result);
        return  result;

    }

}

yml相关配置:

server:
  port: 8025

spring:
  application:
    name: sign-center
  datasource:
    url: jdbc:mysql://192.168.56.10:3306/gulimall_ums
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

#配置日志输出级别
logging:
  level:
    com.atguigu.gulimall: debug   #level 日志等级 指定命名空间的日志输出

模拟前端产生签名测试: 参数name zhaijh ; age 21
在这里插入图片描述

模拟后端签名校验测试:
请求头
在这里插入图片描述
请求参数
在这里插入图片描述

这里我将sign放到header中,是因为签名signature中有些字符是特殊字符,放到parameter中可能会导致有些字符被过滤,最终在校验签名时抛出签名长度不够的错误,这个问题可以通过url编码解决:

当传的参数是一样的话,验证通过 返回true 【参数name zhaijh ; age 21】
在这里插入图片描述

当参数被修改时,签名验证就会返回false: 【参数name zhaijh ; age 22】
在这里插入图片描述

参考链接:https://blog.csdn.net/weixin_47560078/article/details/118222785

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值