常見登陸認證 DEMO

⭐️ 更多前端技術和知識點,搜索訂閱號 JS 菌 訂閱html

20190724002503.png

basic auth

basic auth 是最簡單的一種,將用戶名和密碼經過 form 表單提交的方式在 Http 的 Authorization 字段設置好併發送給後端驗證前端

要點:ios

  • 不要經過 form 提交表單的默認方式發送請求,轉而使用 fetch 或 ajax
  • 客戶端注意設置 Authorization 字段的值爲 'Basic xxx',經過該 Http 字段傳遞用戶名密碼
  • base64 的方法在客戶端要注意兼容性 btoa ,建議使用現成的庫如 'js-base64' 等,NodeJS 方面使用全局的 Buffer
  • 服務端驗證失敗後,注意返回 401,但不用返回 'WWW-Authenticate: Basic realm="..."' 避免瀏覽器出現彈窗
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>AMD</title>
</head>

<body>
  <script defer async="true" src="js/require.js" data-main="js/main"></script>
  <!-- BasicAuth -->
  <div>
    <form id="form" action="">
      <input type="text" name="username" id="username">
      <input type="password" name="password" id="password">
      <button id="login">login</button>
    </form>
  </div>
</body>

</html>
複製代碼
require.config({
  baseUrl: 'js/libs',
  paths: {
    'zepto': 'zepto.min',
  },
  shim: {
    'zepto': 'zepto',
  }
});

define(['zepto'], function ($) {
  let $form = $('#form')
  $form.on('submit', (e) => {
    e.preventDefault()
    $.ajax({
      // ajax 發送驗證請求
      type: 'POST',
      url: '/login',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Basic ' + btoa($('#username').val() + ':' + $('#password').val()),
        // 經過 Authorization 傳遞 base64 編碼後的用戶名密碼
      },
      success: function (data) {
        console.dir(data) // 回調
      }
    })
  })
});
複製代碼

(忽略上述 ajax 加 requirejs 古老的寫法 😆 )git

const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')

const app = new Koa()
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))
app.listen(8080)

router.post('/login', (ctx, next) => {
  // 省略從數據庫中提取用戶密碼
  if (ctx.get('Authorization') === 'Basic ' + Buffer('fdsa:fdsa').toString('base64')) {
    // 獲取 Authorization 字段 比對 base64 用戶名密碼
    ctx.body = 'secret'
    ctx.type = 'text/html'
    ctx.status = 200 // 匹配成功
  } else {
    ctx.status = 401 // 匹配失敗
  }
  next()
})
複製代碼

cookie auth

這種登陸方式實際上就是驗證用戶信息後,將驗證 session 存放在 session cookie 內。一旦過時就須要用戶從新登陸github

要點:web

  • session cookie 用戶信息容易被截取,須要設置 https
  • session 的會話時間內 cookie 有效,如須要長時生效須要設置過時時間 Max-age, Expires 等
const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const fs = require('fs')

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))

router.post('/login', (ctx, next) => {
  // 省略從數據庫中提取用戶密碼
  let auth = ctx.request.body
  if (auth.username === 'fdsa', auth.password === 'fdsa') {
    // session cookie驗證的用戶名和密碼屬於明文傳輸,須要 https
    ctx.cookies.set('auth', auth.username) // 沒有設置過時時間,屬於Session Cookie
    // Koa 服務端默認設置的 cookie 是 session cookie
    ctx.status = 200
    ctx.type = 'application/json'
    ctx.body = { data: 1 }
    next()
  } else {
    ctx.status = 401
    next()
  }
})

router.get('/admin', (ctx, next) => {
  if (ctx.cookies.get('auth')) {
    ctx.body = 'secret'
    ctx.status = 200
    next()
  }
})
複製代碼

SessionSigned Cookie Auth

目前經常使用的方法,針對 cookie Auth 的改進ajax

要點:算法

  • 通過簽名的 Cookie 安全性提升,要注意增強對簽名的密鑰的保護
  • 可經過每次訪問授權限限制的頁面刷新 SessionCookie
  • Koa 建議使用 koa-session 庫
const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const session = require('koa-session'); // session

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))
app.keys = ['session key'] // 簽名
app.use(session({
  key: '_session',
  signed: true, // 簽名,通過簽名的 cookie 安全性比普通 cookie 高
  maxAge: 'session' // 設置過時時間 session 表示當前會話有效
}, app))

router.post('/login', (ctx, next) => {
  // 省略從數據庫中提取用戶密碼
  let auth = ctx.request.body
  if (auth.username === 'fdsa', auth.password === 'fdsa') {
    // 登錄成功,username 結合簽名放入到 session cookie 中用於未來鑑別身份
    ctx.session.user = auth.username
    ctx.status = 200
    ctx.type = 'application/json'
    ctx.body = { data: 1 }
    next()
  } else {
    ctx.status = 401
    next()
  }
})

