关于Solr4.4Cloud Admin Page登录认证的源码修改与工程编译

摘要:在工作中需要对与Solr 服务进行追加认证登录的功能,本篇博客用来记录自己对于Solr4.4版本的登录认证的源码修改和源码工程编译过程

一、源码下载及Solr源码工程编译


1.1 源码下载


 先到Apachee Solr 官网或者其他途径下载Solr源码,由于生产环境使用的是Solr4.4版本,故此处我下载的是Solr4.4源码包








1.2 Solr工程生成


1.2.1 构建环境准备

 下载到本地之后,通过解压软件解压。我是解压到F:\javazipfile目录下,官方默认提供的源码包并不是一个标准的Eclipse Java Project,需要使用ivy进行构建,通过ivy的构建可以将下载下来的源码包转换成一个标准的Java Project,然后我们就能把Java Project import到我们的Eclipse中去了,然后你就能在Eclipse中自由的阅读源码并自己打包Solr和Lucene成Jar包啦。记住,构建命令是ant eclipse。不过前提是你需要事先准备好构建环境:

    1. 首先你需要安装ANT并配置好ANT的环境变量ANT_HOME
    1. 下载IVY并将ivy.jar复制到ANT_HOME\lib目录下
  • 3.下载并安装Maven以及配置好MAVEN_HOME环境变量,如果有必要的话,你还可以设置修改下默认的本地仓库位置;
  • 4.你需要下载一个Eclipse,由于Solr4.x对于JDK要求至少1.6+

1.2.2 配置ANT


 配置ANT_HOME

1
2
D:\DevDir\apache-ant-1.9.7-bin\apache-ant-1.9.7
解压文件夹路径,极其简单





 配置Path

1
%ANT_HOME%\bin

 如图:


 验证Ant是否生效:



 如图所示,执行了命令,即配置成功,显示失败是构建失败,属正常现象。

1.2.3 配置Ivy


 下载ivy,如图:



 下载完成后,解压ivy压缩包,然后复制ivy.jar包到你的ANT_HOME\lib目录下,如图:


1.2.4 生成Eclipse 工程


 打开命令行,进入你的Solr源码解压根目录,如图:



 此处我已经构建完成Eclipse项目,未构建时是如下图:


在此文件夹下运行命令:

1
ant eclipse

 即可执行构建过程,由于Solr的依赖包有ivy进行引入,故可能需要比较久的下载过程,甚至可能出现下载失败,请翻墙。结果如图:


 当初下载过程耗时71分钟。
 至此生成Solr的Eclipse工程完成,剩下即导入Eclipse的过程,不再赘述。导入成功的结果如图:


1.3 配置Eclipse开发编译环境


1.3.1 打包编译Solr源代码


 真正部署SolrCloud需要把源码打包成jar

 首先打开Eclipse中的Ant视图,如图过程:












 如果你的Eclipse没有安装ivy插件,则你执行jar命令后可能会得到如图这样的异常:


1.3.1 为Eclipse增加ivy插件


 无关此内容,MyEclipse请参考博客:http://blog.csdn.net/my491063321/article/details/18963747

二、SolrCloud前期配置准备


 在开启Solr服务后,我们发现其中并没有内置任何安全性相关检查,任何人如果知道了我们的外网地址就能直接访问并修改其中的索引。经过查找可以使用jetty的方式来限制web访问。碰巧我们是使用jetty方式来启动服务的。

  • 在solr/server/solr-webapp/WEB-INF/web.xml中增加以下字段,配置验证方式BASIC(用户名密码的方式)。:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<security-constraint>  
<web-resource-collection>
<web-resource-name>solr</web-resource-name>
<url-pattern>/</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>solr_admin</role-name>
<role-name>admin</role-name>
</auth-constraint>

<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Solr Admin</realm-name>
</login-config>
</security-constraint>
  • solr/server/etc/jetty.xml中增加Call标签:
