一卓的博客

怕什么真理无穷,
进一寸有一寸的欢喜。

0%

前后端通信有字段需要加解密你会如何处理

引言:

最近有个需求:后端所有数据的 ID 字段返回前段时均需要加密处理,前端传入后端需要解密处理,你会如何设计呢?

普通做法:

返回数据时直接调用工具类进行 ID 字符串的加密,前端传入数据后再调用工具类进行 ID 的解密

这样做的问题是所有地方都需要进行加解密的处理,但是此操作和业务无关,不应该被感知,所以本文带大家来从框架层面解决这个问题。

方案一:无感知版本 需按以下规范使用

1、返回前端数据除了 ID,其他字段不能使用 Long 类型(Long 类型的所有字段都将进行 hash 编码)
2、前端传入加密 id 字段名统一为 hashId, 后端接收参数名为 hashId 则接收未解码的 hashId.后端参数为 id 则接收解码的 id;

出参加密:使用自定义 Jackson 序列化类解决出参加密

创建一个 SpringBoot 项目,添加自定义的 Jackson 序列化类如下

1
2
3
4
5
6
7
8
9
10
/** 添加此注解 则 SpringBoot 框架会自动将此序列化类注入 */
@JsonComponent
public class CustomeJackSon {
public static class Serialize extends JsonSerializer<Long> {
@Override
public void serialize(Long id, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(HashIdUtils.encode(id));
}
}
}

测试代码:

模拟编解码工具类HashIdUtils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HashIdUtils {
public static String encode(Long id){
return BaseConstant.PREFIX + id;
}
public static String decode(String hashId){
if (!hashId.contains(BaseConstant.PREFIX)){
return hashId;
}
return hashId.replace(BaseConstant.PREFIX, "");
}
public static Long decode2Long(String hashId){
if (!hashId.contains(BaseConstant.PREFIX)){
try {
return Long.valueOf(hashId);
} catch (Exception e){
return null;
}
}
return Long.valueOf(hashId.replace(BaseConstant.PREFIX, ""));
}
}
常量类 BaseConstant
1
2
3
4
5
6
7
8
public class BaseConstant {
public static final String PREFIX = "hash_";
public static final String ENCODE_KEY = "hashId";
public static final String DECODE_KEY = "id";

public static final String SUCCESS_CODE = "00000";
public static final String ILLEGAL_ARGUMENT = "00001";
}
用户信息UserInfo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserInfo {
private Long id;
private Long height;
private String nickName;
private String name;
private String email;
private List<Car> cars;
public UserInfo() {
}
public UserInfo(Long id, String name) {
this.id = id;
this.name = name;
}
// 省略 Get Set
}
车辆信息 Car

用来测试嵌套的 ID 是否会被加解密

1
2
3
4
5
public class Car {
private Long id;
private String name;
// 省略 Get Set
}
通用返回对象 BaseResponse
1
2
3
4
5
6
public class BaseResponse<T> {
private String code;
private String message;
private T data;
// 省略...
}
创建 UserController:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/list")
public BaseResponse userList(){
UserInfo zhangSan = new UserInfo(1L,"张三");
zhangSan.setHeight(178L);
UserInfo jack = new UserInfo(2L,"jack");
UserInfo james = new UserInfo(3L,"james");
jack.addCar(new Car(999L,"保时捷"));
jack.addCar(new Car(888L,"奥迪"));
List<UserInfo> users = new ArrayList<>();
users.add(zhangSan);
users.add(jack);
users.add(james);
return BaseResponse.success(users);
}
}
打开浏览器访问 http://localhost:8080/user/list 返回信息如下
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
{
"code": "00000",
"message": "成功",
"data": [
{
"id": "hash_1",
"height": "hash_178",
"nickName": null,
"name": "张三",
"email": null,
"cars": null
},
{
"id": "hash_2",
"height": null,
"nickName": null,
"name": "jack",
"email": null,
"cars": [
{
"id": "hash_999",
"name": "保时捷"
},
{
"id": "hash_888",
"name": "奥迪"
}
]
},
{
"id": "hash_3",
"height": null,
"nickName": null,
"name": "james",
"email": null,
"cars": null
}
]
}

可以看到返回信息 ID 已经变为 hash_1hash_2hash_3 并且Height车辆的 ID 也被自动加密。

优点:

使用者无感知,不需要添加任何注解,对代码无侵入,只需按规范使用即可,可以专注于业务逻辑。