router.get('/admin', (ctx, next) => {
  if (ctx.session.user === 'fdsa') {
    let count = ctx.session.count || 0
    // 每次都將刷新 session cookie 存在客戶端的 session cookie 會隨着刷新動做而變化
    ctx.session.count = ++count
    ctx.body = 'visit count: ' + count
    ctx.status = 200
    next()
  } else {
    ctx.status = 401
    next()
  }
})
複製代碼

JWT token auth

此種令牌登陸方式比較主流,用戶輸入登陸信息,發送給服務器驗證,經過後返回 token,token 能夠存儲在前端任何地方。隨後用戶請求須要驗證的資源,發送 http 請求的同時將 token 放置在請求頭中,後端解析 JWT 並判斷令牌是否新鮮並有效數據庫

要點:json

  • 用戶輸入其登陸信息
  • 服務器驗證信息是否正確,並返回已簽名的token
  • token儲在客戶端,常見的是存儲在local storage中,但也能夠存儲在session或cookie中
  • 以後的HTTP請求都將token添加到請求頭裏
  • 服務器解碼JWT,而且若是令牌有效,則接受請求
  • 一旦用戶註銷,令牌將在客戶端被銷燬,不須要與服務器進行交互一個關鍵是,令牌是無狀態的。後端服務器不須要保存令牌或當前session的記錄。

1. 基本介紹

認證流程 https://jothy1023.github.io/2016/11/04/server-authentication-using-jwt/

首先,擁有某網站帳號的某 client 使用本身的帳號密碼發送 post 請求 login,因爲這是首次接觸,server 會校驗帳號與密碼是否合法,若是一致,則根據密鑰生成一個 token 並返回,client 收到這個 token 並保存在本地的 localStorage。在這以後,須要訪問一個受保護的路由或資源時,而只要附加上你保存在本地的 token(一般使用 Bearer 屬性放在 Header 的 Authorization 屬性中),server 會檢查這個 token 是否仍有效,以及其中的校驗信息是否正確,再作出相應的響應。

優勢是自包含不須要服務端儲存、無狀態客戶端銷燬便可實現用戶註銷,以及跨域、易於實現CDN,比cookie更支持原生移動端應用

JWT 的三個部分:header頭, payload載荷, signature簽名,即:xxx.yyy.zzz

header部分(base64以前):

{
    "alg": "SHA256", // algorithm 哈希算法主要有 HMAC、SHA25六、RSA等等
    "typ": "JWT" // type 令牌類型,應當設置爲 JWT
}
複製代碼

payload部分(base64以前):

三種payload聲明類型:registered, public, private,其中,registered 還包括 iss(issuer),sub(subject),aud(audience),exp(expiration time),nbf(not before),iat(issued at),jti(JWT ID)

{
    "sub": "subject id",
    "exp": "1300819380",
    "role": "admin"
}
複製代碼

signature部分

若是使用 HMACSHA256 方式:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
複製代碼

這三個部分之間加入.即完成了JWT的構造

須要注意,header部分和payload部分只是通過了base64的編碼,並未加密,不能在載荷部分保存涉及安全的東西

JWT 令牌一般經過 HTTP 的 Authorization: Bearer <token> 來傳輸,並存儲在 session cookie, localStorage 等地方

2. 例子

<!-- JWT Token SessionCookie Auth -->
  <div>
    <form id="form" action="">
      <input type="text" name="username" id="username">
      <input type="password" name="password" id="password">
      <button id="login">login</button>
    </form>
  </div>
  <!-- JWT Token LocalStorage Auth -->
  <div>
    <pre id="pre"></pre>
    <button id="getData">getData</button>
  </div>
複製代碼

server:

const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const jwt = require('jsonwebtoken')
const fs = require('fs')

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))
app.keys = ['private key']

router.post('/login', (ctx, next) => {
  // 省略從數據庫中提取用戶密碼
  if (ctx.request.body) {
    if (ctx.request.body.username === 'fdsa', ctx.request.body.password === 'fdsa') {
      // 生成 jwt token
      let token = jwt.sign({ username: 'fdsa', role: 'admin' }, app.keys[0], { algorithm: 'HS256' })
      ctx.cookies.set('koa:token', token)
      ctx.body = { data: 1, token }
      ctx.status = 200
    } else {
      ctx.body = { data: 0, err: 'error' }
      ctx.status = 401
    }
  } else {
    ctx.status = 401
  }
  next()
})

// 經過 session cookie 驗證令牌
router.get('/admin', (ctx, next) => {
  let token = ctx.cookies.get('koa:token')
  if (token) {
    // 驗證 jwt 令牌
    jwt.verify(token, app.keys[0], function (err, decoded) {
      if (err) {
        ctx.status = 401
        console.log(err)
      } else {
        ctx.body = `welcome ${decoded.role}, ${decoded.username}`
        ctx.type = 'text/html'
        ctx.status = 200
      }
    });
  } else {
    ctx.status = 401
  }
})

