json web token 實踐登陸以及校驗碼驗證

去年我寫了一篇介紹 jwt文章javascript

文章指出若是沒有特別的用戶註銷及單用戶多設備登陸的需求,可使用 jwt,而 jwt 的最大的特徵就是無狀態,且不加密。html

除了用戶登陸方面外,還可使用 jwt 驗證郵箱驗證碼,其實也能夠驗證手機驗證碼,可是鑑於我囊中羞澀,只能驗證郵箱了。前端

另外,我已在個人試驗田進行了實踐,不過目前前端代碼寫的比較簡陋,甚至沒有失敗的回饋提示。至於爲何前端寫的簡陋,徹底是由於前端的代碼量相比後端來說實在過於龐大...java

另外,若是你熟悉 graphql,也能夠在本項目的 graphql-playground 中查看效果。數據庫

本文地址 shanyue.tech/post/jwt-an…後端

發送驗證碼

校驗以前,須要配合一個隨機數供郵箱和短信發送。使用如下代碼片斷生成一個六位數字的隨機碼,你也能夠把它包裝爲一個函數安全

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

若是使用傳統有狀態的解決方案,此時須要在服務端維護一個用戶郵箱及隨機碼的鍵值對,而使用 jwt 也須要給前端返回一個 token,隨後用來校驗驗證碼。bash

咱們知道 jwt 只會校驗數據的完整性,而不對數據加密。此時當拿用戶郵箱及校驗碼配對時,可是若是都放到 payload 中,而 jwt 使用明文傳輸數據,校驗碼會被泄露dom

// 放到明文中,校驗碼泄露
jwt.sign({ email, verifyCode }, config.jwtSecret, { expiresIn: '30m' })
複製代碼

那如何保證校驗碼不被泄露,並且可以正確校驗數據呢異步

咱們知道 secret 是不會被泄露的,此時把校驗碼放到 secret 中,完成配對

// 再給個半小時的過時時間
const token = jwt.sign({ email }, config.jwtSecret + verifyCode, { expiresIn: '30m' })
複製代碼

在服務端發送郵件的同時,把 token 再傳遞給前端,隨註冊時再發送到後端進行驗證,這是我項目中關於校驗的 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 是否能夠與 JWTsecret 設置爲同一字符串?

再題外話,這裏的輸入正確郵箱的 Error 明顯不該該發送至 Sentry (報警系統),而有的 Error 的信息能夠直接顯示在前端,如何對 Error 進行規範與分類

校驗碼由傳統方法實現與 jwt 比較

若是使用傳統方法,只須要一個 key/value 數據庫,維護手機號/郵箱與檢驗碼的對應關係便可實現,相比 jwt 而言要簡單不少。

登陸

一個用 jwt 實現登陸的 graphql 代碼,把 user_iduser_role 置於 payload 中

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' })
  }
}
複製代碼

關注公衆號山月行,記錄個人技術成長,歡迎交流

歡迎關注公衆號山月行,記錄個人技術成長,歡迎交流
相關文章
相關標籤/搜索