为什么要设计安全的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