Spring Data MongoDB:如何防止@CreatedBy与@CreatedDate字段被更新?
解决Spring Boot MongoDB审计字段(CreatedBy/CreatedDate)不可篡改且返回正确值的问题
我理解你遇到的问题:继承AuditingDocument的实体在通过REST API更新时,创建审计字段(creator/created)可能被篡改,而且自定义MongoTemplate后数据库数据正确,但save方法返回的对象中这些字段为null。下面给出一套完整的解决方案,既能保护审计字段不被更新,又能让save返回包含正确字段值的对象。
核心思路
- 拦截更新操作,过滤受保护字段:通过MongoDB事件监听器,在执行更新前移除
creator、created等不允许修改的字段,确保这些字段不会被请求中的值覆盖。 - 返回更新后的最新文档:修改
MongoTemplate的save方法,使用findAndModify执行更新并返回数据库中最新的文档,解决返回对象字段为null的问题。
具体实现
1. 创建审计字段保护的事件监听器
这个监听器会在所有MongoDB更新操作执行前触发,过滤掉我们不允许修改的审计字段:
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; import org.springframework.data.mongodb.core.mapping.event.BeforeUpdateEvent; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Component; @Component public class AuditingFieldProtectionListener extends AbstractMongoEventListener<Object> { // 定义受保护的字段:创建审计字段+乐观锁版本号 private static final String[] PROTECTED_FIELDS = {"creator", "created", "version"}; @Override public void onBeforeUpdate(BeforeUpdateEvent<Object> event) { Update filteredUpdate = new Update(); // 遍历原更新请求中的所有字段,仅保留非受保护的字段 event.getUpdate().getUpdateObject().forEach((key, value) -> { boolean isProtected = false; for (String field : PROTECTED_FIELDS) { if (field.equals(key)) { isProtected = true; break; } } if (!isProtected) { filteredUpdate.set(key, value); } }); // 将过滤后的更新替换原更新,确保受保护字段不会被修改 event.setUpdate(filteredUpdate); } }
2. 自定义MongoTemplate,返回更新后的最新文档
默认的MongoTemplate.save方法在更新时会返回传入的实体对象(可能缺失审计字段),我们重写该方法,使用findAndModify来执行更新并返回数据库中最新的文档:
import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.MongoDatabaseFactory; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.FindAndModifyOptions; import org.springframework.util.Assert; public class CustomMongoTemplate extends MongoTemplate { public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory) { super(mongoDbFactory); } public CustomMongoTemplate(MongoDatabaseFactory mongoDbFactory, MongoConverter mongoConverter) { super(mongoDbFactory, mongoConverter); } @Override public <T> T save(T objectToSave) { return save(objectToSave, getCollectionName(objectToSave.getClass())); } @Override public <T> T save(T objectToSave, String collectionName) { Assert.notNull(objectToSave, "Object to save must not be null!"); Object id = getId(objectToSave); if (id == null) { // 插入操作:直接调用父类方法,审计字段会自动填充 return super.save(objectToSave, collectionName); } else { // 更新操作:使用findAndModify返回最新的文档 Query query = Query.query(Criteria.where("_id").is(id)); Update update = Update.fromDocument(getConverter().write(objectToSave, new Document())); // 配置选项:返回更新后的文档 FindAndModifyOptions options = FindAndModifyOptions.options().returnNew(true); return findAndModify(query, update, options, (Class<T>) objectToSave.getClass(), collectionName); } } }
3. 注册自定义MongoTemplate
在配置类中替换默认的MongoTemplate Bean:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.convert.MongoConverter; @Configuration public class MongoConfig { @Bean public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDbFactory, MongoConverter mongoConverter) { return new CustomMongoTemplate(mongoDbFactory, mongoConverter); } }
方案验证
- 创建文档:首次调用
save时,审计字段(creator/created/version/modifier/modified)会被正确填充,返回的对象包含所有字段值。 - 更新文档:
- 即使请求中传入
creator或created的新值,这些字段也不会被更新到数据库。 save方法返回的对象会从数据库加载最新数据,包含正确的creator和created字段值。
- 即使请求中传入
- 通用性:所有继承
AuditingDocument的@Document实体都会自动应用这个保护逻辑,无需逐个修改。
为什么你的原方案会出现返回值为null的问题
你之前的CustomMongoTemplate.doUpdate方法虽然保证了数据库中的字段不被覆盖,但save方法默认返回的是传入的实体对象(而该对象中creator/created可能为null),不是数据库中更新后的文档。使用findAndModify并配置returnNew(true)可以直接获取更新后的最新文档,完美解决这个问题。
内容的提问来源于stack exchange,提问作者1Z10
相关产品推荐
相关产品推荐

