Прежде чем мы углубимся в код, давайте немного поговорим о том, как на самом деле выглядит аутентификация. По сути, я хочу иметь возможность войти на свой сайт, а затем иметь возможность редактировать свой профиль/задачи. Итак, как что-то подобное будет работать?

  1. Сначала сохраните зашифрованные пароли в базу данных
  2. При входе проверьте, совпадают ли пароли
  3. Если пароли совпадают, то создайте специальный токен
  4. Этот токен должен быть сохранен на модели пользователя, а также на компьютере пользователя.
  5. Когда пользователь переходит к другому маршруту на сайте, проверьте, есть ли у пользователя этот токен-заголовок при запросе определенных страниц, если да, разрешите доступ.

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

Итак, в нашем пользовательском тестовом файле

describe('Authenticating Users', () => {
 beforeEach(setUpDatabase);
test('Should return user when logging in', async () => {
  const response = await request(app)
   .post(`/users/login`)
   .send({ email: userOne.email, password: userOne.password })
   .expect(200);
  expect(response.body._id).toBe(userOne._id.toString());
 });
test('Should return 400 if incorrect password or email', async () => {
  await request(app)
   .post(`/users/login`)
   .send({ email: userOne.email, password: '1309894138909' })
   .expect(400);
  await request(app)
   .post(`/users/login`)
   .send({ email: '[email protected]', password: userOne.password })
   .expect(400);
 });
});

Давайте сначала просто убедимся, что идентификатор пользователя действителен, и отправим обратно пользователя.

router.post('/users/login', async (req, res) => {
 try {
  const user = await User.findOne({ email: req.body.email });
  if (!user) {
   throw new Error();
  }
   res.send(user);
} catch (error) {
  res.status(400).send({ error: 'Unable to authenticate' });
 }
});

Если пользователь действителен, мы должны убедиться, что пароли совпадают, но как мы это делаем? Мы зашифровали пароли с помощью bcrypt, поэтому открытый текст и зашифрованный определенно не будут совпадать. Для этого мы можем использовать bcrypt.compare(пароль, хеш). Здесь используются промисы, поэтому здесь мы также можем использовать async/await. Давайте изменим часть res.send(user) на следующую

const isMatch = await bcrypt.compare(req.body.password, user.password);
  if (isMatch) {
   res.send(user);
  } else {
   throw new Error();
  }

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

test('Should create add a token to the tokens array on user when logging in', async () => {
  const user = await request(app)
   .post('/users/login')
   .send({ email: userOne.email, password: userOne.password });
expect(user.tokens.length).toBe(1);
  expect(await jwt.verify(user.tokens[0], process.env.JWT_SECRET)._id).toBe(userOne._id.toString());
 });

В наших файлах test и dev env,

JWT_SECRET=laekjflkafioaijkjdfkliofj

Теперь давайте обновим нашу модель пользователя с помощью массива токенов типов токенов.

tokens: [
   {
    token: {
     type: String,
     required: true,
    },
   },
  ],

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

router.post('/users/login', async (req, res) => {
try {
  const user = await User.findByCredentials(req.body.email, req.body.password);
  res.send(user)
  
 } catch (error) {
  
 }
});

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

Итак, в пользовательской модели мы можем сделать что-то вроде:

userSchema.statics.findByCredentials = async => (email, password) {
 const user = await User.findOne({ email })
 if (!user) {
  throw new Error()
 }
 const isMatch = await bcrypt.compare(password, user.password);
 if (isMatch) {
  return user
 } else {
  throw new Error();
 }
}

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

router.post('/users/login', async (req, res) => {
 try {
  const user = await User.findByCredentials(req.body.email, req.body.password);
const token = await user.generateAuthToken()
  res.send({user, token});
 } catch (error) {
  res.status(400).send();
 }
});

На этот раз нам нужен метод экземпляра для пользователя.

userSchema.methods.generateAuthToken = async function() {
 const user = this;
 const token = jwt.sign({ _id: user._id.toString() }, process.env.JWT_SECRET);
 console.log(token);
 user.tokens = user.tokens.concat({ token });
 await user.save();
 return token;
};

