Аутентификация на стороне сервера в Meteor

Каков наилучший способ (наиболее безопасный и простой) для аутентификации пользователя для маршрута на стороне сервера?

Программное обеспечение/Версии

Я использую новейший Iron Router 1. * и Meteor 1. *, и для начала я просто использую пароль для учетных записей.

Код ссылки

У меня есть простой серверный маршрут, который отображает PDF на экран:

оба/routes.js

Router.route('/pdf-server', function() {
  var filePath = process.env.PWD + "/server/.files/users/test.pdf";
  console.log(filePath);
  var fs = Npm.require('fs');
  var data = fs.readFileSync(filePath);
  this.response.write(data);
  this.response.end();
}, {where: 'server'});

В качестве примера я хотел бы сделать что-то близкое к тому, что этот ответ SO предложил:

На сервере:

var Secrets = new Meteor.Collection("secrets"); 

Meteor.methods({
  getSecretKey: function () {
    if (!this.userId)
      // check if the user has privileges
      throw Meteor.Error(403);
    return Secrets.insert({_id: Random.id(), user: this.userId});
  },
});

И затем в клиентском коде:

testController.events({
  'click button[name=get-pdf]': function () {
      Meteor.call("getSecretKey", function (error, response) {
        if (error) throw error;

        if (response) 
          Router.go('/pdf-server');
      });
  }
});

Но даже если я каким-то образом обработаю этот метод, я все равно буду уязвим для пользователей, просто добавив URL-адрес, например '/pdf-server', если только сам маршрут не проверил право на сборку Secrets?

В маршруте я могу получить запрос и каким-то образом получить информацию о заголовке?

Router.route('/pdf-server', function() {
  var req = this.request;
  var res = this.response;
}, {where: 'server'});

И от клиента передайте токен по HTTP-заголовку, а затем в маршруте проверьте, хорош ли токен из коллекции?

Ответ 1

В дополнение к использованию маркеров url в качестве другого ответа вы также можете использовать файлы cookie:

Добавьте в некоторые пакеты, которые позволяют вам устанавливать файлы cookie и читать их на стороне сервера:

meteor add mrt:cookies thepumpinglemma:cookies

Тогда у вас может быть что-то, что синхронизирует файлы cookie с вашим статусом входа в систему.

Клиентская сторона

Tracker.autorun(function() {
     //Update the cookie whenever they log in or out
     Cookie.set("meteor_user_id", Meteor.userId());
     Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
});

Сторона сервера

На стороне сервера вам просто нужно проверить, что этот файл cookie действителен (с железным маршрутизатором)

Router.route('/somepath/:fileid', function() {

   //Check the values in the cookies
   var cookies = new Cookies( this.request ),
       userId = cookies.get("meteor_user_id") || "",
       token = cookies.get("meteor_token") || "";

   //Check a valid user with this token exists
   var user = Meteor.users.findOne({
       _id: userId,
       'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
   });

   //If they're not logged in tell them
   if(!user) return this.response.end("Not allowed");

   //Theyre logged in!
   this.response.end("You're logged in!");

}, {where:'server'});

Ответ 2

Поскольку серверные маршруты действуют как простые конечные точки REST, они не имеют доступа к данным аутентификации пользователя (например, они не могут вызывать Meteor.user()). Поэтому вам нужно разработать альтернативную схему аутентификации. Самый простой способ добиться этого - с помощью какой-либо формы обмена ключами, как обсуждалось здесь и здесь.

Пример реализации:

сервер /app.js

// whenever the user logs in, update her apiKey
Accounts.onLogin(function(info) {
  // generate a new apiKey
  var apiKey = Random.id();
  // add the apiKey to the user document
  Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
});

// auto-publish the current user apiKey
Meteor.publish(null, function() {
  return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
});

Библиотека /routes.js

// example route using the apiKey
Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
  .get(function() {
    // fetch the user with this key
    // note you may want to add an index on apiKey so this is fast
    var user = Meteor.users.findOne({apiKey: this.params.apiKey});

    if (user) {
      // we have authenticated the user - do something useful here
      this.response.statusCode = 200;
      return this.response.end('ok');
    } else {
      // the key is invalid or not provided so return an error
      this.response.statusCode = 403;
      return this.response.end('not allowed');
    }
  });

клиент /app.html

