json веб-токен практика входа в систему и проверка кода подтверждения

Node.js GraphQL

В прошлом году я написал введениеjwtизстатья.

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

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

Кроме того, у меня естьиспытательное полеЭто практиковалось, но текущий интерфейсный код относительно прост, и в случае сбоя нет подсказки обратной связи. Что касается того, почему интерфейс написан плохо, то это полностью потому, что объем кода во фронтенде слишком велик по сравнению с бэкендом...

Кроме того, если вы знакомы с graphql, вы также можетеgraphql-playgroundчтобы увидеть эффект.

Адрес этой статьиГоры.Специализация/пост/Прослушивание - Пресс...

Отправить код подтверждения

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

const verifyCode = Array.from(Array(6), () => parseInt((Math.random() * 10))).join('')

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

Мы знаем, что jwt только проверяет целостность данных, но не шифрует их. В это время при сопряжении почтового ящика пользователя и кода подтверждения, но если обаpayloadв, покаjwtЕсли данные передаются в виде обычного текста, код проверки будет утерян.

// 放到明文中,校验码泄露
jwt.sign({ email, verifyCode }, config.jwtSecret, { expiresIn: '30m' })

Так как же сделать так, чтобы проверочный код не утек, а данные можно было проверить корректно?

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

// 再给个半小时的过期时间
const token = jwt.sign({ email }, config.jwtSecret + verifyCode, { expiresIn: '30m' })

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

type Mutation {
  # 发送邮件
  # 返回一个 token,注册时需要携带 token,用以校验验证码
  sendEmailVerifyCode (
    email: String! @constraint(format: "email")
  ): String!
}
const Mutation = {
  async sendEmailVerifyCode (root, { email }, { email: emailService }) {
    // 生成六个随机数
    const verifyCode = Array.from(Array(6), () => parseInt((Math.random() * 10))).join('')
    // TODO 可以放到消息队列里,但是没有多少量,而且本 Mutation 还有限流,其实目前没啥必要...
    // 与打点一样,不关注结果
    emailService.send({
      to: email, 
      subject: '【诗词弦歌】账号安全——邮箱验证',
      html: `您正在进行邮箱验证,本次请求的验证码为:<span style="color:#337ab7">${verifyCode}</span>(为了保证您帐号的安全性,请在30分钟内完成验证)\n\n诗词弦歌团队`
    })
    return jwt.sign({ email }, config.jwtSecret + verifyCode, { expiresIn: '30m' })
  }
}

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

  1. Как думать об асинхронных и синхронных службах, если почта обслуживается службой
  2. Обработка очереди сообщений, отправка писем не требует надежности, больше похоже на UDP
  3. Чтобы пользователи не могли отправлять большое количество писем за короткий промежуток времени, как реализовать ограничение скорости (RateLimit)

Не по теме, как правило, перед отправкой электронных писем или текстовых сообщений на мобильный телефон требуется код подтверждения изображения для проверки подлинности пользователя и ограничения тока. Код проверки изображения также можно реализовать через jwt.

регистр

Регистрация намного проще.Проверьте почтовый ящик на наличие входящих данных от клиента.После успешной проверки вы можете сразу сохранить их на складе.Следующееgraphqlкод

type Mutation {
  # 注册
  createUser (
    name: String!
    password: String!
    email: String! @constraint(format: "email")
    verifyCode: String!
    # 发送邮件传给客户端的 token
    token: String!
  ): User!
}
const Mutation = {
  async createUser (root, { name, password, email, verifyCode, token }, { models }) {
    const { email: verifyEmail } = jwt.verify(token, config.jwtSecret + verifyCode)
    if (email !== verifyEmail) {
      throw new Error('请输入正确的邮箱') 
    }
    const user = await models.users.create({
      name,
      email,
      // 入库时密码做了加盐处理
      password: hash(password)
    })
    return user
  }
}

Вот деталь, пароль, используемый для хранилищаMD5с параметромsaltнеобратимый

function hash (str) {
  return crypto.createHash('md5').update(`${str}-${config.salt}`, 'utf8').digest('hex')
}

не по теме,saltЭто возможноJWTизsecretустановить на ту же строку?

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

Код проверки реализован традиционными методами и сравнивается с jwt

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

Авторизоваться

один сjwtавторизоватьсяgraphqlкод, поставитьuser_idиuser_roleположить в полезную нагрузку

type Mutation {
  # 登录,如果返回 null,则登录失败
  createUserToken (
    email: String! @constraint(format: "email")
    password: String!
  ): String
}
const Mutation = {
  async createUserToken (root, { email, password }, { models }) {
    const user = await models.users.findOne({
      where: {
        email,
        password: hash(password)
      },
      attributes: ['id', 'role'],
      raw: true
    })
    if (!user) {
      // 返回空代表用户登录失败
      return
    }
    return jwt.sign(user, config.jwtSecret, { expiresIn: '1d' })
  }
}

Обратите внимание на общедоступный номерГорная Луна Путешествие, зафиксируйте мой технический рост, добро пожаловать в общение

欢迎关注公众号山月行,记录我的技术成长,欢迎交流