http接口公网对接时用到的RSA加密/解密实现示例

目录

一、背景

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算法

  1. RSA是目前最有影响力和最常用的公钥加密算法,它能够抵抗到目前为止已知的绝大多数密码攻击,已被ISO推荐为公钥数据加密标准。

  2. 今天只有短的RSA钥匙才可能被强力方式破解。但在分布式计算和量子计算机理论日趋成熟的今天,RSA加密安全性收到了挑战和质疑。

  3. RSA算法基于一个十分简单的数论事实:将两个大质数相乘十分容易,但是想要对其乘积进行因式分解缺及其困难,因此可以将乘积公开作为加密密钥。

  4. 可以自己实现,无需购买,算法公开。


1.2、HTTPS


        1.2.1、 HTTPS优点

  1. 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器。

  2. HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。

  3. HTTPS是现行框架下最安全的解决方案,虽然不是觉得安全,但它增加了中间人攻击的成本。


        1.2.2、 HTTPS缺点

  1. SSL的专业证书需要购买,功能越强大的证书费用越高

  2. 相同的网络环境下,HTTPS协议会使页面的加载时间延长50%,增加10%-20%的耗电。此外,HTTPS协议还会影响缓存,增加数据开销和功耗。

  3. HTTPS协议的安全性是有范围的,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。

  4. 最关键的是,SSL证书的信用链体系并不安全。特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。


二、目标

        2.1、实现如下示例加签规则

  1. 将参数列表中除了sign的字段按照key升序排列,类似get的方式,用”=”和”&”拼接成字符串。
  2. 将编码得到的字符串使用私钥加密,密文字符串进行base64编码,得到的结果就是sign的值。
  3. 加密采用非对称RSA密钥对,密钥位数1024位。 
  4. 最后以对象的序列化后的json字符串传输。

交互流程图,如下:

        http接口公网对接时用到的RSA加密/解密实现示例_第1张图片

描述:A企业、B企业先生成公私钥,然后互相交换公钥。调用方调用接口前,使用自己的私钥加密; 被调用方接收数据前使用调用方给的公钥解密,解密成功允许调用接口逻辑处理返回数据;解密失败(鉴权失败)不允许调用接口。

        2.2、具体密钥生成方式步骤

        第一步:生成私钥命令

        如:openssl genrsa -out rsa_private_key.pem 1024

命令格式:openssl genras -out 私钥文件名 1024 

实际操作如下(这里使用git bash界面):

http接口公网对接时用到的RSA加密/解密实现示例_第2张图片

 生成一个由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾的rsa_private_key.pem文件,这就是私钥文件。

http接口公网对接时用到的RSA加密/解密实现示例_第3张图片

        第二步:根据私钥生成对应公钥pem文件

        如:openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

命令格式:openssl rsa -in私钥文件名 -pubout -out 公钥文件名

 实际操作如下:http接口公网对接时用到的RSA加密/解密实现示例_第4张图片

  生成一个由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾的rsa_public_key.pem文件,这就是公钥文件。http接口公网对接时用到的RSA加密/解密实现示例_第5张图片

        第三步:将私钥转换成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格式私钥文件名 

实际操作如下:

http接口公网对接时用到的RSA加密/解密实现示例_第6张图片

 生成一个由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾的rsa_private_key_pkcs8.pem文件,这就是适配java语言开发的私钥文件(第一步生成的私钥是pkcs1格式的文件,像php可以直接使用。但java使用就必须转换成pkcs8格式的文件内容)。

http接口公网对接时用到的RSA加密/解密实现示例_第7张图片

 描述:可以发现第三步和第一步都是私钥,他们都是由”----BEGIN PRIVATE KEY-----”开头,由”-----END PRIVATE KEY-----”结尾,密钥内容却不相同。在java代码里,我们读取的密钥体是不包含开头和结尾的,因此我们把第二部和第三步的pem文件去掉开头结尾重新存储下。

