首页 > 数据库技术 > 详细

mongodb认证源码分析——使用策略模式,网络不传输密码

时间:2015-01-27 02:21:41      阅读:858      评论:0      收藏:0      [点我收藏+]

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

bubuko.com,布布扣

 

Driver的代码很简单,不想多说,现在我们来看看mongos收到上面的消息之后,是怎样认证的,现将整个认证过程中重要的步骤的架构图勾画如下:

  bubuko.com,布布扣

我这里不想去解析mongodb的通信过程代码,那部分代码,各位感兴趣的话,可以去研读。Mongo驱动与mongos或者mongod建立连接之后,会通过MessagingServer::acceptedaccept是一个虚函数,就会调用PortMessageServer::accepted函数创建一个线程,而其入口正是接收所有消息的PortMessageServer:: handleIncomingMsg函数,看看其收到消息之后的关键代码:


点击(此处)折叠或打开

  1. static void* handleIncomingMsg(void* arg) {
  2.             TicketHolderReleaser connTicketReleaser( &Listener::globalTicketHolder );

  3.             invariant(arg);
  4.             scoped_ptr<MessagingPortWithHandler> portWithHandler(
  5.                 static_cast<MessagingPortWithHandler*>(arg));
  6.             MessageHandler* const handler = portWithHandler->getHandler();
  7.             .............
  8.             Message m;
  9.             int64_t lastIdle = Listener::getElapsedTimeMillis();
  10.             int64_t avgMillisBetweenIdle = 0;
  11.             try {
  12.                 LastError * le = new LastError();
  13.                 lastError.reset( le ); // lastError now has ownership

  14.                 handler->connected(portWithHandler.get());

  15.                 while ( ! inShutdown() ) {
  16.                     m.reset();
  17.                     portWithHandler->psock->clearCounters();

  18.                     if (!portWithHandler->recv(m)) {
  19.                         if (!serverGlobalParams.quiet) {
  20.                             int conns = Listener::globalTicketHolder.used()-1;
  21.                             const char* word = (conns == 1 ? " connection" : " connections");
  22.                             log() << "end connection " << portWithHandler->psock->remoteString()
  23.                                   << " (" << conns << word << " now open)" << endl;
  24.                         }
  25.                         portWithHandler->shutdown();
  26.                         break;
  27.                     }

  28.                     handler->process(m, portWithHandler.get(), le);
  29.                     networkCounter.hit(portWithHandler->psock->getBytesIn(),
  30.                                        portWithHandler->psock->getBytesOut());

  31.                     // Connections that dont run at a high rate should mark an idle point
  32.                     // between operations to allow cleanup of the thread-local malloc cache.
  33.                     // Just before a receive is a reasonable point, as we may overlap with
  34.                     // the processing of a command response. Avoid doing this in very active
  35.                     // threads as they are actively using their memory and not experiencing
  36.                     // resource starvation. Use the course clock with averaging for efficiency.

  37.                     const int64_t now = Listener::getElapsedTimeMillis();
  38.                     const int64_t millisSinceIdle = now - lastIdle;
  39.                     avgMillisBetweenIdle = (7 * avgMillisBetweenIdle + millisSinceIdle) / 8;
  40.                     if (avgMillisBetweenIdle >= 10) {
  41.                         markThreadIdle();
  42.                     }
  43.                     lastIdle = now;
  44.                 }
  45.             }
  46.             catch ( AssertionException& e ) {
  47.                 log() << "AssertionException handling request, closing client connection: " << e << endl;
  48.                 portWithHandler->shutdown();
  49.             }
  50.             catch ( SocketException& e ) {
  51.                 log() << "SocketException handling request, closing client connection: " << e << endl;
  52.                 portWithHandler->shutdown();
  53.             }
  54.             catch ( const DBException& e ) { // must be right above std::exception to avoid catching subclasses
  55.                 log() << "DBException handling request, closing client connection: " << e << endl;
  56.                 portWithHandler->shutdown();
  57.             }
  58.             catch ( std::exception &e ) {
  59.                 error() << "Uncaught std::exception: " << e.what() << ", terminating" << endl;
  60.                 dbexit( EXIT_UNCAUGHT );
  61.             }

  62.             // Normal disconnect path.
  63. #ifdef MONGO_SSL
  64.             SSLManagerInterface* manager = getSSLManager();
  65.             if (manager)
  66.                 manager->cleanupThreadLocals();
  67. #endif
  68.             handler->disconnected(portWithHandler.get());

  69.             return NULL;
  70.         }

