前后分离的系统统一加解密

背景

前端使用vue(axios),后台使用的spring boot,为了保证数据传输的安全,防止爬虫等。
需要把前端请求参数加密,后台将入参解密,再把出参加密,前端拿到响应后进行解密。

前端

前端使用的axios封装的http请求,使用拦截器对请求加密,对响应解密。

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
// 请求拦截
interceptors(instance, url) {
// 添加请求拦截器
instance.interceptors.request.use(config => {
// 请求数据加密
if (config.data) {
config.data = Util.encrypt(config.data);
}

return config;
}, error => {
// 对请求错误做些什么
return Promise.reject({respCo: '9999', respMsg: error.toLocaleString()});
});

// 添加响应拦截器
instance.interceptors.response.use((res) => {
// 响应数据解密
let data = Util.decrypt(res.data);
this.destroy(url);
if (data.respCo === '0000') {
// 成功
return data;
} else {
// 各种失败
return Promise.reject(data);
}
}, (error) => {
// 对响应错误做点什么
return Promise.reject({respCo: '9999', respMsg: error.toLocaleString()});
});
}

其中util.vue:

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
import CryptoJS from 'crypto-js';

let util = {};

/**
* aes
*
* @type {string}
*/
util.AES_KEY = CryptoJS.enc.Latin1.parse('kangyonggan12345');
util.AES_IV = CryptoJS.enc.Latin1.parse('kangyonggan12345');

/**
* aes加密
*
* @param data
* @returns {string}
*/
util.encrypt = function (data) {
data = CryptoJS.AES.encrypt(JSON.stringify(data), util.AES_KEY, {
iv: util.AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
}).toString();
return JSON.stringify(data);
};

/**
* aes解密
*
* @param data
* @returns {Object}
*/
util.decrypt = function (data) {
if (!data) {
return {};
}

let decrypted = CryptoJS.AES.decrypt(data, util.AES_KEY, {
iv: util.AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
}).toString(CryptoJS.enc.Utf8);

return JSON.parse(decrypted);
};


export default util;

后台

在filter中对RequestBody进行解密,并把解密后的json按照名值对放入parameter中,以便controller中获取参数时免写@RequestBody注解。

SecretRequestFilter.java:

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
package com.kangyonggan.demo.filter;

import com.kangyonggan.demo.util.SecretRequestWrapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @author kangyonggan
* @since 2019-04-15
*/
@Configuration
public class SecretRequestFilter extends OncePerRequestFilter {

@Value("${app.aes-key}")
private String aesKey;

@Value("${app.aes-iv}")
private String aesIv;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
filterChain.doFilter(new SecretRequestWrapper(request, aesKey, aesIv), response);
}

}

其中SecretRequestWrapper.java:

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
package com.kangyonggan.demo.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.kangyonggan.demo.constants.AppConstants;
import lombok.extern.log4j.Log4j2;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

/**
* @author kangyonggan
* @since 2019-04-15
*/
@Log4j2
public class SecretRequestWrapper extends HttpServletRequestWrapper {

/**
* 最终参数=原始参数+自定义参数
*/
private Map<String, String[]> parameterMap = new HashMap<>();

private byte[] body;

private String aesKey;

private String aesIv;

private JSONObject params;

public SecretRequestWrapper(HttpServletRequest request, String aesKey, String aesIv) throws IOException {
super(request);
this.aesKey = aesKey;
this.aesIv = aesIv;
this.params = new JSONObject();
body = getBodyString(request).getBytes(Charset.forName(AppConstants.DEFAULT_CHARSET));

// 把原始参数放入最终参数中
this.parameterMap.putAll(request.getParameterMap());

// 把body中的json参数放入最终参数中
JSONObject jsonObject = getAttrs();
for (String key : jsonObject.keySet()) {
addParameter(key, jsonObject.get(key));
}
}

@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

public JSONObject getAttrs() {
if (params.isEmpty()) {
try {
String encryptedText = new String(body, AppConstants.DEFAULT_CHARSET);
params = JSON.parseObject(Aes.desEncrypt(encryptedText, aesKey, aesIv));
} catch (Exception e) {
throw new RuntimeException("无法获取body中加密参数对应的json对象", e);
}
}

if (params == null) {
params = new JSONObject();
}

return params;
}

@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(body);

return new ServletInputStream() {

@Override
public int read() throws IOException {
return inputStream.read();
}

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {

}
};
}

public static String getBodyString(ServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), Charset.forName(AppConstants.DEFAULT_CHARSET)));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
return sb.toString();
}