不足:

  1. 如果使用 @JsonComponent 注解,将会把 Jackson 序列化注册为全局处理,其他返回前端的业务字段不能使用 Long 类型。

  2. 序列化方式为 Jackson,后续如果使用其他序列化方式则需要改动。

入参解密:使用 Filter + RequestWrapper

第一步:新增自定义Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class HttpServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new ParameterRequestWrapper((HttpServletRequest) request), response);
}
@Override
public void destroy() {
}
}

第二步:新增自定义 ParameterRequestWrapper

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
public class ParameterRequestWrapper extends HttpServletRequestWrapper {

private String bodyParams;

private Map<String, String[]> params;

public ParameterRequestWrapper(HttpServletRequest request) {
super(request);
initParameterMap(request);
initInputStream(request);
}
@Override
public String getParameter(String name) {
String result;
Object v = params.get(name);
if (v == null) {
result = null;
} else if (v instanceof String[]) {
String[] strArr = (String[]) v;
if (strArr.length > 0) {
result = strArr[0];
} else {
result = null;
}
} else if (v instanceof String) {
result = (String) v;
} else {
result = v.toString();
}
return result;
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
@Override
public Enumeration<String> getParameterNames() {
return new Vector<>(params.keySet()).elements();
}
@Override
public String[] getParameterValues(String name) {
String[] result;
Object v = params.get(name);
if (v == null) {
result = null;
} else if (v instanceof String[]) {
result = (String[]) v;
} else if (v instanceof String) {
result = new String[]{(String) v};
} else {
result = new String[]{v.toString()};
}
return result;
}
@Override
public ServletInputStream getInputStream() throws IOException{
return new PostServletInputStream(bodyParams);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}

/**
* 读取 输入流
*/
private void initInputStream(HttpServletRequest request) {
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
bodyParams = stringBuilder.toString();
}
/**
* 初始化参数 Map 处理 key1=value1&key2=value2 类型的传参
*/
private void initParameterMap(HttpServletRequest req) {
this.params = new HashMap<>(req.getParameterMap());
// 此处获取前端参数解密
String hashId = req.getParameter(BaseConstant.ENCODE_KEY);
if (hashId != null && hashId.length() > 0){
params.put(BaseConstant.DECODE_KEY, new String[]{HashIdUtils.decode(hashId)});
}
String queryString = req.getQueryString();
if (queryString != null && queryString.trim().length() > 0) {
String[] params = queryString.split("&");
for (int i = 0; i < params.length; i++) {
int splitIndex = params[i].indexOf("=");
if (splitIndex == -1) {
continue;
}
String key = params[i].substring(0, splitIndex);
if (!this.params.containsKey(key)) {
if (splitIndex < params[i].length()) {
String value = params[i].substring(splitIndex + 1);
this.params.put(key, new String[]{value});
}
}
}
}
}
}

第三步:重写输入流

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
public class PostServletInputStream  extends ServletInputStream {
private InputStream inputStream;
/** 解析json之后的文本 */
private String body ;

public PostServletInputStream(String body) throws IOException {
this.body=body;
inputStream = null;
}
private InputStream acquireInputStream() throws IOException {
if(inputStream == null) {
if (body != null && body.contains(BaseConstant.ENCODE_KEY)){
ObjectMapper mapper = new ObjectMapper();
HashMap hashMap = mapper.readValue(body, HashMap.class);
Object o = hashMap.get(BaseConstant.ENCODE_KEY);
if (o != null){
try{
String decode = HashIdUtils.decode(o.toString());
hashMap.put(BaseConstant.DECODE_KEY, decode);
} catch (Exception e){
//ignore this exception
}
}
body = mapper.writeValueAsString(hashMap);
}
//通过解析之后传入的文本生成inputStream以便后面control调用
inputStream = new ByteArrayInputStream(body.getBytes());
}
return inputStream;
}
@Override
public void close() throws IOException {
try {
if(inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
throw e;
} finally {
inputStream = null;
}
}
@Override
public int read() throws IOException {
return acquireInputStream().read();
}
@Override
public boolean markSupported() {
return false;
}
@Override
public synchronized void mark(int i) {
throw new UnsupportedOperationException("mark not supported");
}
@Override
public synchronized void reset() throws IOException {
throw new IOException(new UnsupportedOperationException("reset not supported"));
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
}

测试代码:

UserController 新增两个方法如下
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
/**
* 前端传入加密 id 字段名统一为 hashId
* 后端接收参数名为 hashId 则接收未解码的 hashId.后端参数为 id 则接收解码的 id;
* @param hashId UserController#userList() 接口返回的 id
*/
@RequestMapping("/info")
public BaseResponse queryUserInfo(@RequestParam(value = "hashId",required = true) String hashId,
@RequestParam(value = "id") Long id){
System.out.println("queryUserInfo hashId = " + hashId);
System.out.println("queryUserInfo id = " + id);
UserInfo info = new UserInfo();
info.setId(id);
info.setName("张三");
info.setNickName("法外狂徒"+id);
info.setEmail("zhangsan@gmail.com");
return BaseResponse.success(info);
}

@RequestMapping("/query")
public BaseResponse queryUser(@RequestBody UserInfo user){
System.out.println("queryUser id = " + user.getId());
if (user.getId() == null){
return new BaseResponse(BaseConstant.ILLEGAL_ARGUMENT,"用户 id 为空!");
}
UserInfo info = new UserInfo();
info.setId(user.getId());
info.setHeight(178L);
info.setName("张三");
info.setNickName("法外狂徒");
info.setEmail("zhangsan@gmail.com");
info.addCar(new Car(333L,"大众"));
return BaseResponse.success(info);
}
使用 Postman 测试 http://localhost:8080/user/query 接口,传入参数
1
2
3
4
{
"hashId": "hash_1",
"name": "张三"
}

后台输出日志:

1
queryUser id = 1
使用 Postman 调用 http://localhost:8080/user/info?hashId=hash_2

后台输出日志

1
2
queryUserInfo hashId = hash_2
queryUserInfo id = 2

前端收到返回值:

1
2
3
4
5
6
7
8
9
10
11
12
{
"code": "00000",
"message": "成功",
"data": {
"id": "hash_2",
"height": null,
"nickName": "法外狂徒2",
"name": "张三",
"email": "zhangsan@gmail.com",
"cars": null
}
}

优点:

对业务代码无任何侵入。业务编写者无感知。

不足:

  1. 前端传入数据限制为 Json 和 key-value 类型
  2. 如果前端传入多个需要解密的参数,目前方案需要修改
  3. 目前方案不支持解密前端传入的嵌套 Json

方案二:使用者需添加一个注解

1、对返回前端需要编码的字段加注解 @JsonSerialize(using = CustomeJackSon.Serialize.class)
2、(可选)对前端传入需要解码的字段加注解 @JsonDeserialize(using = CustomeJackSon.Deserializer.class),添加该注解可为任意 Long 类型的字段,字段名不需按照第三条定义
3、前端传入加密 id 字段名统一为 hashId(可配置 也可以为 id 但是这样就无法区分传入的是加密的还是未加密的 id),
后端参数 为 hashId 则接受未解码的 hashId,后端参数为 id 则接收解码的 id;

在方案一基础上修改自定义 Jackson 序列化如下:

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
/**不加 @JsonComponent 注解 
* 使用者自行加 @JsonSerialize(using = CustomeJackSon.Serialize.class)
* 和 @JsonDeserialize(using = CustomeJackSon.Deserializer.class)
*/
public class CustomeJackSon {

public static class Serialize extends JsonSerializer<Long> {
@Override
public void serialize(Long id, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(HashIdUtils.encode(id));
}
}

public static class Deserializer extends JsonDeserializer<Long> {
@Override
public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String id = jsonParser.getValueAsString();
if (id != null){
try {
return HashIdUtils.decode2Long(id);
} catch (NumberFormatException e) {
throw new JsonParseException(jsonParser, id, e);
}
}
return null;
}
}
}

两种方案对比

  • 方案一按照规范来使用,对业务代码无任何侵入,并且在开发业务时对加解密毫无感知。

    • 不足之处为其他字段类型不可使用 Long 类型,但是经过系统中分析,发现其他字段基本不存在 Long 类型字段。
    • 不支持前端传入嵌套 Json 的解密,目前前端参数无嵌套 Json
    • 不支持前端传入多个加密字段的解密,可修改方案为迭代 Json 的 key ,如果存在加密标识则解密。
  • 方案二需添加注解,会对代码产生一定的侵入性,但是自由度比较大。

代码示例

本文的完整工程可以查看下面仓库中的spring-boot-2.x-samples/spring-boot-samples-jackson目录:

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

请作者喝杯咖啡吧