9行定义了一个Message的变量m,用来接收驱动发送过来的数据,7行获取到了最终消息需要处理的一个Handler指针,22-32行,就是网络中接收数据,并对网络异常做出判断和处理。最关键的不过是handler->process(m, portWithHandler.get(), le);将收到的消息放入到handler中去处理。其实mongos已启动初始化的时候就已向MessagingPortWithHandler注册了一个ShardedMessageHandler,而从框架图中则可以看出,process函数是一个虚函数,直接调用ShardedMessageHandler::process函数中处理。此函数我们只看前面关键部分:

点击(此处)折叠或打开

  1. virtual void process( Message& m , AbstractMessagingPort* p , LastError * le) {
  2.      verify( p );
  3.      Request r( m , p );
  4.     
  5.      verify( le );
  6.      lastError.startRequest( m , le );
  7.     
  8.      try {
  9.      r.init();
  10.      r.process();
  11.      }
  12.      ......
  13.     }

首先3行创建了一个用收到的消息创建了Request对象,并在9行对r进行初始化,初始化完成之后,就在Request::process中进行处理。

点击(此处)折叠或打开

  1. void Request::process( int attempt ) {
  2.           init();
  3.           int op = _m.operation();
  4.           verify( op > dbMsg );
  5.   
  6.           int msgId = (int)(_m.header().getId());
  7.   
  8.           Timer t;
  9.           LOG(3) << "Request::process begin ns: " << getns()
  10.                  << " msg id: " << msgId
  11.                  << " op: " << op
  12.                 << " attempt: " << attempt
  13.                  << endl;
  14.  
  15.           _d.markSet();
  16.   
  17.           bool iscmd = false;
  18.           if ( op == dbKillCursors ) {
  19.               cursorCache.gotKillCursors( _m );
  20.               globalOpCounters.gotOp( op , iscmd );
  21.           }
  22.           else if ( op == dbQuery ) {
  23.               NamespaceString nss(getns());
  24.               iscmd = nss.isCommand() || nss.isSpecialCommand();
  25.   
  26.               if (iscmd) {
  27.                   int n = _d.getQueryNToReturn();
  28.                   uassert( 16978, str::stream() << "bad numberToReturn (" << n
  29.                                                 << ") for $cmd type ns - can only be 1 or -1",
  30.                           n == 1 || n == -1 );
  31.  
  32.                  STRATEGY->clientCommandOp(*this);
  33.              }
  34.               else {
  35.                   STRATEGY->queryOp( *this );
  36.               }
  37.   
  38.               globalOpCounters.gotOp( op , iscmd )
  39.           }
  40.       ...........
  41.  }

2-15行获取消息操作类型和消息ID,认证消息其实是一个查询的BSON,其操作类型为查询。所以if语句走等于opQuery分支,24行为判断其消息是否含有“$cmd”或者“$cmd.sys,显然,此处得到iscmdtrue, 那么执行  STRATEGY->clientCommandOp(*this);对于Strategy在这里作用,不就是策略模式中的环境类的作用,没错,这确实是一个策略模式,command定义了一组相关算法——命令执行,而CmdAuthenticate是认证命令的具体实现着, 其独立于使用它的Strategy类而变化,这就是策略模式的要义。从图可看出所有命令执行的算法都是通过run函数执行,而mongodb开发团队没有直接使用Strategy类调用run,而是通过Command:: runAgainstRegisteredCommand:: execCommandClientBasic函数来调用,使得某些读者认为这不是一个策略模式。既然这里懂了那我们直接跳到CmdAuthenticate::run去看看怎么去实现认证喽。