三、准备(order作为A企业服务,product作为B企业服务)

  1. 服务order实现一个查询商品接口,商品接口由服务product以http接口形式提供,并实现一个消息转换器,做加密认证操作(基于目前大部分服务都是高可用分布式微服务,所以本次order服务调用product服务接口使用springcloud的feignClient接口实现。注意,这里feignclient不用eureka服务,而是通过配置url直接调用product服务)。
  2. 服务product实现一个基于spring MVC框架实现Http接口,并实现一个切面拦截被调用接口的请求做解密认证。
  3. 环境:jdk1.8。

四、代码展示

        4.1、order服务

                pom.xml配置如下:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.5.5
         
    
    com.example
    order
    0.0.1-SNAPSHOT
    order
    Demo project for Spring Boot
    
        1.8
        2020.0.4
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.cloud
            spring-cloud-starter
        
        
            org.springframework.cloud
            spring-cloud-starter-openfeign
        

        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            com.alibaba
            fastjson
            1.2.78
        
    
    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.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;

/**
 * ProductController
 *
 * @author [email protected]
 * @version 1.0.0
 * @date 2021/10/12
 */

@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;

/**
 * ProductService
 *
 * @author [email protected]
 * @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 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 [email protected]
 * @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 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 [email protected]
 * @version 1.0.0
 * @date 2021/10/12
 */

@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;

/**
 * ProductMicroServerFallbackFactory
 *
 * @author [email protected]
 * @version 1.0.0
 * @date 2021/10/12
 */
@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;

/**
 * http接口统一出口消息处理
 *
 * @author [email protected]
 * @version 1.0.0
 * @date 2021/10/12
 */
@Slf4j
@Component
public class HttpGlobalOutMessageConverter extends AbstractHttpMessageConverter {

    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 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 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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @version 1.0.0
 * @date 2021/10/12
 */
@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);
    }
}

 项目结构:

http接口公网对接时用到的RSA加密/解密实现示例_第8张图片

        5.1、product服务

                pom.xml配置:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.5.5
         
    
    com.example
    product
    0.0.1-SNAPSHOT
    product
    Demo project for Spring Boot
    
        1.8
        2020.0.4
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.cloud
            spring-cloud-starter
        

        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            com.alibaba
            fastjson
            1.2.78
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
    
    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.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;

/**
 * ProductController
 *
 * @author [email protected]
 * @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 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 [email protected]
 * @version 1.0.0
 * @date 2021/10/12
 */
@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;

/**
 * 安全验签切面
 * @author [email protected]
 * @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      AbstractDidiGlobalRequest
   * @return 字符串
   * @throws IllegalAccessException
   */
  public static  String requestString(T request, boolean filterNull)
    throws IllegalAccessException {
    return requestString(requestMap(request), filterNull, false);
  }

  /**
   * 将 请求参数 转成 Map
   * 按属性值升序排序
   *
   * @param request 请求参数
   * @param      AbstractDidiGlobalRequest
   * @return Map
   * @throws IllegalAccessException
   */
  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;
  }

  /**
   * 将 参数Map 转成 字符串
   * eg:a=1&b=2
   *
   * @param requestMap 参数Map
   * @return 字符串
   */
  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;

/**
 * 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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @version 1.0.0
 * @date 2021/10/12
 */
@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);
    }
}

项目结构:

http接口公网对接时用到的RSA加密/解密实现示例_第9张图片

五、测试验证

        第一步:启动order服务。

        第二部:启动product服务。

        第三步:访问order接口: http://localhost:8081/order/query/1,结果展示: 

http接口公网对接时用到的RSA加密/解密实现示例_第10张图片

查看order服务的关键日志展示:

         第四步:查看product的关键日志:

六、源码地址

  https://download.csdn.net/download/u010132847/33493685。

资料参考:密码学:RSA加密算法详解_大鱼-CSDN博客_rsa加密算法

你可能感兴趣的:(java,java,安全,http)