非https下的数据加密传输

2022/3/3 SpringBoot

# 一、应用场景

  • 1、已经开发完成的系统需要对传输的数据进行加解密操作;
  • 2、开发的接口有加密要求,传来的原始数据就是密文,方法接收密文后再内部解析后使用太啰嗦;
  • 3、返回的数据是加密数据,在每个接口返回的地方调用加密太啰嗦代码量增加;

# 二、解决思路

使用@ControllerAdvice的在执行目标Controller方法之前执行被注释类的特性, 配合RequestBodyAdviceResponseBodyAdvice可以修改请求数据和返回数据的特性解决以上问题。

# 三、实例讲解

前后端分离项目非https下的数据加密传输,技术栈列表:

  • SpringBoot
  • RSA
  • vue
  • axios
  • JSEncrypt
  • CryptoJS 这里实现的是RSA非对称加密,也可以使用AES对称加密,实现思路一样只是算法不一样而已。

# 1、设计思路

1、前端在axios中添加拦截器,对要发送的数据和返回的数据进行加密和解密操作

2、后台添加`RequestBodyAdvicer`接口实现类,对进入请求方法前的数据进行解密处理

3、后台添加`ResponseBodyAdvice`接口实现类,对方法返回的数据传递到请求处前进行加密处理

4、登录后后台动态分配公钥给到前端,之后的数据加解密前端都使用该公钥,后台缓存私钥到redis中并设置存活时间,前台缓存到vuex中

# 2、预判的问题

1、前端是使用公钥加密和解密的,JSEncrypt并不支持公钥加密,需自己实现

2、默认的RSA加密解密是不支持长文本的,需要自己实现分段加密和分段解密

3、中文和特殊字符等的加密解密前后端可能不一致,需要自己实现处理

4、并不是所有的方法都需要加密和解密,需支持排除某些请求

# 3、具体代码实现

# 1、修改axios配置

以下用到的encrypt4Long,base64_encode,base64_decode,decrypt4Long方法需要自己实现,后面给出