点击(此处)折叠或打开

  1. bool CmdAuthenticate::run(OperationContext* txn, const string& dbname,
  2.                               BSONObj& cmdObj,
  3.                               int,
  4.                               string& errmsg,
  5.                               BSONObjBuilder& result,
  6.                               bool fromRepl) {

  7.         if (!serverGlobalParams.quiet) {
  8.             mutablebson::Document cmdToLog(cmdObj, mutablebson::Document::kInPlaceDisabled);
  9.             redactForLogging(&cmdToLog);
  10.             log() << " authenticate db: " << dbname << " " << cmdToLog;
  11.         }

  12.         UserName user(cmdObj.getStringField("user"), dbname);
  13.         if (Command::testCommandsEnabled &&
  14.                 user.getDB() == "admin" &&
  15.                 user.getUser() == internalSecurity.user->getName().getUser()) {
  16.             // Allows authenticating as the internal user against the admin database. This is to
  17.             // support the auth passthrough test framework on mongos (since you cant use the local
  18.             // database on a mongos, so you cant auth as the internal user without this).
  19.             user = internalSecurity.user->getName();
  20.         }

  21.         std::string mechanism = cmdObj.getStringField("mechanism");
  22.         if (mechanism.empty()) {
  23.             mechanism = "MONGODB-CR";
  24.         }
  25.         Status status = _authenticate(txn, mechanism, user, cmdObj);
  26.         audit::logAuthentication(ClientBasic::getCurrent(),
  27.                                  mechanism,
  28.                                  user,
  29.                                  status.code());
  30.        ...........
  31. }

14行到23为获取用户名,24-25行获取认证机制,有两种一种是MONGODB-CR,还有一种是SSL,我们只看MONGODB-CR这种,然后调用CmdAuthenticate::_authenticate,而CmdAuthenticate::_authenticate转手就交给了CmdAuthenticate::_authenticateCR

点击(此处)折叠或打开

  1. Status CmdAuthenticate::_authenticateCR(
  2.                 OperationContext* txn, const UserName& user, const BSONObj& cmdObj) {

  3.        .............

  4.         string key = cmdObj.getStringField("key");
  5.         string received_nonce = cmdObj.getStringField("nonce");

  6.         if( user.getUser().empty() || key.empty() || received_nonce.empty() ) {
  7.             sleepmillis(10);
  8.             return Status(ErrorCodes::ProtocolError,
  9.                           "field missing/wrong type in received authenticate command");
  10.         }
  11.        
  12.         ............
  13.         User* userObj;
  14.         Status status = getGlobalAuthorizationManager()->acquireUser(txn, user, &userObj);
  15.         if (!status.isOK()) {
  16.             // Failure to find the privilege document indicates no-such-user, a fact that we do not
  17.             // wish to reveal to the client. So, we return AuthenticationFailed rather than passing
  18.             // through the returned status.
  19.             return Status(ErrorCodes::AuthenticationFailed, status.toString());
  20.         }
  21.         string pwd = userObj->getCredentials().password;
  22.         getGlobalAuthorizationManager()->releaseUser(userObj);

  23.         if (pwd.empty()) {
  24.             return Status(ErrorCodes::AuthenticationFailed,
  25.                           "MONGODB-CR credentials missing in the user document");
  26.         }

  27.         md5digest d;
  28.         {
  29.             digestBuilder << user.getUser() << pwd;
  30.             string done = digestBuilder.str();

  31.             md5_state_t st;
  32.             md5_init(&st);
  33.             md5_append(&st, (const md5_byte_t *) done.c_str(), done.size());
  34.             md5_finish(&st, d);
  35.         }

  36.         string computed = digestToString( d );

  37.         if ( key != computed ) {
  38.             return Status(ErrorCodes::AuthenticationFailed, "key mismatch");
  39.         }

  40.         AuthorizationSession* authorizationSession =
  41.             ClientBasic::getCurrent()->getAuthorizationSession();
  42.         status = authorizationSession->addAndAuthorizeUser(txn, user);
  43.         if (!status.isOK()) {
  44.             return status;
  45.         }

  46.         return Status::OK();
  47.     }

6-16行,获取驱动生成的key和服务端发送给驱动的随机码,并对他们做简单的是否为空判断,第18行获取mongodbusers表中存放的用户信息,后面会详细解析此函数,得到此信息之后,25-26为获取服务器中的保存的密码信息,并且以客户端相同的方式生成digest(这里忽略了将随机码输入到digestBuilder中的代码),然后比较驱动发送过来的key和生成的computed是否相等, 相等就是认证通过,不相等说明密码错误,这就是互联网中经常用的不在网络中传输密码的认证过程。

    到这里好像还有一个问题没有解开,那就是,mongos是怎样从mongodb中获取到用户信息的,看看如下函数:

点击(此处)折叠或打开

  1. Status AuthorizationManager::acquireUser(
  2.                 OperationContext* txn, const UserName& userName, User** acquiredUser) {
  3.         .........

  4.         unordered_map<UserName, User*>::iterator it;

  5.         CacheGuard guard(this, CacheGuard::fetchSynchronizationManual);
  6.         while ((_userCache.end() == (it = _userCache.find(userName))) &&
  7.                guard.otherUpdateInFetchPhase()) {

  8.             guard.wait();
  9.         }

  10.         if (it != _userCache.end()) {
  11.             fassert(16914, it->second);
  12.             fassert(17003, it->second->isValid());
  13.             fassert(17008, it->second->getRefCount() > 0);
  14.             it->second->incrementRefCount();
  15.             *acquiredUser = it->second;
  16.             return Status::OK();
  17.         }

  18.         std::auto_ptr<User> user;

  19.         int authzVersion = _version;
  20.         guard.beginFetchPhase();

  21.         // Number of times to retry a user document that fetches due to transient
  22.         // AuthSchemaIncompatible errors. These errors should only ever occur during and shortly
  23.         // after schema upgrades.
  24.         static const int maxAcquireRetries = 2;
  25.         Status status = Status::OK();
  26.         for (int i = 0; i < maxAcquireRetries; ++i) {
  27.             if (authzVersion == schemaVersionInvalid) {
  28.                 Status status = _externalState->getStoredAuthorizationVersion(txn, &authzVersion);
  29.                 if (!status.isOK())
  30.                     return status;
  31.             }

  32.             switch (authzVersion) {
  33.             default:
  34.                 status = Status(ErrorCodes::BadValue, mongoutils::str::stream() <<
  35.                                 "Illegal value for authorization data schema version, " <<
  36.                                 authzVersion);
  37.                 break;
  38.             case schemaVersion28SCRAM:
  39.             case schemaVersion26Final:
  40.             case schemaVersion26Upgrade:
  41.                 status = _fetchUserV2(txn, userName, &user);
  42.                 break;
  43.             case schemaVersion24:
  44.                 status = Status(ErrorCodes::AuthSchemaIncompatible, mongoutils::str::stream() <<
  45.                                 "Authorization data schema version " << schemaVersion24 <<
  46.                                 " not supported after MongoDB version 2.6.");
  47.                 break;
  48.             }
  49.             if (status.isOK())
  50.                 break;
  51.             if (status != ErrorCodes::AuthSchemaIncompatible)
  52.                 return status;

  53.             authzVersion = schemaVersionInvalid;
  54.         }
  55.         if (!status.isOK())
  56.             return status;

  57.         guard.endFetchPhase();

  58.         user->incrementRefCount();
  59.         // NOTE: It is not safe to throw an exception from here to the end of the method.
  60.         if (guard.isSameCacheGeneration()) {
  61.             _userCache.insert(std::make_pair(userName, user.get()));
  62.             if (_version == schemaVersionInvalid)
  63.                 _version = authzVersion;
  64.         }
  65.         else {
  66.             // If the cache generation changed while this thread was in fetch mode, the data
  67.             // associated with the user may now be invalid, so we must mark it as such. The caller
  68.             // may still opt to use the information for a short while, but not indefinitely.
  69.             user->invalidate();
  70.         }
  71.         *acquiredUser = user.release();

  72.         return Status::OK();
  73.     }

7-12行,首先定一个护卫类CacheGuard,其作用是进入此区域后,只能是单线程操作,并手工加载用户数据,首先从本地缓存中查找是否包含有此用户的数据,如果没有,就调用AuthzManagerExternalStateMongos::getStoredAuthorizationVersion函数获取所有的用户信息,不要被函数名欺骗,是获取该用户存在磁盘上的所有用户信息,并加入缓存,此过程不成功会重复2次,最后通过_fetchUserV2解析获取到的用户信息,我所用的代码是2.8版本,不支持2.4以前的解析过程,只支持2.6版本后的。到此认证过程全部解析完毕。
注意:其实此过程还使用到了一个命令模式,就是在收到消息之后,到消息到Command的处理过程,请读者自行体会。








mongodb认证源码分析——使用策略模式,网络不传输密码

原文:http://blog.chinaunix.net/uid-26904464-id-4781857.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!