1
2
3
4
5
6
7
8
9
10
11
<Call name="addBean">  
<Arg>
<New class="org.eclipse.jetty.security.HashLoginService">
<Set name="name">Solr Admin</Set>
<Set name="config">
/Users/mazhiqiang/develop/tools/solr-5.5.0/server/etc/realm.properties
</Set>
<Set name="refreshInterval">0</Set>
</New>
</Arg>
</Call>
  • config中指定密码文件的路径,可以在其中使用来共同组合路径,例如配置了环境变量的情况下,可以使用下面的方式:
1
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>
  • 指定的realm.properties就是相关密码文件了:
1
admin:xxxx,solr_admin
  • 设置完成,重新启动solr即可,如果不输入用户名和密码,无法登陆成功:


三、增加Solr认证代码


3.1 增加认证会影响SolrCloud的HTTP连接


 通过查看Log错误日志,可以看出出现了:

1
RemoteSolrException:Server at 10.1.XX.XX  returned non ok status: 401,message:UnAthorized

 通过跟踪异常出现点,我们可以看到,该异常在HttpSolrServer类中的大致392行抛出,属于以下方法抛出的异常:

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
public NamedList<Object> request(final SolrRequest request,
final ResponseParser processor) throws SolrServerException, IOException
{

.....
try {
// Execute the method.
final HttpResponse response = httpClient.execute(method);
int httpStatus = response.getStatusLine().getStatusCode();

// Read the contents
respBody = response.getEntity().getContent();

// handle some http level checks before trying to parse the response
switch (httpStatus) {
case HttpStatus.SC_OK:
case HttpStatus.SC_BAD_REQUEST:
case HttpStatus.SC_CONFLICT: // 409
break;
case HttpStatus.SC_MOVED_PERMANENTLY:
case HttpStatus.SC_MOVED_TEMPORARILY:
if (!followRedirects) {
throw new SolrServerException("Server at " + getBaseURL()
+ " sent back a redirect (" + httpStatus + ").");
}
break;
default:
throw new RemoteSolrException(httpStatus, "Server at " + getBaseURL()
+ " returned non ok status:" + httpStatus + ", message:"
+ response.getStatusLine().getReasonPhrase(), null);
//此处抛出异常
}


.......
}

 由此可知,在客户端solrj发起请求链接Solr的Http服务时,必须增加认证信息,如果不认证,则不予链接。

3.2 尝试设置认证信息


 我们知道Solr不同节点之间也需要相互通信,当节点设置完成认证信息时,之间通过Http通信也必然要携带认证信息,如果没有认证信息,自然会造成401未授权状态。我们可以看一下在Solrj中创建HttpSolrServer的服务代码,在下面这Java类中:/solr-4.4.0/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrServer.java

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
....

public HttpSolrServer(String baseURL, HttpClient client, ResponseParser parser) {
this.baseUrl = baseURL;
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
if (baseUrl.indexOf('?') >= 0) {
throw new RuntimeException(
"Invalid base url for solrj. The base URL must not contain parameters: "
+ baseUrl);
}

if (client != null) {
httpClient = client;
internalClient = false;
} else {
internalClient = true;
ModifiableSolrParams params = new ModifiableSolrParams();
params.set(HttpClientUtil.PROP_MAX_CONNECTIONS, 128);
params.set(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, 32);
params.set(HttpClientUtil.PROP_FOLLOW_REDIRECTS, followRedirects);
/*params.set(HttpClientUtil.PROP_BASIC_AUTH_USER, "admin");
params.set(HttpClientUtil.PROP_BASIC_AUTH_PASS, "password"); */

params.set(HttpClientUtil.PROP_MAX_CONNECTIONS, 1000);
params.set(HttpClientUtil.PROP_ALLOW_COMPRESSION, true);
params.set(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, 1000);
httpClient = HttpClientUtil.createClient(params);
}

this.parser = parser;
}
....

 猜想是否在此处进行配置认证信息即可完成HttpSolr服务节点之间的通信呢?
 于是如代码中,将认证信息,硬性编码进入代码中进行测试。在服务器中运行测试中,不在出现401错误,但是又抛出了其他异常,如下异常代码:

1
Replication for recovery failed.

 说明在主从模式下,主节点已经启动完成,但是zookeeper尝试启动子节点失败,说明仍有地方需要进行设置认证信息。

 查看抛出此异常代码位置,位于:/solr-4.4.0/solr/core/src/java/org/apache/solr/cloud/RecoveryStrategy.java

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
private void replicate(String nodeName, SolrCore core, ZkNodeProps leaderprops)
throws SolrServerException, IOException {


ZkCoreNodeProps leaderCNodeProps = new ZkCoreNodeProps(leaderprops);
String leaderUrl = leaderCNodeProps.getCoreUrl();

log.info("Attempting to replicate from " + leaderUrl + ". core=" + coreName);

// send commit
commitOnLeader(leaderUrl);

// use rep handler directly, so we can do this sync rather than async
SolrRequestHandler handler = core.getRequestHandler(REPLICATION_HANDLER);
if (handler instanceof LazyRequestHandlerWrapper) {
handler = ((LazyRequestHandlerWrapper) handler).getWrappedHandler();
}
ReplicationHandler replicationHandler = (ReplicationHandler) handler;

if (replicationHandler == null) {
throw new SolrException(ErrorCode.SERVICE_UNAVAILABLE,
"Skipping recovery, no " + REPLICATION_HANDLER + " handler found");
}

ModifiableSolrParams solrParams = new ModifiableSolrParams();
solrParams.set(ReplicationHandler.MASTER_URL, leaderUrl);

if (isClosed()) retries = INTERRUPTED;
boolean success = replicationHandler.doFetch(solrParams, false);

if (!success) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Replication for recovery failed.");//抛出的异常
}

.......

}

 往上层跟踪代码:->ReplicationHandler.doFetch->SnapPuller.fetchLatestIndex->SnapPuller.SnapPuller()构造方法->
