Статьи

Игровые серверы и Couchbase с Node.js — часть 2

Это сообщение от Бретта Лоусона из блога Couchbase.

Вступление

Если вы еще не прочитали Часть 1 этой серии, я предлагаю вам сделать это, поскольку она настраивает базовый макет проекта, а также базовое управление пользователями и является предпосылкой для Части 2!

В этой части серии мы будем реализовывать управление сеансами и аутентифицированные конечные точки (конечные точки, требующие входа в систему). Давайте начнем!

Управление сессиями — Модель

Первым шагом к созданию наших конечных точек управления сеансом является настройка модели, которая будет использовать эти конечные точки для управления базой данных. В отличие от модели счета, эта модель не требует ссылочных документов или какой-либо сложной логики и, следовательно, является довольно простой. Первым шагом является импорт различных необходимых нам модулей, а также получение ссылки на соединение с нашей базой данных из нашего модуля базы данных.

var db = require('./../database').mainBucket;
var couchbase = require('couchbase');
var uuid = require('uuid');

Затем, снова аналогично нашей модели учетной записи, мы создаем небольшую функцию для удаления свойств нашей базы данных. ‘type’ — единственный, что нам нужно удалить и в этом случае.

function cleanSessionObj(obj) {
  delete obj.type;
  return obj;
}

Теперь давайте начнем работать с самим классом модели сеанса. Сначала идет наш пустой конструктор. Здесь я хотел бы упомянуть, что наши классы моделей в этом примере в настоящее время полностью статичны, но хорошей практикой является возвращение классов вашей модели из статических операций CRUD, мы просто не в том смысле, что это было бы полезно все же.

function SessionModel() {
}
module.exports = SessionModel;

Теперь для нашей первой функции модели сеанса, которая фактически сделает любую работу.

SessionModel.create = function(uid, callback) {
};

