性能与安全的权衡
对数据库Id字段的设计,各种五花八门的都有。
- Long类型Id
数据库查询,数值比字符串性能高,正常来说会把主键设计为自增的Long类型,其他表关联也通过该字段来关联,但字段暴露到前端去,用户可以通过简单的修改id获取刷取整站的记录,安全性上存在一定的问题。
- 字符类型Id
使用字符串如uuid之类的做主键,字段安全属性,但查询效率低下。
如何做到安全与高效兼得呢。
思路
在接口层面将所有id在输出到前端的时候自动做加密,前端提交过来的时候,再自动解密处理成数值类型。
这样是不是就能满足要求了。
- 对前端来说是一个String,顺便解决了大整数javascript无法处理的问题。
- 对服务端业务层来说,因框架层面做了解密处理,看到的永远是Long类型的。
需要处理的请求信息包括,query、body、header及respnose里面的id字段
方案
对于互联网来说,任何问题都可以加入一个虚拟的层来解决。
本问题的层,可以在api层,可以在存储层。
- 存储层
所有Id字段在数据库中是Long类型,但在代码模型上定义为String类型,对DAO做一层封装,统一对Id进行转换,如where id =“xxxxx” 会变成 where id = 111 ,where id in (“xxx”,“yyy”) 变为 where id in(111,222),查询出来的id,在装载模型的时候,自动进行加密处理。
但这存在一个问题,代码中的所有id数据都是加密的,管理后台根C端都会被加密
- 雪花算法
类型还是Long,值空间也挺大,只要原样通过ToStringSerializer解析成字符串给前端,就能解决javascript大整数的问题。query参数转化问题,也天然支持。
- api层
spring mvc的扩展性足够好,我们可以通过对C端的api进行扩展来满足需求。
实现细节
本文只对API层的处理,做介绍。
为了实现简单对系统做了如下约束:
字段或变量名为id或Id结尾的,Long类型的id字段的充要条件。
id不会在header中出现,即我们可以不用处理header
定义个序列化器
重写serialize方法,从gen参数中,可以获取到对应字段的名称
对接口的输出参数识别id字段,并对其进行加密处理,否则原样输出
加密逻辑可以任意替换
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
public class LongStringSerializer extends ToStringSerializerBase {
public static final LongStringSerializer instance = new LongStringSerializer();
public LongStringSerializer() {
super(Object.class);
}
public LongStringSerializer(Class<?> handledType) {
super(handledType);
}
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if(gen.getOutputContext().getCurrentName()!=null
&& (gen.getOutputContext().getCurrentName().equals("id") ||
gen.getOutputContext().getCurrentName().endsWith("Id"))){
super.serialize(value, gen, provider);
}
else{
gen.writeNumber((Long)value);
}
}
public final String valueToString(Object value) {
if(value == null ){
return null;
}
return CryptUtils.encrypt(value + "", "idEn");
}
}定义一个反序列化器
覆盖deserialize方法,从jsonParser参数中可获取字段的名称
判断id字段,并对其进行解密操作
解密方法需跟加密方法配套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LongStringDeSerializer extends JsonDeserializer<Long> {
public static final LongStringDeSerializer instance = new LongStringDeSerializer();
public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
if (jsonParser.getCurrentName() == null
|| jsonParser.getCurrentName().equals("id")
|| jsonParser.getCurrentName().endsWith("Id")) {
return (Long.parseLong(CryptUtils.decrypt(UriEncoder.decode(jsonParser.getText()), "idEn")));
} else {
return Long.parseLong(jsonParser.getText());
}
}
}定义一个Long类型解析器
这个解析器主要用来解析请求参数
处理Long类型即Search对象类型
调用上面的 LongStringDeSerializer对字段进行解密处理
这里暂时只处理了满足项目需求的场景,更多场景需自行细化
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
62public class LongArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Long.class) || SearchBase.class.isAssignableFrom(parameter.getParameterType());
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(Long.class, LongStringDeSerializer.instance);
simpleModule.addDeserializer(Long.TYPE, LongStringDeSerializer.instance);
objectMapper.registerModule(simpleModule);
if (parameter.getParameterType().equals(Long.class)) {
var query = ((ServletWebRequest) webRequest).getRequest().getQueryString();
var map = getQueryParams(query);
var data = UriEncoder.decode(map.entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null));
if (Longs.tryParse(data) != null) {
return data;
}
return objectMapper.readValue(JSON.toJSONString(data)
, parameter.getParameterType());
}
if (SearchBase.class.isAssignableFrom(parameter.getParameterType())) {
if (((ServletWebRequest) webRequest).getHttpMethod().equals(HttpMethod.GET)) {
var query = ((ServletWebRequest) webRequest).getRequest().getQueryString();
var map = getQueryParams(query);
return objectMapper.readValue(
JSON.toJSONString(map), parameter.getParameterType());
}
}
return null;
}
/**
* Retrieve the query parameters from given url
*
* @return params Map with query parameters
* @throws IOException
*/
static public Map<String, String> getQueryParams(String query)
throws IOException {
Map<String, String> params = new HashMap<String, String>();
if (query == null) {
return params;
}
Arrays.stream(query.split("&")).forEach(e -> {
var token = e.split("=");
if (token.length == 2) {
params.put(token[0], token[1]);
}
});
return params;
}
}
修改Mvc配置
- 添加HttpMessageConverter,将LongStringSerializer及LongStringDeSerializer配置为Long类型的序列化处理器
- 添加HandlerMethodArgumentResolver,将LongArgumentResolver配置为请求参数的处理器
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
public class InterceptorConfig implements WebMvcConfigurer {
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = objectMapper();
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
converters.add(jackson2HttpMessageConverter);
}
ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, LongStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, LongStringSerializer.instance);
simpleModule.addDeserializer(Long.class, LongStringDeSerializer.instance);
simpleModule.addDeserializer(Long.TYPE, LongStringDeSerializer.instance);
objectMapper.registerModule(simpleModule);
return objectMapper;
}
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new LongArgumentResolver());
}
};
}
}