Apache Solr特定字段加解密实现遇阻,求成熟解决方案
现有方案问题分析
方案一(自定义SearchComponent)问题
自定义解密组件中responseBuilder.getResponseDocs()返回null,核心原因是调用时机过早:prepare/process阶段Solr尚未生成SolrDocumentList,且未正确从DocList中提取文档并转换为可修改的SolrDocument。
方案二(自定义分析器过滤器)问题
分析器中的加密过滤器仅作用于分词后的索引token,不会加密存储原始字段内容,这与“索引时加密数据”的需求不符;同时查询时/select接口未触发解密逻辑,大概率是因为字段未绑定该自定义类型,或查询分析器的执行顺序未正确匹配。
经过验证的成熟实现方案
方案一:UpdateRequestProcessor + 自定义ResponseWriter(推荐,支持全文搜索+存储加密)
该方案分离索引加密与搜索解密逻辑,完全符合“索引时加密存储、搜索时解密返回”的需求,且不影响字段的全文搜索能力。
1. 索引阶段:加密存储字段
实现自定义UpdateRequestProcessorFactory,在文档写入索引前加密content字段:
package com.emorphis.solr.processor; import com.emorphis.solr.encrypt.util.EncryptUtil; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.update.UpdateRequestProcessor; import org.apache.solr.update.UpdateRequestProcessorFactory; import java.io.IOException; public class EncryptionUpdateProcessorFactory extends UpdateRequestProcessorFactory { @Override public UpdateRequestProcessor getInstance(SolrQueryRequest req, SolrQueryResponse rsp, UpdateRequestProcessor next) { return new UpdateRequestProcessor(next) { @Override public void addDoc(SolrInputDocument doc) throws IOException { Object contentVal = doc.getFieldValue("content"); if (contentVal != null && contentVal instanceof String) { String encryptedContent = EncryptUtil.encrypt((String) contentVal); doc.setField("content", encryptedContent); } super.addDoc(doc); } }; } }
在solrconfig.xml中配置处理器链:
<updateRequestProcessorChain name="encryption-chain"> <processor class="com.emorphis.solr.processor.EncryptionUpdateProcessorFactory"/> <processor class="solr.RunUpdateProcessorFactory"/> </processor> <requestHandler name="/update" class="solr.UpdateRequestHandler"> <lst name="defaults"> <str name="update.chain">encryption-chain</str> </lst> </requestHandler>
2. 搜索阶段:解密返回字段
实现自定义ResponseWriter,在生成响应前解密content字段:
package com.emorphis.solr.writer; import com.emorphis.solr.encrypt.util.EncryptUtil; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.response.TextResponseWriter; import java.io.IOException; public class DecryptionResponseWriter extends TextResponseWriter { @Override public void write(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { SolrDocumentList results = rsp.getResults(); if (results != null) { for (SolrDocument doc : results) { Object encryptedContent = doc.getFieldValue("content"); if (encryptedContent instanceof String) { String decryptedContent = EncryptUtil.decrypt((String) encryptedContent); doc.setField("content", decryptedContent); } } } super.write(req, rsp); } @Override public String getContentType(SolrQueryRequest req, SolrQueryResponse rsp) { return "application/json"; // 可根据需求改为xml/csv等类型 } }
在solrconfig.xml中配置响应处理器:
<queryResponseWriter name="decrypt-json" class="com.emorphis.solr.writer.DecryptionResponseWriter"/>
搜索时指定使用该处理器:
/select?q=*:*&wt=decrypt-json
也可将其设为默认响应处理器,避免每次查询指定参数。
方案二:Solr官方EncryptedField(适用于仅存储加密,无需全文搜索场景)
如果仅需要对字段进行存储加密,不需要基于明文做全文搜索,可使用Solr 8.10+官方提供的EncryptedField类型,无需自定义代码:
<fieldType name="encrypted_string" class="solr.EncryptedField" keyStore="/path/to/your/keystore.jks" keyStorePassword="your_keystore_pass" keyAlias="your_key_alias" keyPassword="your_key_pass"/> <field name="content" type="encrypted_string" stored="true" indexed="false"/>
该方案会自动处理存储加密与查询解密,但indexed="false"意味着无法对该字段做全文搜索。
现有方案修复建议
方案一(自定义SearchComponent)修复
调整解密逻辑到QUERY_FINISHED阶段,并从DocList中提取文档转换为SolrDocument:
@Override public void finishStage(ResponseBuilder rb) { if (rb.stage == ResponseBuilder.STAGE_QUERY_FINISHED) { DocList docList = rb.getResults().docList; if (docList == null || docList.size() == 0) return; SolrIndexSearcher searcher = rb.req.getSearcher(); SolrDocumentList solrDocs = new SolrDocumentList(); solrDocs.setNumFound(docList.getNumFound()); solrDocs.setStart(docList.offset()); DocIterator iter = docList.iterator(); while (iter.hasNext()) { int docId = iter.nextDoc(); float score = iter.score(); try { SolrDocument doc = searcher.doc(docId, rb.getFieldFlags()); doc.setField("score", score); // 解密content字段 Object encryptedContent = doc.getFieldValue("content"); if (encryptedContent instanceof String) { String decrypted = EncryptUtil.decrypt((String) encryptedContent); doc.setField("content", decrypted); } solrDocs.add(doc); } catch (IOException e) { // 异常处理逻辑 } } rb.setResponseDocs(solrDocs); } super.finishStage(rb); }
同时在solrconfig.xml中将解密组件添加到查询组件链的末尾:
<searchComponent name="decryption" class="com.emorphis.solr.search.component.MyDecryptionSearchComponent"/> <requestHandler name="/select" class="solr.SearchHandler"> <arr name="components"> <str>query</str> <str>facets</str> <str>decryption</str> </arr> </requestHandler>
方案二(自定义分析器过滤器)修复
若需对索引token加密(仅加密索引内容,原始字段仍为明文),需确保:
content字段绑定encrypted_content_text类型- 查询分析器中解密过滤器置于tokenizer之后、其他过滤器之前
- 明确该方案仅加密索引token,不加密存储原始字段,不符合“存储加密数据”的核心需求
内容的提问来源于stack exchange,提问作者Lalit Dashora