// 經過 Authorization 驗證令牌
router.get('/secret.json', (ctx, next) => {
  let token = ctx.get('Authorization').split(' ')[1]
  if (token) {
    jwt.verify(token, app.keys[0], function (err, decoded) {
      if (err) {
        ctx.status = 401
        console.log(err)
      } else {
        if (decoded.role === 'admin') {
          let msg = fs.readFileSync('./secret.json', 'utf-8')
          ctx.body = { data: 1, msg }
          ctx.status = 200
        } else {
          ctx.status = 401
        }
      }
    })
  } else {
    ctx.status = 401
  }
})
複製代碼

client:

require.config({
  baseUrl: 'js/libs',
  paths: {
    'zepto': 'zepto.min',
  },
  shim: {
    'zepto': 'zepto',
  }
});

(在此忽略此前寫的古老的 requireJS 🤕)

define(['zepto'], function ($) {
  let $form = $('#form')
  $form.on('submit', (e) => {
    e.preventDefault()
    $.ajax({
      // ajax 發送驗證請求
      type: 'POST',
      url: '/login',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      data: {
        username: $('#username').val(),
        password: $('#password').val()
      },
      success: function (data) {
        if (data.data === 1) {
          // 返回的token用於發起請求受限資源
          window.localStorage.setItem('koa:token', data.token)
          location.replace('./admin')
        }
      }
    })
  })

  $('#getData').on('click', (e) => {
    e.preventDefault()
    $.ajax({
      type: 'GET',
      url: '/secret.json',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Bearer ' + window.localStorage.getItem('koa:token')
        // 客戶端設置 Authorization Token 令牌
      },
      success: function (data) {
        if (data.data === 1) {
          // 令牌認證後的操做
          $('#pre').text(JSON.parse(data.msg).key)
        }
      }
    })
  })
});
複製代碼

OAuth

OAuth 是目前用的最多的登陸認證方式,用戶首先確認受權登陸,經過一連串方法獲取 access token,最後經過 token 請求各類受限的資源

阮一峯老哥的文章清除講解了這種方法的工做方式:

原理:理解OAuth 2.0 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

要點:

  • 用戶首先確認受權
  • 再獲取 code 臨時憑證
  • 經過 code 臨時憑證,換取 access token
  • 最後由 token 再獲取受限的資源

下面封裝了一個基於微博的 OAuth 認證:

let axios = require('axios');

const Koa = require('koa')
const static = require('koa-static')
const router = require('koa-better-router')().loadMethods()
const koaBody = require('koa-body')
const jwt = require('jsonwebtoken')
const fs = require('fs')

const app = new Koa()
app.listen(8080)
app.use(koaBody())
app.use(router.middleware())
app.use(static('public'))

app.keys = ['appid', 'secretid']

class WeiboApi {
  // 獲取 code 臨時兌換券
  constructor(query) {
    this.code = query.code
  }
  // 根據 code 獲取 token
  getToken() {
    return new Promise((resolve, reject) => {
      axios({
        method: 'POST',
        url: `https://api.weibo.com/oauth2/access_token?client_id=${app.keys[0]}&client_secret=${app.keys[1]}&grant_type=authorization_code&redirect_uri=http://127.0.0.1:8080/auth&code=${this.code}`
      }).then(d => { resolve(d) }).catch(e => { reject(e) })
    })
  }
  // 根據 token 獲取 相關的用戶信息
  getUserInfo(token) {
    return new Promise((resolve, reject) => {
      axios({
        method: 'GET',
        url: `https://api.weibo.com/2/users/show.json?access_token=${token.data.access_token}&uid=${token.data.uid}`
      }).then(d => { resolve(d) }).catch(e => { reject(e) })
    })
  }
  // 根據 token 獲取 用戶的關注人列表
  getUserFriends(token) {
    return new Promise((resolve, reject) => {
      axios({
        method: 'GET',
        url: `https://api.weibo.com/2/friendships/friends.json?access_token=${token.data.access_token}&uid=${token.data.uid}`
      }).then(d => { resolve(d) }).catch(e => { reject(e) })
    })
  }
}

router.get('/auth', async (ctx, next) => {
  if (ctx.query.code) {
    let weiboApi = new WeiboApi(ctx.request.query)
    let token = await weiboApi.getToken()
    let userInfo = await weiboApi.getUserInfo(token)
    let userFriends = await weiboApi.getUserFriends(token)
    // 根據用戶信息,查詢數據庫,登陸邏輯
    ctx.body = { userInfo: userInfo.data, userFriends: userFriends.data }
  } else {
    ctx.status = 401
  }
})
複製代碼
<!-- OAuth2.0 Weibo -->
  <a href="https://api.weibo.com/oauth2/authorize?client_id=HEREISYOURAPPID&response_type=code&redirect_uri=http://127.0.0.1:8080/auth">微博登陸</a>
複製代碼

JS 菌公衆帳號

請關注個人訂閱號,不按期推送有關 JS 的技術文章,只談技術不談八卦 😊

相關文章
相關標籤/搜索