Теперь мои тесты по-прежнему не работают, но я также изменил способ отправки своих данных. Раньше я отправлял пользователя обратно как объект response.body, теперь он должен быть на response.body.user. Итак, давайте обновим мои тесты.

describe('Authenticating Users', () => {
 beforeEach(setUpDatabase);
test('Should return user when logging in', async () => {
  const response = await request(app)
   .post(`/users/login`)
   .send({ email: userOne.email, password: userOne.password })
   .expect(200);
  expect(response.body.user._id).toBe(userOne._id.toString());
 });
test('Should return 400 if incorrect password or email', async () => {
  await request(app)
   .post(`/users/login`)
   .send({ email: userOne.email, password: '1309894138909' })
   .expect(400);
  await request(app)
   .post(`/users/login`)
   .send({ email: '[email protected]', password: userOne.password })
   .expect(400);
 });
test('Should create add a token to the tokens array on user when logging in', async () => {
  const response = await request(app)
   .post('/users/login')
   .send({ email: userOne.email, password: userOne.password });
expect(response.body.user.tokens.length).toBe(1);
  expect(await jwt.verify(response.body.user.tokens[0].token, process.env.JWT_SECRET)._id).toBe(
   userOne._id.toString()
  );
 });
});

Ладно все работает! Теперь, поскольку я отправляю пользователя обратно в пользовательское свойство response.body, я думаю, что должен сделать это согласованным для всех моих маршрутов. Пользователь всегда должен возвращаться в response.body.user, а задача всегда должна возвращаться в response.body.task. В своих маршрутах я заменю каждый res.send(user) на res.send({user}).

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

test('Should deny access to everything but login and create user routes', async () => {
  await request(app).get(`/users/${userOne._id}`).send().expect(401);
  await request(app).patch(`/users/${userOne._id}`).send().expect(401);
  await request(app).delete(`/users/${userOne._id}`).send().expect(401);
})

Хорошо, так как мы вообще начнем справляться с этим? Нам нужно подумать об экспресс промежуточном программном обеспечении. Если вы вернетесь к нашему файлу app.js, мы уже используем промежуточное ПО, cors и express.json.

app.use(cors());
app.use(express.json());

Промежуточное ПО запускается между запросами. Чтобы создать его, вам просто нужна функция с параметрами (req, res, next). next() сообщит, когда переходить к следующему, если вы никогда не напишете next(), то запрос зависнет. Итак, давайте создадим папку промежуточного программного обеспечения с файлом auth.js.

const auth = async (req, res, next) => {};
module.exports = auth;

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

const jwt = require('jsonwebtoken');
const User = require('../models/user');
const auth = async (req, res, next) => {
 try {
  const token = req.header('Authorization').replace('Bearer ', '');
  const decode = jwt.verify(token, process.env.JWT_SECRET);
  const user = await User.findOne({ _id: decode._id, 'tokens.token': token });
  if (!user) {
   throw new Error();
  }
  req.user = user;
  req.token = token;
  next();
 } catch (error) {
  res.status(401).send();
 }
};
module.exports = auth;

Так вот как выглядит мое промежуточное ПО для аутентификации. Сначала я получаю токен из заголовка авторизации. Токены отправляются в виде строк с «Bearer» перед фактическим токеном. Затем я декодирую токен, чтобы вернуть идентификатор пользователя. Затем я запрашиваю базу данных для пользователя, соответствующего этому идентификатору, и у которого также есть токен, соответствующий передаваемому токену. Если есть подходящий пользователь, я прикрепляю его к запросу и передаю вместе с токеном. Таким образом, мои маршруты, которым требуется аутентификация, могут легко получить объект пользователя, а не искать его снова.

Но если я использую app.use(auth), то все мои маршруты будут защищены аутентификацией, а это не то, что мне нужно. Вместо этого я пойду на маршруты, которым нужна авторизация, и просто передам ее в параметры функции прямо посередине. Например,