// request interceptor
service.interceptors.request.use(
  (config) => {
    // 省略其他配置,末尾追加以下配置

    // 如果不是文件流,且不是配置不需加密的
    if (config.responseType !== 'blob' && !config.notEncrypt) {
      config.transformRequest = [
        (data) => {
          // 解决添加该配置后改为:application/x-www-form-urlencoded的问题
          config.headers['Content-Type'] = 'application/json;charset=UTF-8'

          // 对 data 加密处理
          data = encrypt4Long(base64_encode(JSON.stringify(data)), Vue.ls.get(PUBLICKEY))

          return data
        }
      ]
    }

    // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
    config.transformResponse = [
      (data) => {
        // 对 data 进行解密
        try {
          data = JSON.parse(data)
          if (data.translate && data.result) {
            const ll = base64_decode(decrypt4Long(data.result, Vue.ls.get(PUBLICKEY)))
            data.result = JSON.parse(ll)
          }
        } catch (e) {
          data = { code: 500 }
        }
        return data
      }
    ]

    return config
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 2、后台解密处理

/**
 * 解密客户端的RAS加密请求体
 *
 * @author fuyd
 * @date 2022-03-02
 */
@ControllerAdvice//(basePackages = "org.geecg.xxx.controller")//此处设置需要当前Advice执行的域, 省略默认全局生效
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class RequestBodyDecodeAdvice extends RequestBodyAdviceAdapter {
    final private RedisUtil redisUtil;

    /**
     * 该方法用于判断当前请求,是否要执行beforeBodyRead方法
     *
     * @param methodParameter 方法的参数对象
     * @param targetType      方法的参数类型
     * @param converterType   将会使用到的Http消息转换器类类型
     * @return 返回true则会执行beforeBodyRead
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 添加注解的优先级最高,默认都是需要解密的,如果有注解且request() == false则不需要解密
		NotEncrypt notEncrypt = methodParameter.getMethodAnnotation(NotEncrypt.class);
		if(!Objects.isNull(notEncrypt) && !notEncrypt.request()){
		    return false;
        }

        // 获取注解
        Annotation[] declaredAnnotations = methodParameter.getMethodAnnotations();

        // 获得方法的RequestMapping及其子类注解,method为post、put、patch类型的才会加密
        for (Annotation annotation : declaredAnnotations) {
            if (annotation instanceof PostMapping || annotation instanceof PutMapping || annotation instanceof PatchMapping) {
                return true;
            }
            if (annotation instanceof RequestMapping) {
                RequestMapping mapping = (RequestMapping) (annotation);
                RequestMethod[] m = mapping.method();
                for (int i = 0; i < m.length ; i++) {
                    if (m[i].equals(RequestMethod.POST) || m[i].equals(RequestMethod.PUT) || m[i].equals(RequestMethod.PATCH)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * 在Http消息转换器执转换,之前执行
     *
     * @param inputMessage  客户端的请求数据
     * @param parameter     方法的参数对象
     * @param targetType    方法的参数类型
     * @param converterType 将会使用到的Http消息转换器类类型
     * @return 返回 一个自定义的HttpInputMessage
     */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
                                           Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

        // 读取加密的请求体
        byte[] body = new byte[inputMessage.getBody().available()];
        inputMessage.getBody().read(body);

        // 使用解密后的数据,构造新的读取流
        InputStream rawInputStream = new ByteArrayInputStream(
                Base64.decode(
                        decrypt(StrUtil.str(body, CharsetUtil.CHARSET_UTF_8))
                )
        );

        return new HttpInputMessage() {
            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }

            @Override
            public InputStream getBody() throws IOException {
                return rawInputStream;
            }
        };
    }

    /**
     * 解密RSA数据
     *
     * @param body 密文
     * @return 明文 byte[]
     */
    public byte[] decrypt(String body) {
        return ResponseBodyEncodeAdvice.getRSAofPrivateKeyByUserName(redisUtil).decrypt(body, KeyType.PrivateKey);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

# 3、后台加密处理

/**
 * 对返回数据进行加密
 *
 * @author fuyd
 * @date 2022-03-03
 */
@ControllerAdvice
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ResponseBodyEncodeAdvice implements ResponseBodyAdvice<Result> {
	final private RedisUtil redisUtil;
    /**
	 * @param returnType 响应的数据类型
	 * @param converterType 最终将会使用的消息转换器
	 * @return 返回bool,表示是否要在响应之前执行“beforeBodyWrite” 方法
	 */
    @Override
	public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType){
    	// 添加注解的优先级最高,默认都是需要添加的,如果有注解且response() == false则不需要加密
		NotEncrypt notEncrypt = returnType.getMethodAnnotation(NotEncrypt.class);
		if(notEncrypt != null && !notEncrypt.response()){
			return false;
		}
		// 返回类型为Result类型的进行加密
    	if(returnType.getMethod().getReturnType().isAssignableFrom(Result.class)){
    		return true;
		}
        return false;
    }

    /**
	 * @param result 响应的数据,也就是响应体
	 * @param returnType 响应的数据类型
	 * @param selectedContentType 响应的ContentType
	 * @param selectedConverterType 最终将会使用的消息转换器
	 * @param request
	 * @param response
	 * @return 被修改后的响应体,可以为null,表示没有任何响应
	 */
    @Override
    public Result beforeBodyWrite(Result result, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response) {
    	// 获取返回值中的数据部分,只对数据部分加密
        Object data = result.getResult();
		if (data != null) {
			result.setResult(encrypt(data));
			result.setTranslate(true);
		}
		else{
			result.setTranslate(false);
		}
		return result;
    }

	/**
	 * 对数据进行加密,先把对象转换成json格式字符串然后对json格式字符串加密
	 * @param obj 待加密对象
	 * @return 加密后字符串
	 */
	public String encrypt(Object obj){
    	return getRSAofPrivateKeyByUserName(redisUtil)
				.encryptBase64(
						Base64.encode(JSON.toJSONString(obj)), // 解决乱码和特殊字符等
						KeyType.PrivateKey
				);
	}

	/**
	 * 获得用户当前的私钥RSA工具类
	 * @param redisUtil
	 * @return
	 */
	public static RSA getRSAofPrivateKeyByUserName(RedisUtil redisUtil) {
		// 获取登录用户信息,如果没有用户信息,抛出异常
		LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
		if (sysUser == null) {
			throw new RuntimeException("数据需要加密,请先登录!");
		}

		// redis中获取私钥,如果私钥不存在说明过期需重新登录
		Object privateKey = redisUtil.get(CommonConstant.PREFIX_RSA_PRK + sysUser.getUsername());
		if (Objects.isNull(privateKey)) {
			throw new RuntimeException("数据需要加密,请先登录!");
		}

		return new RSA(privateKey.toString(), null);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

# 4、需要自己实现的方法或注解

# 1、后台自定义排除注解

/**
 * 系统方法是否排除加密注解
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotEncrypt {
    /**
     * 请求数据是否需要按加密进行解密处理,false 不进行解密处理,true进行解密处理
     */
    boolean request() default false;

    /**
     * 返回的数据是否需要进行加密处理,true 需要进行加密,false不需要进行加密
     */
    boolean response() default true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2和3都是调用的封装,具体的方法实现在下面4中

# 2、前端自定义长文本加密

/**
 * 长字符串加密(公钥)
 * @param {*} str 要加密字符串
 * @param {*} publicKey 公钥
 * @returns
 */
export function encrypt4Long(str, publicKey) {
  const encryptor = new JSEncrypt(); // JSEncrypt对象
  encryptor.setPublicKey(publicKey); // 公钥
  const encryptStr = encryptor.encryptLong(str); // 超长密码进行加密
  return encryptStr;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 3、前端自定义长文本解密(公钥)

/**
 * 长字符串解密(公钥)
 * @param {*} str 待解密字符串
 * @param {*} publicKey 公钥
 * @returns
 */
export function decrypt4Long(str, publicKey) {
  const encryptor = new JSEncrypt(); // JSEncrypt对象
  encryptor.setPublicKey(publicKey); // 公钥
  const decrypStr = encryptor.decryptLong(str, true); // 超长密码进行解密
  return decrypStr;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 4、解决js公钥不能解密问题

function patchLong(JSEncrypt) {
  var b64map =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var b64pad = "=";

  function hex2b64(h) {
    var i;
    var c;
    var ret = "";
    for (i = 0; i + 3 <= h.length; i += 3) {
      c = parseInt(h.substring(i, i + 3), 16);
      ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63);
    }
    if (i + 1 == h.length) {
      c = parseInt(h.substring(i, i + 1), 16);
      ret += b64map.charAt(c << 2);
    } else if (i + 2 == h.length) {
      c = parseInt(h.substring(i, i + 2), 16);
      ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4);
    }
    while ((ret.length & 3) > 0) {
      ret += b64pad;
    }
    return ret;
  }
  var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz";
  function int2char(n) {
      return BI_RM.charAt(n);
  }
  function b64tohex(s) {
    var ret = "";
    var i;
    var k = 0; // b64 state, 0-3
    var slop = 0;
    for (i = 0; i < s.length; ++i) {
        if (s.charAt(i) == b64pad) {
            break;
        }
        var v = b64map.indexOf(s.charAt(i));
        if (v < 0) {
            continue;
        }
        if (k == 0) {
            ret += int2char(v >> 2);
            slop = v & 3;
            k = 1;
        }
        else if (k == 1) {
            ret += int2char((slop << 2) | (v >> 4));
            slop = v & 0xf;
            k = 2;
        }
        else if (k == 2) {
            ret += int2char(slop);
            ret += int2char(v >> 2);
            slop = v & 3;
            k = 3;
        }
        else {
            ret += int2char((slop << 2) | (v >> 4));
            ret += int2char(v & 0xf);
            k = 0;
        }
    }
    if (k == 1) {
        ret += int2char(slop << 2);
    }
    return ret;
  }

  // 长字符串加密
  JSEncrypt.prototype.encryptLong = function (string) {
    var k = this.getKey();
    // var MAX_ENCRYPT_BLOCK = (((k.n.bitLength() + 7) >> 3) - 11);
    var MAX_ENCRYPT_BLOCK = 117;
    try {
      var lt = "";
      var ct = "";
      // RSA每次加密117bytes,需要辅助方法判断字符串截取位置
      // 1.获取字符串截取点
      var bytes = [];
      bytes.push(0);
      var byteNo = 0;
      var len, c;
      len = string.length;
      var temp = 0;
      for (var i = 0; i < len; i++) {
        c = string.charCodeAt(i);
        if (c >= 0x010000 && c <= 0x10ffff) {
          byteNo += 4;
        } else if (c >= 0x000800 && c <= 0x00ffff) {
          byteNo += 3;
        } else if (c >= 0x000080 && c <= 0x0007ff) {
          byteNo += 2;
        } else {
          byteNo += 1;
        }
        if (
          byteNo % MAX_ENCRYPT_BLOCK >= 114 ||
          byteNo % MAX_ENCRYPT_BLOCK == 0
        ) {
          if (byteNo - temp >= 114) {
            bytes.push(i);
            temp = byteNo;
          }
        }
      }
      // 2.截取字符串并分段加密
      if (bytes.length > 1) {
        for (var i = 0; i < bytes.length - 1; i++) {
          var str;
          if (i == 0) {
            str = string.substring(0, bytes[i + 1] + 1);
          } else {
            str = string.substring(bytes[i] + 1, bytes[i + 1] + 1);
          }
          var t1 = k.encrypt(str);
          ct += t1;
        }

        if (bytes[bytes.length - 1] != string.length - 1) {
          var lastStr = string.substring(bytes[bytes.length - 1] + 1);
          ct += k.encrypt(lastStr);
        }
        return hex2b64(ct);
      }
      var t = k.encrypt(string);
      var y = hex2b64(t);
      return y;
    } catch (ex) {
      return false;
    }
  };

  /**
   * 长字符串解密
   * @param {*} text 待解密字符串
   * @param {*} usePublicKey 是否使用公钥
   * @returns
   */
  JSEncrypt.prototype.decryptLong = function (text, usePublicKey) {
    // var maxLength = 128;
    var k = this.getKey()
    if(usePublicKey)(
      k.decrypt = (ctext) => {
        var c = new BigInteger(ctext,16)
        var m = c.modPowInt(k.e, k.n);
        if (m == null) {
            return null;
        }
        return pkcs1unpad2(m);
      }
    )

    var maxLength = 128//((k.n.bitLength() + 7) >> 3) * 2

    try {
      var str = b64tohex(text);
      // var b=hex2Bytes(str);

      var inputLen = str.length;

      var ct = '';

      if (inputLen > maxLength) {
        var lt = str.match(/.{1,256}/g);
        lt.forEach(function (entry) {
          var t1 = k.decrypt(entry);
          ct += t1;
        });
        return ct;
      }
      var y = k.decrypt(b64tohex(text));
      return y;
    } catch (ex) {
      console.error(ex)
      return false;
    }
  }

  /**
   * 公钥解密时覆盖密钥算法
   * @param {*} d
   * @returns
   */
  function pkcs1unpad2(d) {
    var b = d.toByteArray();
    var i = 0;
    while (i < b.length && b[i] == 0) {
        ++i;
    }
    ++i;
    while (b[i] != 0) {
        if (++i >= b.length) {
            return null;
        }
    }
    var ret = "";
    while (++i < b.length) {
        var c = b[i] & 255;
        if (c < 128) { // utf-8 decode
            ret += String.fromCharCode(c);
        }
        else if ((c > 191) && (c < 224)) {
            ret += String.fromCharCode(((c & 31) << 6) | (b[i + 1] & 63));
            ++i;
        }
        else {
            ret += String.fromCharCode(((c & 15) << 12) | ((b[i + 1] & 63) << 6) | (b[i + 2] & 63));
            i += 2;
        }
    }
    return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214

# 5、使用说明

因为是框架层面的封装,开发流程和之前开发无任何区别。需要注意的是:

  • 前端只有post、put、patch的请求才会默认加密数据,其他请求如get是否配置都不会加密
  • 同上,默认只有post、put、patch才会去解密,其他配置了有不会有效
  • 前后端的加密解密配置是分开的,且前端发送的非加密请求,同样可以返回加密数据;前端发送的加密请求 后台也可以返回不加密数据
  • 如果前端配置了不发送加密数据,则后端同时要配置不解析加密配置
  • 如果后端配置了不解密数据,则前端也要配置不发送加密数据
  • 如果后端返回前端的数据是加密数据,前端不需任何操作,因为拦截器自己知道该数据是否是加密数据会自动处理

如果有需要排除的加解密方法时,使用方法如下:

# 1、前端调用后端

前端在调用后端api接口处添加排除加密配置notEncrypt为true,因为默认是发送加密数据,所以要配置不加密,如:

export function login(parameter) {
  return axios({
    url: '/xxx/xxxx', 
    // 自定义参数,是否不加密传值(在post、put、patch这些method情况下才会有效)
    notEncrypt: true,
    method: 'post',
    data: parameter
  })
}
1
2
3
4
5
6
7
8
9

# 2、后端返回数据

默认只有返回Result类型的接口才会加密,其他类型没有做处理。排除加密解密配置如下:

/**
* 后台生成图形验证码 :有效
* @param response
* @param key
*/
@ApiOperation("获取验证码")
@GetMapping(value = "/randomImage/{key}")
@NotEncrypt(response = false)
public Result<String> randomImage(HttpServletResponse response,@PathVariable String key){
    Result<String> res = new Result<String>();
    
    // 省略业务代码,,,,,,
        
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
更新日期: 2022/4/27 下午2:27:30