最近研究了一下mongodb的用户认证全过程,也根据产品需求对mongodb的认证做了相关的修改。现在将mongodb分片过程中mongos的认证过程全解如下。首先从整体上把握认证的全过程,以下面简单的序列图来表示,第一步:调用者调用认证接口之后,从mongos获取随机码,用随机码+用户名+密码生成digest,并且发送用户名,随机码和Digest给mongos认。第二步: Mongos收到认证消息之后,取用户名。取随机码和digest. 第三步:mongos先从其缓存中获取用户信息,获取不到,到mongod去获取保存在数据库中的用户信息。第四步:按照Driver的方式生成一个Digest 即computed。第五步:比较digest 和Computed.返回结果。

Driver的代码很简单,不想多说,现在我们来看看mongos收到上面的消息之后,是怎样认证的,现将整个认证过程中重要的步骤的架构图勾画如下:
我这里不想去解析mongodb的通信过程代码,那部分代码,各位感兴趣的话,可以去研读。Mongo驱动与mongos或者mongod建立连接之后,会通过MessagingServer::accepted而accept是一个虚函数,就会调用PortMessageServer::accepted函数创建一个线程,而其入口正是接收所有消息的PortMessageServer:: handleIncomingMsg函数,看看其收到消息之后的关键代码:
-
static void* handleIncomingMsg(void* arg) {
-
TicketHolderReleaser connTicketReleaser( &Listener::globalTicketHolder );
-
-
invariant(arg);
-
scoped_ptr<MessagingPortWithHandler> portWithHandler(
-
static_cast<MessagingPortWithHandler*>(arg));
-
MessageHandler* const handler = portWithHandler->getHandler();
-
.............
-
Message m;
-
int64_t lastIdle = Listener::getElapsedTimeMillis();
-
int64_t avgMillisBetweenIdle = 0;
-
try {
-
LastError * le = new LastError();
-
lastError.reset( le ); // lastError now has ownership
-
-
handler->connected(portWithHandler.get());
-
-
while ( ! inShutdown() ) {
-
m.reset();
-
portWithHandler->psock->clearCounters();
-
-
if (!portWithHandler->recv(m)) {
-
if (!serverGlobalParams.quiet) {
-
int conns = Listener::globalTicketHolder.used()-1;
-
const char* word = (conns == 1 ? " connection" : " connections");
-
log() << "end connection " << portWithHandler->psock->remoteString()
-
<< " (" << conns << word << " now open)" << endl;
-
}
-
portWithHandler->shutdown();
-
break;
-
}
-
-
handler->process(m, portWithHandler.get(), le);
-
networkCounter.hit(portWithHandler->psock->getBytesIn(),
-
portWithHandler->psock->getBytesOut());
-
-
// Connections that don‘t run at a high rate should mark an idle point
-
// between operations to allow cleanup of the thread-local malloc cache.
-
// Just before a receive is a reasonable point, as we may overlap with
-
// the processing of a command response. Avoid doing this in very active
-
// threads as they are actively using their memory and not experiencing
-
// resource starvation. Use the course clock with averaging for efficiency.
-
-
const int64_t now = Listener::getElapsedTimeMillis();
-
const int64_t millisSinceIdle = now - lastIdle;
-
avgMillisBetweenIdle = (7 * avgMillisBetweenIdle + millisSinceIdle) / 8;
-
if (avgMillisBetweenIdle >= 10) {
-
markThreadIdle();
-
}
-
lastIdle = now;
-
}
-
}
-
catch ( AssertionException& e ) {
-
log() << "AssertionException handling request, closing client connection: " << e << endl;
-
portWithHandler->shutdown();
-
}
-
catch ( SocketException& e ) {
-
log() << "SocketException handling request, closing client connection: " << e << endl;
-
portWithHandler->shutdown();
-
}
-
catch ( const DBException& e ) { // must be right above std::exception to avoid catching subclasses
-
log() << "DBException handling request, closing client connection: " << e << endl;
-
portWithHandler->shutdown();
-
}
-
catch ( std::exception &e ) {
-
error() << "Uncaught std::exception: " << e.what() << ", terminating" << endl;
-
dbexit( EXIT_UNCAUGHT );
-
}
-
-
// Normal disconnect path.
-
#ifdef MONGO_SSL
-
SSLManagerInterface* manager = getSSLManager();
-
if (manager)
-
manager->cleanupThreadLocals();
-
#endif
-
handler->disconnected(portWithHandler.get());
-
-
return NULL;
-
}
9行定义了一个Message的变量m,用来接收驱动发送过来的数据,7行获取到了最终消息需要处理的一个Handler指针,22-32行,就是网络中接收数据,并对网络异常做出判断和处理。最关键的不过是handler->process(m, portWithHandler.get(), le);将收到的消息放入到handler中去处理。其实mongos已启动初始化的时候就已向MessagingPortWithHandler注册了一个ShardedMessageHandler,而从框架图中则可以看出,process函数是一个虚函数,直接调用ShardedMessageHandler::process函数中处理。此函数我们只看前面关键部分:
-
virtual void process( Message& m , AbstractMessagingPort* p , LastError * le) {
-
verify( p );
-
Request r( m , p );
-
-
verify( le );
-
lastError.startRequest( m , le );
-
-
try {
-
r.init();
-
r.process();
-
}
-
......
-
}
首先3行创建了一个用收到的消息创建了Request对象,并在9行对r进行初始化,初始化完成之后,就在Request::process中进行处理。
-
void Request::process( int attempt ) {
-
init();
-
int op = _m.operation();
-
verify( op > dbMsg );
-
-
int msgId = (int)(_m.header().getId());
-
-
Timer t;
-
LOG(3) << "Request::process begin ns: " << getns()
-
<< " msg id: " << msgId
-
<< " op: " << op
-
<< " attempt: " << attempt
-
<< endl;
-
-
_d.markSet();
-
-
bool iscmd = false;
-
if ( op == dbKillCursors ) {
-
cursorCache.gotKillCursors( _m );
-
globalOpCounters.gotOp( op , iscmd );
-
}
-
else if ( op == dbQuery ) {
-
NamespaceString nss(getns());
-
iscmd = nss.isCommand() || nss.isSpecialCommand();
-
-
if (iscmd) {
-
int n = _d.getQueryNToReturn();
-
uassert( 16978, str::stream() << "bad numberToReturn (" << n
-
<< ") for $cmd type ns - can only be 1 or -1",
-
n == 1 || n == -1 );
-
-
STRATEGY->clientCommandOp(*this);
-
}
-
else {
-
STRATEGY->queryOp( *this );
-
}
-
-
globalOpCounters.gotOp( op , iscmd )
-
}
-
...........
-
}
2-15行获取消息操作类型和消息ID,认证消息其实是一个查询的BSON,其操作类型为查询。所以if语句走等于opQuery分支,24行为判断其消息是否含有“$cmd”或者“$cmd.sys”,显然,此处得到iscmd为true, 那么执行 STRATEGY->clientCommandOp(*this);对于Strategy在这里作用,不就是策略模式中的”环境类”的作用,没错,这确实是一个策略模式,command定义了一组相关算法——命令执行,而CmdAuthenticate是认证命令的具体实现着, 其独立于使用它的Strategy类而变化,这就是策略模式的要义。从图可看出所有命令执行的算法都是通过run函数执行,而mongodb开发团队没有直接使用Strategy类调用run,而是通过Command:: runAgainstRegistered和Command:: execCommandClientBasic函数来调用,使得某些读者认为这不是一个策略模式。既然这里懂了那我们直接跳到CmdAuthenticate::run去看看怎么去实现认证喽。
-
bool CmdAuthenticate::run(OperationContext* txn, const string& dbname,
-
BSONObj& cmdObj,
-
int,
-
string& errmsg,
-
BSONObjBuilder& result,
-
bool fromRepl) {
-
-
if (!serverGlobalParams.quiet) {
-
mutablebson::Document cmdToLog(cmdObj, mutablebson::Document::kInPlaceDisabled);
-
redactForLogging(&cmdToLog);
-
log() << " authenticate db: " << dbname << " " << cmdToLog;
-
}
-
-
UserName user(cmdObj.getStringField("user"), dbname);
-
if (Command::testCommandsEnabled &&
-
user.getDB() == "admin" &&
-
user.getUser() == internalSecurity.user->getName().getUser()) {
-
// Allows authenticating as the internal user against the admin database. This is to
-
// support the auth passthrough test framework on mongos (since you can‘t use the local
-
// database on a mongos, so you can‘t auth as the internal user without this).
-
user = internalSecurity.user->getName();
-
}
-
-
std::string mechanism = cmdObj.getStringField("mechanism");
-
if (mechanism.empty()) {
-
mechanism = "MONGODB-CR";
-
}
-
Status status = _authenticate(txn, mechanism, user, cmdObj);
-
audit::logAuthentication(ClientBasic::getCurrent(),
-
mechanism,
-
user,
-
status.code());
-
...........
-
}
第14行到23为获取用户名,24-25行获取认证机制,有两种一种是MONGODB-CR,还有一种是SSL,我们只看MONGODB-CR这种,然后调用CmdAuthenticate::_authenticate,而CmdAuthenticate::_authenticate转手就交给了CmdAuthenticate::_authenticateCR。
-
Status CmdAuthenticate::_authenticateCR(
-
OperationContext* txn, const UserName& user, const BSONObj& cmdObj) {
-
-
.............
-
-
string key = cmdObj.getStringField("key");
-
string received_nonce = cmdObj.getStringField("nonce");
-
-
if( user.getUser().empty() || key.empty() || received_nonce.empty() ) {
-
sleepmillis(10);
-
return Status(ErrorCodes::ProtocolError,
-
"field missing/wrong type in received authenticate command");
-
}
-
-
............
-
User* userObj;
-
Status status = getGlobalAuthorizationManager()->acquireUser(txn, user, &userObj);
-
if (!status.isOK()) {
-
// Failure to find the privilege document indicates no-such-user, a fact that we do not
-
// wish to reveal to the client. So, we return AuthenticationFailed rather than passing
-
// through the returned status.
-
return Status(ErrorCodes::AuthenticationFailed, status.toString());
-
}
-
string pwd = userObj->getCredentials().password;
-
getGlobalAuthorizationManager()->releaseUser(userObj);
-
-
if (pwd.empty()) {
-
return Status(ErrorCodes::AuthenticationFailed,
-
"MONGODB-CR credentials missing in the user document");
-
}
-
-
md5digest d;
-
{
-
digestBuilder << user.getUser() << pwd;
-
string done = digestBuilder.str();
-
-
md5_state_t st;
-
md5_init(&st);
-
md5_append(&st, (const md5_byte_t *) done.c_str(), done.size());
-
md5_finish(&st, d);
-
}
-
-
string computed = digestToString( d );
-
-
if ( key != computed ) {
-
return Status(ErrorCodes::AuthenticationFailed, "key mismatch");
-
}
-
-
AuthorizationSession* authorizationSession =
-
ClientBasic::getCurrent()->getAuthorizationSession();
-
status = authorizationSession->addAndAuthorizeUser(txn, user);
-
if (!status.isOK()) {
-
return status;
-
}
-
-
return Status::OK();
-
}
6-16行,获取驱动生成的key和服务端发送给驱动的随机码,并对他们做简单的是否为空判断,第18行获取mongodb中users表中存放的用户信息,后面会详细解析此函数,得到此信息之后,25-26为获取服务器中的保存的密码信息,并且以客户端相同的方式生成digest(这里忽略了将随机码输入到digestBuilder中的代码),然后比较驱动发送过来的key和生成的computed是否相等, 相等就是认证通过,不相等说明密码错误,这就是互联网中经常用的不在网络中传输密码的认证过程。
到这里好像还有一个问题没有解开,那就是,mongos是怎样从mongodb中获取到用户信息的,看看如下函数:
-
Status AuthorizationManager::acquireUser(
-
OperationContext* txn, const UserName& userName, User** acquiredUser) {
-
.........
-
-
unordered_map<UserName, User*>::iterator it;
-
-
CacheGuard guard(this, CacheGuard::fetchSynchronizationManual);
-
while ((_userCache.end() == (it = _userCache.find(userName))) &&
-
guard.otherUpdateInFetchPhase()) {
-
-
guard.wait();
-
}
-
-
if (it != _userCache.end()) {
-
fassert(16914, it->second);
-
fassert(17003, it->second->isValid());
-
fassert(17008, it->second->getRefCount() > 0);
-
it->second->incrementRefCount();
-
*acquiredUser = it->second;
-
return Status::OK();
-
}
-
-
std::auto_ptr<User> user;
-
-
int authzVersion = _version;
-
guard.beginFetchPhase();
-
-
// Number of times to retry a user document that fetches due to transient
-
// AuthSchemaIncompatible errors. These errors should only ever occur during and shortly
-
// after schema upgrades.
-
static const int maxAcquireRetries = 2;
-
Status status = Status::OK();
-
for (int i = 0; i < maxAcquireRetries; ++i) {
-
if (authzVersion == schemaVersionInvalid) {
-
Status status = _externalState->getStoredAuthorizationVersion(txn, &authzVersion);
-
if (!status.isOK())
-
return status;
-
}
-
-
switch (authzVersion) {
-
default:
-
status = Status(ErrorCodes::BadValue, mongoutils::str::stream() <<
-
"Illegal value for authorization data schema version, " <<
-
authzVersion);
-
break;
-
case schemaVersion28SCRAM:
-
case schemaVersion26Final:
-
case schemaVersion26Upgrade:
-
status = _fetchUserV2(txn, userName, &user);
-
break;
-
case schemaVersion24:
-
status = Status(ErrorCodes::AuthSchemaIncompatible, mongoutils::str::stream() <<
-
"Authorization data schema version " << schemaVersion24 <<
-
" not supported after MongoDB version 2.6.");
-
break;
-
}
-
if (status.isOK())
-
break;
-
if (status != ErrorCodes::AuthSchemaIncompatible)
-
return status;
-
-
authzVersion = schemaVersionInvalid;
-
}
-
if (!status.isOK())
-
return status;
-
-
guard.endFetchPhase();
-
-
user->incrementRefCount();
-
// NOTE: It is not safe to throw an exception from here to the end of the method.
-
if (guard.isSameCacheGeneration()) {
-
_userCache.insert(std::make_pair(userName, user.get()));
-
if (_version == schemaVersionInvalid)
-
_version = authzVersion;
-
}
-
else {
-
// If the cache generation changed while this thread was in fetch mode, the data
-
// associated with the user may now be invalid, so we must mark it as such. The caller
-
// may still opt to use the information for a short while, but not indefinitely.
-
user->invalidate();
-
}
-
*acquiredUser = user.release();
-
-
return Status::OK();
-
}
第7-12行,首先定一个护卫类CacheGuard,其作用是进入此区域后,只能是单线程操作,并手工加载用户数据,首先从本地缓存中查找是否包含有此用户的数据,如果没有,就调用AuthzManagerExternalStateMongos::getStoredAuthorizationVersion函数获取所有的用户信息,不要被函数名欺骗,是获取该用户存在磁盘上的所有用户信息,并加入缓存,此过程不成功会重复2次,最后通过_fetchUserV2解析获取到的用户信息,我所用的代码是2.8版本,不支持2.4以前的解析过程,只支持2.6版本后的。到此认证过程全部解析完毕。
注意:其实此过程还使用到了一个命令模式,就是在收到消息之后,到消息到Command的处理过程,请读者自行体会。
mongodb认证源码分析——使用策略模式,网络不传输密码
原文:http://blog.chinaunix.net/uid-26904464-id-4781857.html