SnapPuller.createHttpClient->

1
2
3
4
5
6
7
8
9
10
11
12
private static synchronized HttpClient createHttpClient(String connTimeout, String readTimeout, String httpBasicAuthUser, String httpBasicAuthPassword, boolean useCompression) {

....
final ModifiableSolrParams httpClientParams = new ModifiableSolrParams();

....

HttpClient httpClient = HttpClientUtil.createClient(httpClientParams);//生成HttpClient,关键!

....

}

 我们继续跟踪代码到HttpClientUtil.createClient(httpClientParams)
 其中会用到HttpClientConfigure类中的如下代码:

1
2
3
4
final String basicAuthUser = config  
.get(HttpClientUtil.PROP_BASIC_AUTH_USER);
final String basicAuthPass = config
.get(HttpClientUtil.PROP_BASIC_AUTH_PASS);

 通过配置类获取认证信息。通过简单阅读代码,这样能轻易得出一个结论:

 如果在SolrParams参数(ModifiableSolrParams的父类)中没有设置认证信息,则生成的HttpClient同样无法携带认证信息,就无法与SolrServer进行通信。

 所以需要设置认证信息,如果没有设置则会出现无法连接。带如果每次都在设置ModifiableSolrParams参数的地方增加认证信息的话要修改的地方太多,所以转变思路,从配置文件中读取认证信息,静态初始化进ModifiableSolrParams该类的构造函数,这样每次生成该参数类,即可将认证信息一并加入。尽量与其他Java类解耦。

3.3 进行可配置文件设置


 做法其实相对比较粗暴,增加静态代码块,读取xml配置文件,并在ModifiableSolrParams的构造函数中将其放进Map中,即可。这边遇到一个问题,就是原本设置成properties文件,但是将代码编译打包成jar包之后一直无法读取文件流信息。不知道为什么。所以改用xml文件。

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
//规定的权限用户配置文件名称
private final static String PROPERTY_FILE_NAME_XML = "authorization.xml";

