目录
一、背景
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字符串传输。
交互流程图,如下:
????????
描述: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服务
? ? ? ? ? ? ? ? pom.xml配置如下:
<?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.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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>
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;
/**
* ProductController
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@RestController
public class ProductController {
@Resource
private ProductService productService;
@RequestMapping(value = "/query/{id}")
public Response<ProductResponse> 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;
/**
* ProductService
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Service
public class ProductService {
@Resource
private ProductMicroServer productMicroServer;
public ProductResponse queryById(Integer id){
ProductRequest productRequest = new ProductRequest();
productRequest.setId(id);
productRequest.setAppId("010");
Response<ProductResponse> 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;
/**
* ProductMicroServer
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@FeignClient(name = "product", url="http://localhost:8082/product", fallbackFactory = ProductMicroServerFallbackFactory.class)
public interface ProductMicroServer {
@PostMapping(value = "/selectByCondition", consumes = MediaType.APPLICATION_JSON_VALUE)
Response<ProductResponse> 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;
/**
* ProductMicroServerFallback
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Service
public class ProductMicroServerFallback implements ProductMicroServer{
@Override
public Response<ProductResponse> 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;
/**
* ProductMicroServerFallbackFactory
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Slf4j
@Service
public class ProductMicroServerFallbackFactory implements FallbackFactory<ProductMicroServer> {
@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;
/**
* http接口统一出口消息处理
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Slf4j
@Component
public class HttpGlobalOutMessageConverter<T extends BaseRequest> extends AbstractHttpMessageConverter<T> {
private static final String QUOTE_MARK = "\"";
/**
* 跟踪得traceId
*/
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);
}
/**
* 重写readInternal方法
* 处理请求中的数据
* @param clazz
* @param inputMessage
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
*/
@Override
protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage) {
throw new RuntimeException("暂不支持");
}
/**
* 重写writeInternal方法
* 处理任何输出数据到response
* @param t
* @param outputMessage
* @throws IOException
* @throws HttpMessageNotWritableException
*/
@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);
}
/**
* 将 参数Map 转成 字符串
* eg:a=1&b=2
*
* @param requestMap 参数Map
* @return 字符串
*/
private static String requestString(TreeMap<String, Object> 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 {
/**
* base64编码
*
* @param bytes bytes
* @return 编码后的字符串
*/
@SuppressWarnings("restriction")
public static String encode(byte[] bytes) {
return new String(Base64.getEncoder().encode(bytes)).replaceAll("[\r\n]", "");
}
/**
* base64解码
*
* @param str str
* @return byte[]
*/
@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;
/**
* RSA加密解密工具类
*/
@Slf4j
public class RsaUtils {
/**
* 钥 处理
*
* @param key 钥
* @return 钥
*/
private static String handleKey(String key) {
//1. 去开头结尾符
key = key.replaceAll("--.*--", "");
//2. 去除换行
key = key.replaceAll("[\r\n]", "");
//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;
/**
* GlobalValues
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/14
*/
@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;
/**
* BaseRequest
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Getter
@Setter
public class BaseRequest {
/**
* 品牌编号
*/
private String appId;
/**
* 时间戳
*/
private String timestamp;
}
package com.example.order.vo;
import lombok.Getter;
import lombok.Setter;
/**
* ProductRequest
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Getter
@Setter
public class ProductRequest extends BaseRequest {
/**
* 商品id
*/
private Integer id;
}
package com.example.order.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
/**
* ProductResponse
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@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;
/**
* Response
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Setter
@Getter
public class Response<T>{
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 <T> Response<T> 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配置:
<?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.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>product</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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>
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;
/**
* ProductController
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Slf4j
@RestController
public class ProductController {
@Resource
private ProductService productService;
@PostMapping(value = "/selectByCondition", consumes = APPLICATION_JSON_VALUE)
public Response<ProductResponse> 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;
/**
* ProductService
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Service
public class ProductService {
private static Map<Integer, ProductResponse> 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;
/**
* 安全验签切面
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/14
*/
@Slf4j
@Aspect
@Component
public class SecretVerifyAspect{
/**
* 调用者traceId(方便跟踪)
*/
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 {
/**
* base64编码
*
* @param bytes bytes
* @return 编码后的字符串
*/
@SuppressWarnings("restriction")
public static String encode(byte[] bytes) {
return new String(Base64.getEncoder().encode(bytes)).replaceAll("[\r\n]", "");
}
/**
* base64解码
*
* @param str str
* @return byte[]
*/
@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 = "\"";
/**
* 将 请求参数 转成 字符串
* eg:a=1&b=2
*
* @param request 请求参数
* @param <T> AbstractDidiGlobalRequest
* @return 字符串
* @throws IllegalAccessException
*/
public static <T extends BaseRequest> String requestString(T request, boolean filterNull)
throws IllegalAccessException {
return requestString(requestMap(request), filterNull, false);
}
/**
* 将 请求参数 转成 Map
* 按属性值升序排序
*
* @param request 请求参数
* @param <T> AbstractDidiGlobalRequest
* @return Map
* @throws IllegalAccessException
*/
public static <T extends BaseRequest> Map<String, Object> requestMap(T request)
throws IllegalAccessException {
Map<String, Object> 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;
}
/**
* 将 参数Map 转成 字符串
* eg:a=1&b=2
*
* @param requestMap 参数Map
* @return 字符串
*/
public static String requestString(Map<String, Object> 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;
/**
* RSA加密解密工具类
*/
@Slf4j
public class RsaUtils {
/**
* 钥 处理
*
* @param key 钥
* @return 钥
*/
private static String handleKey(String key) {
//1. 去开头结尾符
key = key.replaceAll("--.*--", "");
//2. 去除换行
key = key.replaceAll("[\r\n]", "");
//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;
/**
* GlobalValues
*
* @author chengjiangbo@shandiantech.com
* @version 1.0.0
* @date 2021/10/14
*/
@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;
/**
* BaseRequest
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Setter
@Getter
public class BaseRequest {
/**
* 注意这里的@JsonProperty注解,加了该注解,GlobalRequestUtils工具里的requestMap(...)方法才会将其解析
*/
@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;
/**
* ProductRequest
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@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;
/**
* ProductResponse
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@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;
/**
* Response
*
* @author chengjiangbo@xxx.com
* @version 1.0.0
* @date 2021/10/12
*/
@Setter
@Getter
public class Response<T>{
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 <T> Response<T> 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加密算法
|