引言:
最近有个需求:后端所有数据的 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
| @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)); } } }
|
测试代码:
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, "")); } }
|
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"; }
|
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; } }
|
用来测试嵌套的 ID 是否会被加解密
1 2 3 4 5
| public class Car { private Long id; private String name; }
|
1 2 3 4 5 6
| public class BaseResponse<T> { private String code; private String message; private T data; }
|
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_1
、hash_2
、hash_3
并且Height
车辆的 ID
也被自动加密。
优点:
使用者无感知,不需要添加任何注解,对代码无侵入,只需按规范使用即可,可以专注于业务逻辑。
不足:
如果使用 @JsonComponent 注解,将会把 Jackson 序列化注册为全局处理,其他返回前端的业务字段不能使用 Long 类型。
序列化方式为 Jackson,后续如果使用其他序列化方式则需要改动。
入参解密:使用 Filter
+ RequestWrapper
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() { } }
|
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(); }
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; 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){ } } body = mapper.writeValueAsString(hashMap); } 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(); } }
|
测试代码:
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
|
@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 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 } }
|
优点:
对业务代码无任何侵入。业务编写者无感知。
不足:
- 前端传入数据限制为 Json 和 key-value 类型
- 如果前端传入多个需要解密的参数,目前方案需要修改
- 目前方案不支持解密前端传入的嵌套 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
|
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; } } }
|
两种方案对比
代码示例
本文的完整工程可以查看下面仓库中的spring-boot-2.x-samples/spring-boot-samples-jackson
目录:
如果您觉得本文不错,欢迎Star
支持,您的关注是我坚持的动力!