Openfire系列之管理员登录

摘要:主要分析Openfire管理员后台无法登陆问题

问题

在安装Openfire后,讨论群里经常有人说初始化后在管理员页面输入帐号admin和密码admin后无法登陆到服务器,或者更改了某些配置导致无法登录,如下:


图:登录错误

原因

可能的原因有多种情况,需要一步步分析,通常有以下可能:

  1. 权限问题(帐号不是管理员、帐号密码错误、限制IP等)
  2. 服务器域与管理员域不一致(这种情况通常是修改域后导致)
  3. 重载了provider问题(有用户不是在官网上下载openfire安装后出现这类问题)

分析

首先需要了解加载管理员帐号过程,登录页源码在openfire_src\src\web\login.jsp文件中,认证代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Check that a username was provided before trying to verify credentials
if (loginUsername != null) {
if (LoginLimitManager.getInstance().hasHitConnectionLimit(loginUsername, request.getRemoteAddr())) {
throw new UnauthorizedException("User '" + loginUsername +"' or address '" + request.getRemoteAddr() + "' has his login attempt limit.");
}
if (!AdminManager.getInstance().isUserAdmin(loginUsername, true)) {
throw new UnauthorizedException("User '" + loginUsername + "' not allowed to login.");
}
authToken = AuthFactory.authenticate(loginUsername, password);
}
else {
errors.put("unauthorized", LocaleUtils.getLocalizedString("login.failed.unauthorized"));
}

第一步 判断是否IP地址限制连接

LoginLimitManager::hasHitConnectionLimit位于org.jivesoftware.admin.LoginLimitManager,用来判断是否尝试连接次数过多,防止暴力破解。

第二步 判断用户是否有管理员权限

AdminManager::isUserAdmin位于org.jivesoftware.openfire.admin.AdminManager,用来判断用户是否在管理员列表中。

1
2
3
4
5
6
7
8
9
10
public boolean isUserAdmin(String username, boolean allowAdminIfEmpty) {
if (adminList == null) {
loadAdminList();
}
if (allowAdminIfEmpty && adminList.isEmpty()) {
return "admin".equals(username);
}
JID userJID = XMPPServer.getInstance().createJID(username, null);
return adminList.contains(userJID);
}

加载管理员列表

AdminManager保存了管理员列表adminList,如果为空将首先从provider中加载。

1
2
3
private void loadAdminList() {
adminList = provider.getAdmins();
}

其中provider通过属性provider.admin.className来获取,意为管理员提供者,通过查询openfire数据库中的ofProperty属性表可知默认情况下provider对象为org.jivesoftware.openfire.admin.DefaultAdminProvider的实例,ofProperty表如下:


图:数据库属性表

继续跟踪DefaultAdminProvider::getAdmins()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public List<JID> getAdmins() {
List<JID> adminList = new ArrayList<JID>();
// 获取管理员纯JID
String jids = JiveGlobals.getProperty("admin.authorizedJIDs");
jids = (jids == null || jids.trim().length() == 0) ? "" : jids;
// 获取每个JID添加到管理员列表中,数据库中保存时以符号","分隔
StringTokenizer tokenizer = new StringTokenizer(jids, ",");
while (tokenizer.hasMoreTokens()) {
String jid = tokenizer.nextToken().toLowerCase().trim();
try {
adminList.add(new JID(jid));
}
catch (IllegalArgumentException e) {
}
}
// 未记录时添加默认的
if (adminList.isEmpty()) {
// Add default admin account when none was specified
adminList.add(new JID("admin", XMPPServer.getInstance().getServerInfo().getXMPPDomain(), null, true));
}
return adminList;
}

这里会去获取属性值admin.authorizeJIDs,以上图为例返回的是admin@bfc,yzw@bfc,现在管理员列表中有admin@bfc以及yzw@bfc的JID。

创建JID

1
JID userJID = XMPPServer.getInstance().createJID(username, null);

这句通过用户名来创建资源ID为空的JID,例如当前输入帐号为yzw,当前域为bfc,那么创建的值为yzw@bfc的纯JID。如果当前服务器域为bbk,那么创建的为yzw@bbk。其中域通过ofProperty表中的xmpp.domain来获取。

比较JID

        return adminList.contains(userJID);

这里判断是否在管理员列表里是否有登录用户的JID。如果用户登录服务器的域和管理员的域不一致,那么就不能登录了。

第三步 认证

AuthFactory.authenticate位于org.jivesoftware.openfire.auth. AuthFactory,用来验证帐号密码是否错误,并且返回认证信令。

1
2
3
4
5
6
7
8
9
public static AuthToken authenticate(String username, String password)
throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException {
if (LockOutManager.getInstance().isAccountDisabled(username)) {
LockOutManager.getInstance().recordFailedLogin(username);
throw new UnauthorizedException();
}
authProvider.authenticate(username, password);
return new AuthToken(username);
}

LockOutManager.getInstance().isAccountDisabled用来判断是否帐号被禁用了。执行到authProvider.authenticate(username, password),通过查看数据库表ofProperty表中的provider.auth.className可知默认情况下authProvider指向org.jivesoftware.openfire.auth.DefaultAuthProvider。执行DefaultAuthProvider:: authenticate(String,String):

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
public void authenticate(String username, String password) throws UnauthorizedException {
if (username == null || password == null) {
throw new UnauthorizedException();
}
username = username.trim().toLowerCase();
if (username.contains("@")) {
// Check that the specified domain matches the server's domain
int index = username.indexOf("@");
String domain = username.substring(index + 1);
if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
username = username.substring(0, index);
} else {
// Unknown domain. Return authentication failed.
throw new UnauthorizedException();
}
}
try {
if (!password.equals(getPassword(username))) {
throw new UnauthorizedException();
}
}
catch (UserNotFoundException unfe) {
throw new UnauthorizedException();
}
// Got this far, so the user must be authorized.
}

如果登录帐号有域的话需要认证域是否相同,然后认证密码是否正确。注意用户名是不含域的,所以比较后需要先去掉域的部分再获取密码。getPassword默认情况是从ofUser表中去获取密码,默认admin密码为明文的admin。

总结

在ofProperty表登录认证的最主要的几个值为:

  • admin.authorizeJIDs提供了管理员帐号,包括了所在域,例如admin@bfc。
  • provider.admin.className对应的类提供了管理员列表,默认从admin.authorizeJIDs加载。
  • provider.auth.className对应的类实现了认证方法。
  • xmpp.domain指出了服务器所在的域。

如果在初始化之后无法登录管理员,需要只修改上述几个值,在ofUser表中添加admin帐号,明文密码admin后即可用admin登录。