目录
一、背景
1.1、RSA算法
1.2、HTTPS
1.2.1、 HTTPS优点
1.2.2、 HTTPS缺点
二、目标
2.1、实现如下示例加签规则
2.2、具体密钥生成方式步骤
第一步:生成私钥命令
第二步:根据私钥生成对应公钥pem文件
第三步:将私钥转换成pkcs8格式
三、准备(order作为A企业服务,product作为B企业服务)
四、代码展示
4.1、order服务
5.1、product服务
五、测试验证
六、源码地址
一、背景
对于程序项目来说,企业间业务对接,少不了http api接口公网对接。而http接口公网对接就必须做到接口安全认证,防止接口或数据被拦截窃取,破解泄露商业信息,甚至黑客攻击。此时就必须做安全措施,如加白名单、数字安全认证证书(https)等。其中,RSA非对称加密进行加签和验证是常用的一种。RSA公钥加密算法是1977年由Ron Rivest、Adi Shamirh和LenAdleman在(美国麻省理工学院)开发的。RSA取名来自开发他们三者的名字。RSA是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知的所有密码攻击,已被ISO推荐为公钥数据加密标准。RSA算法详细请看密码学:RSA加密算法详解_大鱼-CSDN博客_rsa加密算法。
这里大致认识下RSA算法和数字安全认证https:
1.1、RSA算法
-
RSA是目前最有影响力和最常用的公钥加密算法,它能够抵抗到目前为止已知的绝大多数密码攻击,已被ISO推荐为公钥数据加密标准。
-
今天只有短的RSA钥匙才可能被强力方式破解。但在分布式计算和量子计算机理论日趋成熟的今天,RSA加密安全性收到了挑战和质疑。
-
RSA算法基于一个十分简单的数论事实:将两个大质数相乘十分容易,但是想要对其乘积进行因式分解缺及其困难,因此可以将乘积公开作为加密密钥。
-
可以自己实现,无需购买,算法公开。
1.2、HTTPS
1.2.1、 HTTPS优点
-
使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器。
-
HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
-
HTTPS是现行框架下最安全的解决方案,虽然不是觉得安全,但它增加了中间人攻击的成本。
1.2.2、 HTTPS缺点
-
SSL的专业证书需要购买,功能越强大的证书费用越高
-
相同的网络环境下,HTTPS协议会使页面的加载时间延长50%,增加10%-20%的耗电。此外,HTTPS协议还会影响缓存,增加数据开销和功耗。
-
HTTPS协议的安全性是有范围的,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。
-
最关键的是,SSL证书的信用链体系并不安全。特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。
二、目标
2.1、实现如下示例加签规则
- 将参数列表中除了sign的字段按照key升序排列,类似get的方式,用”=”和”&”拼接成字符串。
- 将编码得到的字符串使用私钥加密,密文字符串进行base64编码,得到的结果就是sign的值。
- 加密采用非对称RSA密钥对,密钥位数1024位。
-
最后以对象的序列化后的json字符串传输。
RSA是目前最有影响力和最常用的公钥加密算法,它能够抵抗到目前为止已知的绝大多数密码攻击,已被ISO推荐为公钥数据加密标准。
今天只有短的RSA钥匙才可能被强力方式破解。但在分布式计算和量子计算机理论日趋成熟的今天,RSA加密安全性收到了挑战和质疑。
RSA算法基于一个十分简单的数论事实:将两个大质数相乘十分容易,但是想要对其乘积进行因式分解缺及其困难,因此可以将乘积公开作为加密密钥。
可以自己实现,无需购买,算法公开。
1.2、HTTPS
1.2.1、 HTTPS优点
-
使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器。
-
HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
-
HTTPS是现行框架下最安全的解决方案,虽然不是觉得安全,但它增加了中间人攻击的成本。
1.2.2、 HTTPS缺点
-
SSL的专业证书需要购买,功能越强大的证书费用越高
-
相同的网络环境下,HTTPS协议会使页面的加载时间延长50%,增加10%-20%的耗电。此外,HTTPS协议还会影响缓存,增加数据开销和功耗。
-
HTTPS协议的安全性是有范围的,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。
-
最关键的是,SSL证书的信用链体系并不安全。特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。
二、目标
2.1、实现如下示例加签规则
- 将参数列表中除了sign的字段按照key升序排列,类似get的方式,用”=”和”&”拼接成字符串。
- 将编码得到的字符串使用私钥加密,密文字符串进行base64编码,得到的结果就是sign的值。
- 加密采用非对称RSA密钥对,密钥位数1024位。
-
最后以对象的序列化后的json字符串传输。
使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器。
HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
HTTPS是现行框架下最安全的解决方案,虽然不是觉得安全,但它增加了中间人攻击的成本。
1.2.2、 HTTPS缺点
-
SSL的专业证书需要购买,功能越强大的证书费用越高
-
相同的网络环境下,HTTPS协议会使页面的加载时间延长50%,增加10%-20%的耗电。此外,HTTPS协议还会影响缓存,增加数据开销和功耗。
-
HTTPS协议的安全性是有范围的,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。
-
最关键的是,SSL证书的信用链体系并不安全。特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。
二、目标
2.1、实现如下示例加签规则
- 将参数列表中除了sign的字段按照key升序排列,类似get的方式,用”=”和”&”拼接成字符串。
- 将编码得到的字符串使用私钥加密,密文字符串进行base64编码,得到的结果就是sign的值。
- 加密采用非对称RSA密钥对,密钥位数1024位。
-
最后以对象的序列化后的json字符串传输。
- 将参数列表中除了sign的字段按照key升序排列,类似get的方式,用”=”和”&”拼接成字符串。
- 将编码得到的字符串使用私钥加密,密文字符串进行base64编码,得到的结果就是sign的值。
- 加密采用非对称RSA密钥对,密钥位数1024位。
-
最后以对象的序列化后的json字符串传输。
交互流程图,如下:
描述:A企业、B企业先生成公私钥,然后互相交换公钥。调用方调用接口前,使用自己的私钥加密; 被调用方接收数据前使用调用方给的公钥解密,解密成功允许调用接口逻辑处理返回数据;解密失败(鉴权失败)不允许调用接口。
2.2、具体密钥生成方式步骤
第一步:生成私钥命令
如:openssl genrsa -out rsa_private_key.pem 1024
命令格式:openssl genras -out 私钥文件名 1024
实际操作如下(这里使用git bash界面):
生成一个由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾的rsa_private_key.pem文件,这就是私钥文件。
第二步:根据私钥生成对应公钥pem文件
如:openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
命令格式:openssl rsa -in私钥文件名 -pubout -out 公钥文件名
实际操作如下:
生成一个由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾的rsa_public_key.pem文件,这就是公钥文件。
第三步:将私钥转换成pkcs8格式
如:openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt > rsa_private_key_pkcs8.pem
命令格式:openssl pkcs8 -topk8 -inform PEM -in 私钥文件名 -outform PEM -nocrypt > pkcs8格式私钥文件名
实际操作如下:
生成一个由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾的rsa_private_key_pkcs8.pem文件,这就是适配java语言开发的私钥文件(第一步生成的私钥是pkcs1格式的文件,像php可以直接使用。但java使用就必须转换成pkcs8格式的文件内容)。
描述:可以发现第三步和第一步都是私钥,他们都是由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾,密钥内容却不相同。在java代码里,我们读取的密钥体是不包含开头和结尾的,因此我们把第二部和第三步的pem文件去掉开头结尾重新存储下。
三、准备(order作为A企业服务,product作为B企业服务)
- 服务order实现一个查询商品接口,商品接口由服务product以http接口形式提供,并实现一个消息转换器,做加密认证操作(基于目前大部分服务都是高可用分布式微服务,所以本次order服务调用product服务接口使用springcloud的feignClient接口实现。注意,这里feignclient不用eureka服务,而是通过配置url直接调用product服务)。
- 服务product实现一个基于spring MVC框架实现Http接口,并实现一个切面拦截被调用接口的请求做解密认证。
- 环境:jdk1.8。
四、代码展示
4.1、order服务
4.1、order服务
pom.xml配置如下:
4.0.0 org.springframework.boot spring-boot-starter-parent2.5.5 com.example order0.0.1-SNAPSHOT order Demo project for Spring Boot 1.8 2020.0.4 org.springframework.boot spring-boot-starter-weborg.springframework.cloud spring-cloud-starterorg.springframework.cloud spring-cloud-starter-openfeignorg.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest com.alibaba fastjson1.2.78 org.springframework.cloud spring-cloud-dependencies${spring-cloud.version} pom import org.springframework.boot spring-boot-maven-pluginorg.projectlombok lombok
application.yml配置文件配置:
注意:这里的rsa.010.private-key就是私钥文件rsa_private_key_pkcs8.pem去掉头尾的内容。
spring:
application:
name: order
server:
port: 8081
servlet:
context-path: /order
#不使用eureka服务
eureka:
client:
enabled: false
#私钥前缀需要取私钥的key保持一致
rsa.010.private-key : MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAONKrJ8MQQlDAye/
sa8xcBauSmlOSlXH8KuBWheS7anovJSlhtPOIqSUuroT0xMcHsiSqYFAp8t2/k3r
vwWCXx2HwHPtw240DIQ5IBKSq743GdXFAOFXdZh1epf+NPtpIeYoF+aXlgwplqSG
iTdA8WnRQ5OPS0KZUdbK9e9jUodPAgMBAAECgYANBPgCXEdVantByZ8589EB25Xz
lkJ3y24jxNMOSqJGe0hiE2E3vLULTGGtyvjqPVAeGRiQiM2TwAstF3XnsOIVyUxF
HY60AXtMzlYkBrsyyIGF7FrVBuWaTbRYPE8EFOVMVZy/nziQE/bZKVYLHufqqob7
RZtzMMd9CI8bbuKK4QJBAPmVezMgTI+mdFWANUL27DM9tAJllN+T9bPKTP443xbd
JDEoKUzx3tktTnQXqQmUIrNuBZTDi5SN29bj3E+ZiokCQQDpInzDprUQmgGX3VnG
JNPx1fcUQF7DQsxm8k8MCbkJetHcIW/TShKL0Dt2viyiW6uapzJJLTxBAK+HFk2W
hS0XAkBVohoxQoXCS+RiaajcnwgP1L3sjJn11DhbRbABEdZJa/q8+wCgq+RAM7FV
V8DhznfRhJBZqHY9tCaXpnqyvQWxAkEAyqb33PqkmfHFQMVgrCSHN8jOJgRuWz1N
gI9Qtx4cgmkI01kdY4UX6gDwL5/QHLGi0aRUyddQcRCvg7WXbCgHsQJBAJMen29/
aQpJC3gOTPjQJowuYRuCLar6YGj3YcPR1DrciNqz7xiFoTtgPJfQLerx+HCFJ0dW
Yk6YY4z7Xsu5utg=
controller实现:
package com.example.order.controller;
import com.example.order.service.ProductService;
import com.example.order.vo.ProductResponse;
import com.example.order.vo.Response;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class ProductController {
@Resource
private ProductService productService;
@RequestMapping(value = "/query/{id}")
public Response queryById(@PathVariable Integer id){
return Response.success(productService.queryById(id));
}
}
service实现:
package com.example.order.service;
import com.example.order.feign.ProductMicroServer;
import com.example.order.vo.ProductRequest;
import com.example.order.vo.ProductResponse;
import com.example.order.vo.Response;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
@Service
public class ProductService {
@Resource
private ProductMicroServer productMicroServer;
public ProductResponse queryById(Integer id){
ProductRequest productRequest = new ProductRequest();
productRequest.setId(id);
productRequest.setAppId("010");
Response responseResponse = productMicroServer.selectByCondition(productRequest);
if(Objects.nonNull(responseResponse)){
return responseResponse.getData();
}
return null;
}
}
feignclient接口实现:
package com.example.order.feign;
import com.example.order.vo.ProductRequest;
import com.example.order.vo.ProductResponse;
import com.example.order.vo.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
@FeignClient(name = "product", url="http://localhost:8082/product", fallbackFactory = ProductMicroServerFallbackFactory.class)
public interface ProductMicroServer {
@PostMapping(value = "/selectByCondition", consumes = MediaType.APPLICATION_JSON_VALUE)
Response selectByCondition(ProductRequest request);
}
package com.example.order.feign;
import com.example.order.vo.ProductRequest;
import com.example.order.vo.ProductResponse;
import com.example.order.vo.Response;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class ProductMicroServerFallback implements ProductMicroServer{
@Override
public Response selectByCondition(ProductRequest request) {
return Response.success(new ProductResponse(0, "棒棒糖(兜底商品)", 1, new BigDecimal(0.5)));
}
}
package com.example.order.feign; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Slf4j @Service public class ProductMicroServerFallbackFactory implements FallbackFactory{ @Resource private ProductMicroServerFallback productMicroServerFallback; @Override public ProductMicroServer create(Throwable cause) { log.error("ProductMicroServerFallback->selectById(Integer id) exception:", cause); return productMicroServerFallback; } }
自定义转换器的实现(继承org.springframework.http.converter.AbstractHttpMessageConverter抽象类):
package com.example.order.config; import com.alibaba.fastjson.JSON; import com.example.order.common.RsaUtils; import com.example.order.service.GlobalValuesService; import com.example.order.vo.baseRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; import javax.annotation.Resource; import java.io.IOException; import java.util.TreeMap; import java.util.UUID; @Slf4j @Component public class HttpGlobalOutMessageConverterextends AbstractHttpMessageConverter { private static final String QUOTE_MARK = """; private String INVOKER_TRACE_ID = "invoke_traceId"; @Resource private GlobalValuesService globalValuesService; public HttpGlobalOutMessageConverter() { //支持的两种媒体类型 super(MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON); } @Override protected boolean supports(Class> clazz) { //表示只支持baseRequest这个类(包括子类) return baseRequest.class.isAssignableFrom(clazz); } @Override protected T readInternal(Class extends T> clazz, HttpInputMessage inputMessage) { throw new RuntimeException("暂不支持"); } @Override protected void writeInternal(T t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //将请求参数组装成map格式 baseRequest request = t; request.setTimestamp(String.valueOf(System.currentTimeMillis())); TreeMap map = JSON.parseObject(JSON.toJSonString(request), TreeMap.class); //参数Map 转成 字符串(使用&符号key=value的形式拼接) String requestString = requestString(map); //4.签名处理 String sign = RsaUtils.signatureByPrivateKey(requestString, globalValuesService.privateKey(request.getAppId())); map.put("sign", sign); String parameters = JSON.toJSonString(map); //2.trace参数 String headerRid = UUID.randomUUID().toString().replaceAll("-", ""); //5.写入body byte[] bytes = parameters.getBytes(); outputMessage.getHeaders().setContentLength(bytes.length); outputMessage.getHeaders().add(INVOKER_TRACE_ID, headerRid); StreamUtils.copy(bytes, outputMessage.getBody()); log.info("traceId:{}, parameters:{}", headerRid, parameters); } private static String requestString(TreeMap requestMap) { StringBuilder requestStringBuilder = new StringBuilder(); requestMap.forEach((property, value) -> { requestStringBuilder.append(property).append("="); if (value != null) { String string = JSON.toJSonString(value); if (string.startsWith(QUOTE_MARK) && string.endsWith(QUOTE_MARK)) { string = string.substring(1, string.length() - 1); } //去掉多次转义 string = string.replaceAll("\\", ""); requestStringBuilder.append(string); } requestStringBuilder.append("&"); }); if (requestStringBuilder.length() > 0) { requestStringBuilder.deleteCharAt(requestStringBuilder.length() - 1); } return requestStringBuilder.toString(); } }
使用到的工具类:
package com.example.order.common;
import lombok.extern.slf4j.Slf4j;
import java.util.base64;
@Slf4j
public class base64Utils {
@SuppressWarnings("restriction")
public static String encode(byte[] bytes) {
return new String(base64.getEncoder().encode(bytes)).replaceAll("[rn]", "");
}
@SuppressWarnings("restriction")
public static byte[] decode(String str) {
return base64.getDecoder().decode(str);
}
}
package com.example.product.common;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.ResourceUtils;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
@Slf4j
public class RsaUtils {
private static String handleKey(String key) {
//1. 去开头结尾符
key = key.replaceAll("--.*--", "");
//2. 去除换行
key = key.replaceAll("[rn]", "");
//3. 去空格
key = key.replaceAll(" ", "");
return key;
}
//#################### 私钥:签名
public static String signatureByPrivateKey(String data, String privateKey) {
if (StringUtils.isBlank(privateKey)) {
log.warn("私钥不可为空");
return "";
}
privateKey = handleKey(privateKey);
try {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(base64Utils.decode(privateKey));
RSAPrivateKey key = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec);
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(key);
signature.update(data.getBytes());
return base64Utils.encode(signature.sign());
} catch (Exception e) {
log.warn("私钥加密失败,data:[{},privateKey:[{}],exception:", data, privateKey,e);
return "";
}
}
//#################### 公钥:验签
public static boolean verifyByPublicKey(String data, String publicKey, String sign) {
if (StringUtils.isBlank(publicKey)) {
log.warn("公钥钥不可为空");
return false;
}
publicKey = handleKey(publicKey);
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(base64Utils.decode(publicKey));
RSAPublicKey rsaPubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initVerify(rsaPubKey);
signature.update(data.getBytes());
return signature.verify(base64Utils.decode(sign));
} catch (Exception e) {
log.warn("公钥解密失败,sign:[{},publicKey:[{}],exception:", sign, publicKey,e);
return false;
}
}
}
package com.example.order.service;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class GlobalValuesService {
@Resource
private Environment environment;
public String privateKey(String appId) {
return environment.getProperty(String.format("rsa.%s.private-key", appId));
}
}
package com.example.order.vo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class baseRequest {
private String appId;
private String timestamp;
}
package com.example.order.vo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ProductRequest extends baseRequest {
private Integer id;
}
package com.example.order.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
public class ProductResponse {
private Integer id;
private String name;
private Integer num;
private BigDecimal price;
}
package com.example.order.vo; import lombok.Getter; import lombok.Setter; @Setter @Getter public class Response{ private Integer errorCode; private String errorMsg; private T data; public Response(Integer errorCode, String errorMsg, T data) { this.errorCode = errorCode; this.errorMsg = errorMsg; this.data = data; } public static Response success(T data){ return new Response<>(null, null, data); } }
启动类:
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients(value = "com.example.order.feign")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
项目结构:
5.1、product服务
pom.xml配置:
4.0.0 org.springframework.boot spring-boot-starter-parent2.5.5 com.example product0.0.1-SNAPSHOT product Demo project for Spring Boot 1.8 2020.0.4 org.springframework.boot spring-boot-starter-weborg.springframework.cloud spring-cloud-starterorg.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest com.alibaba fastjson1.2.78 org.springframework.boot spring-boot-starter-aoporg.springframework.cloud spring-cloud-dependencies${spring-cloud.version} pom import org.springframework.boot spring-boot-maven-pluginorg.projectlombok lombok
application.yml配置:
注意:这里的rsa.010.public-key就是公钥文件rsa_public_key.pem去掉头尾的内容。
spring:
application:
name: product
server:
port: 8082
servlet:
context-path: /product
#不使用eureka服务
eureka:
client:
enabled: false
rsa.010.public-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjSqyfDEEJQwMnv7GvMXAWrkpp
TkpVx/CrgVoXku2p6LyUpYbTziKklLq6E9MTHB7IkqmBQKfLdv5N678Fgl8dh8Bz
7cNuNAyEOSASkqu+NxnVxQDhV3WYdXqX/jT7aSHmKBfml5YMKZakhok3QPFp0UOT
j0tCmVHWyvXvY1KHTwIDAQAB
controller接口实现:
package com.example.product.controller;
import com.example.product.service.ProductService;
import com.example.product.vo.ProductRequest;
import com.example.product.vo.ProductResponse;
import com.example.product.vo.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
@RestController
public class ProductController {
@Resource
private ProductService productService;
@PostMapping(value = "/selectByCondition", consumes = APPLICATION_JSON_VALUE)
public Response selectByCondition(@RequestBody ProductRequest request){
log.info("request.sign:{}", request.getSign());
return Response.success(productService.queryById(request.getId()));
}
}
service实现:
package com.example.product.service;
import com.example.product.vo.ProductResponse;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
@Service
public class ProductService {
private static Map productHashMap = new HashMap<>();
static {
productHashMap.put(1, new ProductResponse(1, "冰箱", 5, new BigDecimal(20000)));
productHashMap.put(2, new ProductResponse(2, "空调", 9, new BigDecimal(30000)));
productHashMap.put(3, new ProductResponse(3, "洗衣机", 8, new BigDecimal(5000)));
}
public ProductResponse queryById(Integer id){
return productHashMap.get(id);
}
}
验签切面类:
package com.example.product.config;
import com.example.product.common.GlobalRequestUtils;
import com.example.product.service.GlobalValuesService;
import com.example.product.common.RsaUtils;
import com.example.product.vo.baseRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import static com.alibaba.fastjson.JSON.toJSONString;
@Slf4j
@Aspect
@Component
public class SecretVerifyAspect{
private String INVOKER_TRACE_ID = "invoke_traceId";
@Resource
GlobalValuesService globalValuesService;
@Before("execution(public * com.example.product.controller.ProductController.*(..))")
public void secretVerify(JoinPoint point){
//打印调用者传过来的traceId
try {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
if (Objects.isNull(sra)) {
log.warn("ServletRequestAttributes is null");
return;
}
HttpServletRequest request = sra.getRequest();
if (Objects.nonNull(request.getHeader(INVOKER_TRACE_ID))) {
//打印调用者的traceId, 出现问题时,方便排查跟踪
log.info("{}:{}", INVOKER_TRACE_ID, request.getHeader(INVOKER_TRACE_ID));
}
} catch (Exception e) {
log.warn("exception:", e);
}
//开始验签
Object[] args = point.getArgs();
for (Object arg : args) {
if (!(arg instanceof baseRequest)) {
continue;
}
baseRequest baseRequest = (baseRequest) arg;
String requestString;
try {
requestString = GlobalRequestUtils.requestString(baseRequest, true);
} catch (IllegalAccessException e) {
log.warn("构建签名参数错误,eMsg:", e);
throw new RuntimeException("签名错误!!!");
}
//校验签名
boolean verify = RsaUtils.verifyByPublicKey(requestString, globalValuesService.didiPublicKey(baseRequest.getAppId()), baseRequest.getSign());
if (!verify) {
log.warn("签名校验错误,requestSign [{}],requestString [{}],args [{}]",
baseRequest.getSign(), requestString, toJSonString(baseRequest));
throw new RuntimeException("签名错误!!!");
}else{
log.info("签名验证正确.");
}
}
}
}
其他工具类:
package com.example.product.common;
import lombok.extern.slf4j.Slf4j;
import java.util.base64;
@Slf4j
public class base64Utils {
@SuppressWarnings("restriction")
public static String encode(byte[] bytes) {
return new String(base64.getEncoder().encode(bytes)).replaceAll("[rn]", "");
}
@SuppressWarnings("restriction")
public static byte[] decode(String str) {
return base64.getDecoder().decode(str);
}
}
package com.example.product.common;
import com.alibaba.fastjson.JSON;
import com.example.product.vo.baseRequest;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang.StringUtils;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Map;
import java.util.TreeMap;
public class GlobalRequestUtils {
private static final String QUOTE_MARK = """;
public static String requestString(T request, boolean filterNull)
throws IllegalAccessException {
return requestString(requestMap(request), filterNull, false);
}
public static Map requestMap(T request)
throws IllegalAccessException {
Map requestMap = new TreeMap<>();
Class> clz = request.getClass();
while (baseRequest.class.isAssignableFrom(clz)) {
for (Field field : clz.getDeclaredFields()) {
field.setAccessible(true);
JsonProperty annotation = field.getAnnotation(JsonProperty.class);
if (annotation == null) {
//没有 @JsonProperty 注解的属性不予解析(sign属性无需加该注解)
continue;
}
String property = StringUtils.isEmpty(annotation.value()) ? field.getName() : annotation.value();
requestMap.put(property, field.get(request));
}
clz = clz.getSuperclass();
}
return requestMap;
}
public static String requestString(Map requestMap, boolean filterNull, boolean urlEncode) {
StringBuilder requestStringBuilder = new StringBuilder();
requestMap.forEach((property, value) -> {
if (filterNull && value == null) {
return;
}
requestStringBuilder.append(property).append("=");
if (value != null) {
String string = JSON.toJSonString(value);
if (string.startsWith(QUOTE_MARK) && string.endsWith(QUOTE_MARK)) {
string = string.substring(1, string.length() - 1);
}
//去掉多次转义
string = string.replaceAll("\\", "");
if (urlEncode) {
try {
string = URLEncoder.encode(string, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
requestStringBuilder.append(string);
}
requestStringBuilder.append("&");
});
if (requestStringBuilder.length() > 0) {
requestStringBuilder.deleteCharAt(requestStringBuilder.length() - 1);
}
return requestStringBuilder.toString();
}
}
package com.example.product.common;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
@Slf4j
public class RsaUtils {
private static String handleKey(String key) {
//1. 去开头结尾符
key = key.replaceAll("--.*--", "");
//2. 去除换行
key = key.replaceAll("[rn]", "");
//3. 去空格
key = key.replaceAll(" ", "");
return key;
}
//#################### 私钥:签名
public static String signatureByPrivateKey(String data, String privateKey) {
if (StringUtils.isBlank(privateKey)) {
log.warn("私钥不可为空");
return "";
}
privateKey = handleKey(privateKey);
try {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(base64Utils.decode(privateKey));
RSAPrivateKey key = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec);
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(key);
signature.update(data.getBytes());
return base64Utils.encode(signature.sign());
} catch (Exception e) {
log.warn("私钥加密失败,data:[{},privateKey:[{}],exception:", data, privateKey,e);
return "";
}
}
//#################### 公钥:验签
public static boolean verifyByPublicKey(String data, String publicKey, String sign) {
if (StringUtils.isBlank(publicKey)) {
log.warn("公钥钥不可为空");
return false;
}
publicKey = handleKey(publicKey);
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(base64Utils.decode(publicKey));
RSAPublicKey rsaPubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initVerify(rsaPubKey);
signature.update(data.getBytes());
return signature.verify(base64Utils.decode(sign));
} catch (Exception e) {
log.warn("公钥解密失败,sign:[{},publicKey:[{}],exception:", sign, publicKey,e);
return false;
}
}
}
package com.example.product.service;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class GlobalValuesService {
@Resource
private Environment environment;
public String didiPublicKey(String appId) {
return environment.getProperty(String.format("rsa.%s.public-key", appId));
}
}
package com.example.product.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class baseRequest {
@JsonProperty
private String appId;
private String sign;
@JsonProperty
private String timestamp;
}
package com.example.product.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ProductRequest extends baseRequest {
@JsonProperty
private Integer id;
}
package com.example.order.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
public class ProductResponse {
private Integer id;
private String name;
private Integer num;
private BigDecimal price;
}
package com.example.order.vo; import lombok.Getter; import lombok.Setter; @Setter @Getter public class Response{ private Integer errorCode; private String errorMsg; private T data; public Response(Integer errorCode, String errorMsg, T data) { this.errorCode = errorCode; this.errorMsg = errorMsg; this.data = data; } public static Response success(T data){ return new Response<>(null, null, data); } }
项目结构:
五、测试验证
第一步:启动order服务。
第二部:启动product服务。
第三步:访问order接口: http://localhost:8081/order/query/1,结果展示:
查看order服务的关键日志展示:
第四步:查看product的关键日志:
六、源码地址
https://download.csdn.net/download/u010132847/33493685。
资料参考:密码学:RSA加密算法详解_大鱼-CSDN博客_rsa加密算法