<template name="myTemplate">
    {{#with currentUser}}
      <a href="{{pathFor route='secret'}}">secret</a>
    {{/with}}
</template>

Примечания

  • Сделать /secret доступным только через HTTPS.

  • В то время как очень вероятно, что пользователь, запрашивающий /secret, в настоящее время подключен, нет никакой гарантии, что она есть. Пользователь мог войти в систему, скопировать ее ключ, закрыть вкладку и позже инициировать запрос.

  • Это простой способ аутентификации пользователей. Я бы изучил более сложные механизмы (см. Ссылки выше), если маршрут сервера показывает данные с высоким значением (SSN, кредитные карты и т.д.).

  • Подробнее о отправке статического содержимого с сервера см. этот вопрос.

Ответ 3

Я думаю, что у меня есть безопасное и простое решение для этого из IronRouter.route(). Запрос должен быть выполнен с допустимым идентификатором пользователя и маркером auth в заголовке. Я вызываю эту функцию из Router.route(), которая затем дает мне доступ к this.user или отвечает 401, если аутентификация терпит неудачу:

//  Verify the request is being made by an actively logged in user
//  @context: IronRouter.Router.route()
authenticate = ->
  // Get the auth info from header
  userId = this.request.headers['x-user-id']
  loginToken = this.request.headers['x-auth-token']

// Get the user from the database
if userId and loginToken
  user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}

// Return an error if the login token does not match any belonging to the user
if not user
  respond.call this, {success: false, message: "You must be logged in to do this."}, 401

// Attach the user to the context so they can be accessed at this.user within route
this.user = user


//  Respond to an HTTP request
//  @context: IronRouter.Router.route()
respond = (body, statusCode=200, headers) ->
  this.response.statusCode statusCode
  this.response.setHeader 'Content-Type', 'text/json'
  this.response.writeHead statusCode, headers
  this.response.write JSON.stringify(body)
  this.response.end()

И что-то вроде этого от клиента:

Meteor.startup ->

  HTTP.get "http://yoursite.com/pdf-server",
    headers:
      'X-Auth-Token': Accounts._storedLoginToken()
      'X-User-Id': Meteor.userId()
    (error, result) ->  // This callback triggered once http response received         
      console.log result

Этот код был сильно вдохновлен RestStop и RestStop2. Это часть метеорного пакета для написания API REST в Meteor 0.9.0+ (построена поверх Iron Router). Вы можете проверить полный исходный код здесь:

https://github.com/krose72205/meteor-restivus

Ответ 4

Я действительно верю, что использование HTTP-заголовков - лучшее решение этой проблемы, потому что они просты и не требуют обмена файлами cookie или разработки новой схемы аутентификации.

Мне понравился ответ @kahmali, поэтому я написал его для работы с WebApp и простым XMLHttpRequest. Это было проверено на Meteor 1.6.

Client

import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';

// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();

// Add files
files.forEach((file) => {
  form.append(file.name,
    // So BusBoy sees as file instead of field, use Blob
    new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});

// XHR progress, load, error, and readystatechange event listeners here

// Open Connection
xhr.open('POST', '/path/to/upload', true);

// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());

// Send
xhr.send(form);

Сервер

import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library

WebApp.connectHandlers
  .use(BusBoy())
  .use('/path/to/upload', (req, res) => {
    const user = req.headers['x-user-id'];
    // We have to get a base64 digest of the sha256 hashed login token
    // I'm not sure when Meteor changed to hashed tokens, but this is
    // one of the major differences from @kahmali answer
    const hash = crypto.createHash('sha256');
    hash.update(req.headers['x-auth-token']);

    // Authentication (is user logged-in)
    if (!Meteor.users.findOne({
      _id: user,
      'services.resume.loginTokens.hashedToken': hash.digest('base64'),
    })) {
      // User not logged in; 401 Unauthorized
      res.writeHead(401);
      res.end();
      return;
    }

    // Authorization
    if (!Roles.userIsInRole(user, 'whatever')) {
      // User is not authorized; 403 Forbidden
      res.writeHead(403);
      res.end();
      return;
    }

    if (req.busboy) {
      // Handle file upload
      res.writeHead(201); // eventually
      res.end();
    } else {
      // Something went wrong
      res.writeHead(500); // server error
      res.end();
    }
  });

Надеюсь, это поможет кому-то!

Ответ 5

Так как Meteor не использует файлы cookie сеанса, клиент должен явно включать какой-то идентификатор пользователя при запросе HTTP на маршрут сервера.

Самый простой способ сделать это - передать userId в строку запроса URL-адреса. Очевидно, вам также нужно добавить маркер безопасности, который докажет, что пользователь действительно является тем, кем он является. Получение этого токена может быть выполнено с помощью метода Метеор.

Метеор сам по себе не обеспечивает такой механизм, поэтому вам нужна некоторая пользовательская реализация. Я написал пакет Meteor под названием mhagmajer:server-route, который был тщательно протестирован. Подробнее об этом можно узнать здесь: https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94