导言
不知道大家在网上购物的时候,有没有这样的念头,如果能把未付款的订单偷偷用一条SQL改成已付款,该多么美好啊。那么在实际开发过程中,我们应当如何保证数据库里的数据在保存后不会被偷偷更改?
理论
在介绍具体的内容之间,先介绍MD5算法,简单的来说,MD5能把任意大小、长度的数据转换成固定长度的一串字符,经常玩大型游戏的朋友应该都注意到过,各种补丁包、端游客户端之类的大型文件一般都附有一个MD5值,用于确保你下载文件的完整性。那么在这里,我们可以借鉴其思想,对订单的某些属性进行加密计算,得出来一个
MD5值一并保存在数据库当中。从数据库取出数据后第一时间进行校验,如果有异常更改,那么及时抛出异常进行人工处理。
实现
道理我都懂,但是我要如何做呢,别急,且听我一一道来。
这种需求听起来并不强绑定于某个具体的业务需求,这就要用到了我们熟悉的鼎鼎有名的AOP(面向切面编程)来实现。
首先定义四个类型的注解作为AOP的切入点。@Sign和@Validate都是作用在方法层面的,分别用于对方法的入参进行加签和验证方法的返回值的签名。
@SignField用于注解关键的不容篡改的字段。@ValidateField用于注解保存计算后得出的签名值。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public
@interface Sign { } 复制代码 @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) public @interface Validate { } 复制代码
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public
@interface SignField { } 复制代码 @Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME) public @interface ValidField { } 复制代码
以订单的实体为例 sn,amt,status,userId就是关键字段,绝不能允许有人在落单到数据库后对这些字段偷偷篡改。
public class Order { @SignField private String sn; @SignField private String
amt; @SignField private int status; @SignField private int userId; @ValidField
private String sign; } 复制代码
下面就到了重头戏的部分,如何通过AOP来进行实现。
1. 定义切入点
@Pointcut("execution(@com.example.demo.annotations.Sign * *(..))") public void
signPointCut() { } @Pointcut("execution(@com.example.demo.annotations.Validate
* *(..))") public void validatePointCut() { } 复制代码
2.环绕切入点
@Around("signPointCut()") public Object signAround(ProceedingJoinPoint pjp)
throws Throwable { Object[] args = pjp.getArgs(); for (Object o : args) {
System.out.println(o); sign(o); } Object res = pjp.proceed(args); return res; }
@Around("validatePointCut()") public Object validateAround(ProceedingJoinPoint
pjp) throws Throwable { Object[] args = pjp.getArgs(); Object res =
pjp.proceed(args); valid(res); return res; } 复制代码
3. 签名的实现
1.获取需要签名字段
private Map<String, String> getSignMap(Object o) throws IllegalAccessException
{ Map<String, String> fieldNameToValue = new HashMap<>(); for (Field f :
o.getClass().getDeclaredFields()) { System.out.println(f.getName()); for
(Annotation annotation : f.getDeclaredAnnotations()) { if
(annotation.annotationType().equals(SignField.class)) { String value = "";
f.setAccessible(true); fieldNameToValue.put(f.getName(), f.get(o).toString());
} } } return fieldNameToValue; } 复制代码
2.计算出签名值,这里在属性名和属性值以外加入了我的昵称以防止他人猜测,同时使用了自定义的分隔符来加强密码强度。
private String getSign(Map<String, String> fieldNameToValue) { List<String>
names = new ArrayList<>(fieldNameToValue.keySet()); StringBuilder sb = new
StringBuilder(); for (String name : names)
sb.append(name).append("@").append(fieldNameToValue.get(name));
System.out.println(sb.append("日暮与星辰之间").toString()); String signValue =
DigestUtils.md5DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8));
return signValue; } 复制代码
* 找到保存签名的字段 private Field getValidateFiled(Object o) { for (Field f :
o.getClass().getDeclaredFields()) { for (Annotation annotation :
f.getDeclaredAnnotations()) { if
(annotation.annotationType().equals(ValidField.class)) { return f; } } } return
null; } 复制代码
* 对保存签名的字段进行赋值 public void sign(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o); if
(fieldNameToValue.isEmpty()) { return; } Field validateField =
getValidateFiled(o); if (validateField == null) return; String signValue =
getSign(fieldNameToValue); validateField.setAccessible(true);
validateField.set(o, signValue); } 复制代码
* 对从数据库中取出的对象进行验证 public void valid(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o); if
(fieldNameToValue.isEmpty()) { return; } Field validateField =
getValidateFiled(o); validateField.setAccessible(true); String signValue =
getSign(fieldNameToValue); if (!Objects.equals(signValue,
validateField.get(o))) { throw new RuntimeException("数据非法"); } } 复制代码
使用示例
对将要保存到数据库的对象进行签名
@Sign public Order save( Order order){ orderList.add(order); return order; }
复制代码
验证从数据库中取出的对象是否合理
@Validate public Order query(@ String sn){ return orderList.stream().filter(e
-> e.getSn().equals(sn)).findFirst().orElse(null); }