router.get('/users/:id', async (req, res) => {

становится

router.get('/users/:id', auth ,async (req, res) => {

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

Первый должен иметь возможность читать пользователей по идентификатору. Это на самом деле, вероятно, не должно быть вещью. Пользователи должны иметь возможность получать свою собственную информацию, но не информацию других пользователей. Давайте обновим его, чтобы он был маршрутом users/me вместо users/:id

let response;
 beforeEach(async () => {
  await setUpDatabase();
  let tokenResponse = await request(app)
   .post('/users/login')
   .send({ email: userOne.email, password: userOne.password });
  const token = tokenResponse.body.token;
response = await request(app)
   .get('/users/me')
   .set('Authorization', `Bearer ${token}`)
   .send();
 });
test('Should be able to get user profile when authenticated', async () => {
  expect(userOne.email).toBe(response.body.user.email);
 });

Итак, мой тест должен войти в систему, получить токен, а затем использовать этот токен для получения профиля пользователя.

А для маршрута users/me это довольно просто, так как auth уже получает пользователя.

router.get('/users/me', auth, async (req, res) => {
 res.send({ user: req.user });
});

Кроме того, я привык использовать beforeEach для всех своих тестов, но, поскольку это просто чтение данных, я могу изменить его на beforeAll, чтобы моя установка запускалась только один раз.

Хорошо, теперь обновления. Мне просто нужно изменить все запросы с :id на /me, и мне не нужно проверять недействительные идентификаторы, поскольку маршрут больше не будет принимать идентификаторы.

Мне понадобится этот токен аутентификации, поэтому давайте поместим его в файл setupTests.

const authorizedUserOneToken = async () => {
 let tokenResponse = await request(app)
  .post('/users/login')
  .send({ email: userOne.email, password: userOne.password });
 const token = tokenResponse.body.token;
 return `Bearer ${token}`;
};

Теперь мой новый тест обновления выглядит так:

test('Should be able to update user name, email, and password by id', async () => {
  const userBefore = await User.findById(userOne._id);
  const authToken = await authorizedUserOneToken();
  await request(app)
   .patch('/users/me')
   .set('Authorization', authToken)
   .send({ name: 'New Name', email: '[email protected]', password: 'newpass1234' })
   .expect(200);
const userAfter = await User.findById(userOne._id);
expect(userBefore.name).not.toBe(userAfter.name);
  expect(userBefore.email).not.toBe(userAfter.email);
  expect(userBefore.password).not.toBe(userAfter.password);
 });

Поскольку пользователь обновления также зависит от идентификатора, нам нужно исправить маршрут.

router.patch('/users/:id', auth, async (req, res) => {
 const allowedUpdates = ['name', 'email', 'password'];
 const updates = Object.keys(req.body);
 const isValidUpdate = updates.every(update => allowedUpdates.includes(update));
if (!isValidUpdate) {
  return res.status(400).send({ error: 'Invalid update request' });
 }
try {
  updates.forEach(update => (req.user[update] = req.body[update]));
  await req.user.save();
  res.send({ user: req.user });
 } catch (error) {
  res.status(400).send(error);
 }
});

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

describe('Deleting User', () => {
 beforeEach(setUpDatabase);
 test('Should delete user when authenticated', async () => {
  const authToken = await authorizedUserOneToken();
await request(app)
   .delete(`/users/me`)
   .set('Authorization', authToken)
   .send()
   .expect(200);
const user = await User.findById(userOne._id);
  expect(user).toBeNull();
 });
});

Итак, здесь мы просто удаляем пользователя.

router.delete('/users/me', auth, async (req, res) => {
 try {
  const user = await req.user.remove();
  res.send({ user });
 } catch (error) {
  res.status(400).send();
 }
});

Последнее — это тесты авторизации, которые у меня были раньше, так как я обновил маршруты, я получаю несколько ошибок 404. Меняем маршруты на /me и все должно работать.

И мы наконец закончили! У нас есть авторизация, работающая с нашими пользовательскими маршрутами, теперь мы должны работать над авторизацией маршрутов задач.