public static String[] SOLR_ADMIN = null;
public static String[] SOLR_PASSWORD = null;

static{
/**
* 配置的认证xml结构
* <root>
* <admin>account<admin>
* <password>password</password>
* </root>
*/

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
//获取jar当前运行路径
String jarAbsolutePath = System.getProperty("user.dir");
String propFilePath = jarAbsolutePath + "/solr-webapp/webapp/WEB-INF/" + PROPERTY_FILE_NAME_XML;
log.info("XML配置文件路径:"+propFilePath);
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(propFilePath);//传入文件名可以是相对路径也可以是绝对路径
NodeList rootList = document.getElementsByTagName("root");
for (int i = 0; i < rootList.getLength(); i++) {
Node userInfo = rootList.item(i);
NodeList childNodes = userInfo.getChildNodes();
for (int k = 0; k < childNodes.getLength(); k++) {
if(childNodes.item(k).getNodeType() == Node.ELEMENT_NODE){
if(childNodes.item(k).getNodeName() != null && "admin".equals(childNodes.item(k).getNodeName())){
SOLR_ADMIN = childNodes.item(k).getNodeName().split(",");
log.info("Admin用户:"+SOLR_ADMIN);
}
if(childNodes.item(k).getNodeName() != null && "password".equals(childNodes.item(k).getNodeName())){
SOLR_PASSWORD = childNodes.item(k).getNodeName().split(",");
log.info("Admin用户密码:"+SOLR_PASSWORD);
}
}
}
}
} catch (ParserConfigurationException e) {
throw new RuntimeException();
} catch (SAXException e) {
throw new RuntimeException();
} catch (IOException e) {
throw new RuntimeException();
}
}

/*
*之后再构造函数中将认证信息加入Map中
*/

private Map<String,String[]> vals;

public ModifiableSolrParams()
{

// LinkedHashMap so params show up in CGI in the same order as they are entered
vals = new LinkedHashMap<String, String[]>();
setAuthorization();
}

/** Constructs a new ModifiableSolrParams directly using the provided Map&lt;String,String[]&gt; */
public ModifiableSolrParams( Map<String,String[]> v )
{

vals = v;
setAuthorization();
}

/** Constructs a new ModifiableSolrParams, copying values from an existing SolrParams */
public ModifiableSolrParams(SolrParams params)
{

vals = new LinkedHashMap<String, String[]>();
if( params != null ) {
this.add( params );
}
setAuthorization();
}

private void setAuthorization(){
//vals Map中不存在用户或者其密码,则进行设置认证权限
if(vals.get(HttpClientUtil.PROP_BASIC_AUTH_USER) == null || vals.get(HttpClientUtil.PROP_BASIC_AUTH_PASS) == null){
log.info("ModifiableSolrParams设置认证权限");
vals.put(HttpClientUtil.PROP_BASIC_AUTH_USER, SOLR_ADMIN);
vals.put(HttpClientUtil.PROP_BASIC_AUTH_PASS, SOLR_PASSWORD);
}else{//如果已经存在任意一个,无须操作
log.info("ModifiableSolrParams已经设置过权限");
log.info("设置权限用户名:"+ModifiableSolrParams.SOLR_ADMIN[0]);
log.info("设置权限用户名:"+ModifiableSolrParams.SOLR_PASSWORD[0]);
}
}

 通过以上步骤即可简单配置认证信息。但仍然比较粗糙,需要后面不断修改。只在代码和配置文件配置认证信息的这种做法在我看来就是自欺欺人,如果没有搭配传输层上的SSL协议,这种做法仍然对于攻击者是透明的。在Solr5.0之后有了SSL支持,使用Solr5会比较好。