/**
* 把自定义参数放入最终参数中
*
* @param key
* @param value
*/
private void addParameter(String key, Object value) {
if (value instanceof String[]) {
parameterMap.put(key, (String[]) value);
} else if (value instanceof String) {
parameterMap.put(key, new String[]{(String) value});
} else {
parameterMap.put(key, new String[]{String.valueOf(value)});
}
}

/**
* 重写getParameter相关方法
*
* @param name
* @return
*/
@Override
public String getParameter(String name) {
String[] arr = parameterMap.get(name);
if (arr != null && arr.length > 0) {
return arr[0];
}
return null;
}

/**
* 重写getParameter相关方法
*
* @return
*/
@Override
public Map<String, String[]> getParameterMap() {
return parameterMap;
}

/**
* 重写getParameter相关方法
*
* @return
*/
@Override
public Enumeration<String> getParameterNames() {
Vector<String> vector = new Vector<>(parameterMap.keySet());
return vector.elements();
}

/**
* 重写getParameter相关方法
*
* @param name
* @return
*/
@Override
public String[] getParameterValues(String name) {
return parameterMap.get(name);
}
}

在ResponseBodyAdvice中对响应进行统一加密,ResponseAdvice.java:

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
package com.kangyonggan.demo.advice;

import com.alibaba.fastjson.JSON;
import com.kangyonggan.demo.annotation.Secret;
import com.kangyonggan.demo.util.Aes;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
* @author kangyonggan
* @since 2019-04-15
*/
@RestControllerAdvice
@Log4j2
public class ResponseAdvice implements ResponseBodyAdvice {

@Value("${app.aes-key}")
private String aesKey;

@Value("${app.aes-iv}")
private String aesIv;

@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}

@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (enable(methodParameter)) {
try {
return Aes.encrypt(JSON.toJSONString(o), aesKey, aesIv);
} catch (Exception e) {
throw new RuntimeException("出参加密异常", e);
}
}

return o;
}

/**
* 判断是否启用加密
*
* @param parameter
* @return
*/
private boolean enable(MethodParameter parameter) {
boolean enable = false;

// 父类(第三优先级)
Secret superSecret = parameter.getContainingClass().getSuperclass().getAnnotation(Secret.class);
if (superSecret != null) {
enable = superSecret.enable();
}

// 当前类(第二优先级)
Secret classSecret = parameter.getContainingClass().getAnnotation(Secret.class);
if (classSecret != null) {
enable = classSecret.enable();
}

// 当前方法(第一优先级)
Secret methodSecret = parameter.getMethod().getAnnotation(Secret.class);
if (methodSecret != null) {
enable = methodSecret.enable();
}

return enable;
}
}

其中@Secret注解用来决定是否对响应进行加密,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.kangyonggan.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author kangyonggan
* @since 2018/6/3 0003
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Secret {

/**
* 是否启用加密
*
* @return
*/
boolean enable() default true;

}

Aes加密工具代码如下:

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
package com.kangyonggan.demo.util;


import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
* @author kangyonggan
* @since 5/4/18
*/
public final class Aes {

private Aes() {}

/**
* 加密
*
* @param data
* @param aesKey
* @param aesIv
* @return
* @throws Exception
*/
public static String encrypt(String data, String aesKey, String aesIv) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
byte[] dataBytes = data.getBytes();
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keySpec = new SecretKeySpec(aesKey.getBytes(), "AES");
IvParameterSpec parameterSpec = new IvParameterSpec(aesIv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
byte[] encrypted = cipher.doFinal(plaintext);
return Base64.encodeBase64String(encrypted);
}

/**
* aes解密
*
* @param encrypted
* @param aesKey
* @param aesIv
* @return
* @throws Exception
*/
public static String desEncrypt(String encrypted, String aesKey, String aesIv) throws Exception {
byte[] encrypted1 = Base64.decodeBase64(encrypted);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey.getBytes(), "AES");
IvParameterSpec parameterSpec = new IvParameterSpec(aesIv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
return originalString.trim();
}

}

如果使用swagger来生成接口文档给前端童鞋使用,当我们队接口进行了统一加密解密之后,
swagger-ui的那一套界面就废了,因为他并不没有实现加密解密,因此我们需要重写一套自己的swagger-ui,
实现的思路也比较简单,swagger会把所有的接口信息生成一个json信息,接口地址是:http://localhost:8080/v2/api-docs。

我们拿到所有接口的json信息之后,可以画一个简单的界面,列出所有的接口信息,然后实现自己的”try it out”,这时就可以对所有的请求进行加解密了。
也可以在github上搜索swagger-ui,clone到本地后对请求响应进行加解密,这样就不用自己画ui了。