И внутри этой функции нам нужно создать наш документ, который мы будем вставлять, и связанный с ним ключ (мы сохраняем только uid, а затем получаем доступ к оставшейся информации пользователей непосредственно из их документа учетной записи через нашу модель учетной записи, которую вы увидите позже.

var sessDoc = {
  type: 'session',
  sid: uuid.v4(),
  uid: uid
};
var sessDocName = 'sess-' + sessDoc.sid;

Затем мы сохраняем наш недавно созданный документ сеанса в нашем кластере. Вы также заметите, что здесь мы вызываем нашу функцию очистки, а также устанавливаем значение истечения 60 минут. Это заставит кластер «истечь» сеанс, удалив его через это время.

db.add(sessDocName, sessDoc, {expiry: 3600}, function(err, result) {
  callback(err, cleanSessionObj(sessDoc), result.cas);
});

Затем нам нужно построить функцию на нашей модели, которая позволит нам извлекать информацию о сеансе, которую мы сохранили ранее. Для этого мы генерируем ключ, соответствующий тому, который мы сохранили, и затем выполняем запрос get, возвращая пользовательский uid из сеанса в наш обратный вызов.

SessionModel.get = function(sid, callback) {
  var sessDocName = 'sess-' + sid;

  db.get(sessDocName, function(err, result) {
    if (err) {
      return callback(err);
    }

    callback(null, result.value.uid);
  });
};

И вот завершенный accountmodel.js:

var db = require('./../database').mainBucket;
var couchbase = require('couchbase');
var uuid = require('uuid');

function cleanSessionObj(obj) {
  delete obj.type;
  return obj;
}

function SessionModel() {
}

SessionModel.create = function(uid, callback) {
  var sessDoc = {
    type: 'session',
    sid: uuid.v4(),
    uid: uid
  };
  var sessDocName = 'sess-' + sessDoc.sid;

  db.add(sessDocName, sessDoc, {expiry: 3600}, function(err, result) {
    callback(err, cleanSessionObj(sessDoc), result.cas);
  });
};

SessionModel.get = function(sid, callback) {
  var sessDocName = 'sess-' + sid;

  db.get(sessDocName, function(err, result) {
    if (err) {
      return callback(err);
    }

    callback(null, result.value.uid);
  });
};

module.exports = SessionModel;

Управление сеансом — поиск учетной записи

Прежде чем мы сможем начать писать сам наш обработчик запросов, нам нужно создать метод, который позволит нам искать пользователя по его имени. Мы не хотим, чтобы пользователи запомнили свои uid! В первой части мы создали нашу учетную модель в accountmodel.js. Вернемся к этому файлу и добавим новый метод.

AccountModel.getByUsername = function(username, callback) {
};

Как мы справимся с этим методом, мы создадим ключ, используя предоставленное имя пользователя, которое будет искать один из ссылочных документов, созданных нами в AccountModel.create. Если мы не можем найти этот ссылочный документ, мы предполагаем, что пользователь не существует, и возвращаем ошибку. Если имя пользователя может быть найдено, и мы находим ссылочный документ, мы затем выполняем `AccountModel.get`, чтобы найти сам пользовательский документ, и пересылать туда обратный вызов. Это означает, что вызовы AccountModel.getByUsername вернут полный объект пользователя, как если бы вы напрямую вызывали AccountModel.get с помощью uid.

Вот вся функция:

AccountModel.getByUsername = function(username, callback) {
  var refdocName = 'username-' + username;
  db.get(refdocName, function(err, result) {
    if (err && err.code === couchbase.errors.keyNotFound) {
      return callback('Username not found');
    } else if (err) {
      return callback(err);
    }

    // Extract the UID we found
    var foundUid = result.value.uid;

    // Forward to a normal get
    AccountModel.get(foundUid, callback);
  });
};

Управление сеансами — обработка запросов

Последний шаг включения создания сеанса — это написание самого обработчика запроса. К счастью, большая часть важной логики ушла в разделы выше, и наш обработчик запросов пересылает информацию каждому для обработки. Сначала мы проверяем наши входные данные от пользователя, чтобы убедиться, что все необходимое было предоставлено. Далее мы пытаемся найти учетную запись по указанному имени пользователя. Затем мы проверяем, что пароль соответствует тому, что предоставил пользователь, путем хеширования предоставленного пароля и сравнения. И, наконец, мы создаем сеанс с нашей вновь созданной моделью сеанса и возвращаем детали пользователю. Вы можете заметить, что идентификатор сеанса напрямую не предоставляется пользователю, а вместо этого передается в заголовке. Это для согласованности, поскольку заголовок также используется для аутентификации каждого запроса позже.

app.post('/sessions', function(req, res, next) {
  if (!req.body.username) {
    return res.send(400, 'Must specify a username');
  }
  if (!req.body.password) {
    return res.send(400, 'Must specify a password');
  }

  accountModel.getByUsername(req.body.username, function(err, user) {
    if (err) {
      return next(err);
    }

    if (crypt.sha1(req.body.password) !== user.password) {
      return res.send(400, 'Passwords do not match');
    }

    sessionModel.create(user.uid, function(err, session) {
      if (err) {
        return next(err);
      }

      res.setHeader('Authorization', 'Bearer ' + session.sid);

      // Delete the password for security reasons
      delete user.password;
      res.send(user);
    });
  });
});

Теперь, когда наше создание сеанса существует, давайте добавим метод для аутентификации пользователя на основе запроса. Это проверит сеанс пользователей, а пока я поместил это в наш app.js, однако, возможно, лучше будет поместить его в отдельный файл позже, когда маршруты начнут разделяться на отдельные файлы. Чтобы аутентифицировать пользователя, мы проверяем стандартный заголовок HTTP Authorization, извлекаем из него идентификатор сеанса и затем ищем его, используя нашу модель сеанса. Если все идет по плану, мы сохраняем вновь найденный идентификатор пользователя в запросе для последующих обработчиков маршрута.

function authUser(req, res, next) {
  req.uid = null;
  if (req.headers.authorization) {
    var authInfo = req.headers.authorization.split(' ');
    if (authInfo[0] === 'Bearer') {
      var sid = authInfo[1];
      sessionModel.get(sid, function(err, uid) {
        if (err) {
          next('Your session id is invalid');
        } else {
          req.uid = uid;
          next();
        }
      });
    } else {
      next('Must be authorized to access this endpoint');
    }
  } else {
    next('Must be authorized to access this endpoint');
  }
}

Главным образом для отображения метода authUser в действии я реализовал конечную точку `/ me`, которая возвращает документ пользователя. Мы просто выполняем модель учетной записи на основе идентификатора пользователя, который был сохранен в запросе обработчиком authInfo, и возвращаем его клиенту, конечно же, сначала удаляя пароль.

app.get('/me', authUser, function(req, res, next) {
  accountModel.get(req.uid, function(err, user) {
    if (err) {
      return next(err);
    }

    delete user.password;
    res.send(user);
  });
});

Финал

Теперь вы сможете создавать учетные записи, входить в них и запрашивать сохраненную информацию о пользователе. Вот пример использования пользователя, которого мы создали в первой части.

> POST /sessions
{
  "username": "brett19",
  "password": "success!"
}
< 200 OK
Header(Authorization): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7


> GET /me
Header(Authorization): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
< 200 OK
{
    "uid": "b836d211-425c-47de-9faf-5d0adc078edc",
    "name": "Brett Lawson",
    "username": "brett19"
}

Полный исходный код этого приложения доступен здесь: https://github.com/brett19/node-gameapi.

Наслаждайтесь! Brett