前端鑑權(Cookie/Session、Token和OAuth)

  先後端未分離之前,頁面都是經過後臺來渲染的,能不能訪問到頁面直接由後臺邏輯判斷。先後端分離之後,頁面的元素由頁面自己來控制,因此頁面間的路由是由前端來控制了。固然,僅有前端作權限控制是遠遠不夠的,後臺還須要對每一個接口作驗證。
  爲何前端作權限控制是不夠的呢?由於前端的路由控制僅僅是視覺上的控制,前端能夠隱藏某個頁面或者某個按鈕,可是發送請求的方式仍是有不少,徹底能夠跳過操做頁面來發送某個請求。因此就算前端的權限控制作的很是嚴密,後臺依舊須要驗證每一個接口。
  前端的權限控制主要有三種:路由控制(路由的跳轉)、視圖控制(按鈕級別)和請求控制(請求攔截器)。這幾種方式以後再詳談,前端作完權限控制,後臺仍是須要驗證每個接口,這就是鑑權。如今先後端配合鑑權的方式主要有如下幾種:html

  1. session-cookie
  2. Token 驗證(JWT)
  3. OAuth(開放受權)

session-cookie

cookie

  Http協議是一個無狀態的協議,服務器不會知道究竟是哪一臺瀏覽器訪問了它,所以須要一個標識用來讓服務器區分不一樣的瀏覽器。cookie就是這個管理服務器與客戶端之間狀態的標識。
  cookie的原理是,瀏覽器第一次向服務器發送請求時,服務器在response頭部設置Set-Cookie字段,瀏覽器收到響應就會設置cookie並存儲,在下一次該瀏覽器向服務器發送請求時,就會在request頭部自動帶上Cookie字段,服務器端收到該cookie用以區分不一樣的瀏覽器。固然,這個cookie與某個用戶的對應關係應該在第一次訪問時就存在服務器端,這時就須要session了。前端

const http = require('http')
http.createServer((req, res) => {
  if (req.url === '/favicon.ico') {
    return
  } else {
    res.setHeader('Set-Cookie', 'name=zhunny')
    res.end('Hello Cookie')
  }
}).listen(3000) 
複製代碼

session

  session是會話的意思,瀏覽器第一次訪問服務端,服務端就會建立一次會話,在會話中保存標識該瀏覽器的信息。它與cookie的區別就是session是緩存在服務端的,cookie 則是緩存在客戶端,他們都由服務端生成,爲了彌補Http協議無狀態的缺陷。ios

session-cookie認證

  1. 服務器在接受客戶端首次訪問時在服務器端建立seesion,而後保存seesion(咱們能夠將seesion保存在 內存中,也能夠保存在redis中,推薦使用後者),而後給這個session生成一個惟一的標識字符串,而後在 響應頭中種下這個惟一標識字符串。
  2. 簽名。這一步經過祕鑰對sid進行簽名處理,避免客戶端修改sid。(非必需步驟)
  3. 瀏覽器中收到請求響應的時候會解析響應頭,而後將sid保存在本地cookie中,瀏覽器在下次http請求的 請求頭中會帶上該域名下的cookie信息。
  4. 服務器在接受客戶端請求時會去解析請求頭cookie中的sid,而後根據這個sid去找服務器端保存的該客 戶端的session,而後判斷該請求是否合法。
    session-cookie
const http = require('http')
//此時session存在內存中
const session = {}
http.createServer((req, res) => {
  const sessionKey = 'sid'
  if (req.url === '/favicon.ico') {
    return
  } else {
    const cookie = req.headers.cookie
    //再次訪問,對sid請求進行認證
    if (cookie && cookie.indexOf(sessionKey) > -1) {
      res.end('Come Back')
    }
    //首次訪問,生成sid,保存在服務器端
    else {
      const sid = (Math.random() * 9999999).toFixed()
      res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
      session[sid] = { name: 'zhunny' }
      res.end('Hello Cookie')
    }
  }
}).listen(3000)
複製代碼

redis

  redis是一個鍵值服務器,能夠專門放session的鍵值對。如何在koa中使用session:git

const koa = require('koa')
const app = new koa()
const session = require('koa-session')

const redisStore = require('koa-redis')
const redis = require('redis')
const redisClient = redis.createClient(6379, 'localhost')

const wrapper = require('co-redis')
const client = wrapper(redisClient)

//加密sessionid
app.keys = ['session secret']

const SESS_CONFIG = {
  key: 'kbb:sess',
  //此時讓session存儲在redis中
  store: redisStore({ client })
}

app.use(session(SESS_CONFIG, app))

app.use(ctx => {
  //查看redis中的內容
  redisClient.keys('*', (errr, keys) => {
    console.log('keys:', keys)
    keys.forEach(key => {
      redisClient.get(key, (err, val) => {
        console.log(val)
      })
    })
  })
  if (ctx.path === '/favicon.ico') return
  let n = ctx.session.count || 0
  ctx.session.count = ++n
  ctx.body = `第${n}次訪問`
})

app.listen(3000)
複製代碼

用戶登陸認證

  使用session-cookie作登陸認證時,登陸時存儲session,退出登陸時刪除session,而其餘的須要登陸後才能操做的接口須要提早驗證是否存在session,存在才能跳轉頁面,不存在則回到登陸頁面。
  在koa中作一個驗證的中間件,在須要驗證的接口中使用該中間件。github

//前端代碼
async login() {
    await axios.post('/login', {
        username: this.username,
        password: this.password
    })
},
async logout() {
    await axios.post('/logout')
},
async getUser() {
    await axios.get('/getUser')
}
複製代碼
//中間件 auth.js
module.exports = async (ctx, next) => {
  if (!ctx.session.userinfo) {
    ctx.body = {
      ok: 0,
      message: "用戶未登陸" };
  } else {
    await next();
} };
//須要驗證的接口
router.get('/getUser', require('auth'), async (ctx) => {
  ctx.body = {
    message: "獲取數據成功",
    userinfo: ctx.session.userinfo
  }
})
//登陸
router.post('/login', async (ctx) => {
  const {
    body
  } = ctx.request
  console.log('body', body)
  //設置session
  ctx.session.userinfo = body.username;
  ctx.body = {
    message: "登陸成功"
  }
})
//登出
router.post('/logout', async (ctx) => {
  //設置session
  delete ctx.session.userinfo
  ctx.body = {
    message: "登出系統"
  }
})
複製代碼

Token

  token是一個令牌,瀏覽器第一次訪問服務端時會簽發一張令牌,以後瀏覽器每次攜帶這張令牌訪問服務端就會認證該令牌是否有效,只要服務端能夠解密該令牌,就說明請求是合法的,令牌中包含的用戶信息還能夠區分不一樣身份的用戶。通常token由用戶信息、時間戳和由hash算法加密的簽名構成。web

Token認證流程

  1. 客戶端使用用戶名跟密碼請求登陸
  2. 服務端收到請求,去驗證用戶名與密碼
  3. 驗證成功後,服務端會簽發一個 Token,再把這個 Token 發送給客戶端
  4. 客戶端收到 Token 之後能夠把它存儲起來,好比放在 Cookie 裏或者 Local Storage 裏
  5. 客戶端每次向服務端請求資源的時候須要帶着服務端簽發的 Token
  6. 服務端收到請求,而後去驗證客戶端請求裏面帶着的 Token(request頭部添加Authorization),若是驗證成功,就向客戶端返回請求的數據,若是不成功返回401錯誤碼,鑑權失敗。

Token和session的區別

  1. session-cookie的缺點:(1)認證方式侷限於在瀏覽器中使用,cookie是瀏覽器端的機制,若是在app端就沒法使用cookie。(2)爲了知足全局一致性,咱們最好把session存儲在redis中作持久化,而在分佈式環境下,咱們可能須要在每一個服務器上都備份,佔用了大量的存儲空間。(3)在不是Https協議下使用cookie,容易受到CSRF跨站點請求僞造攻擊。
  2. token的缺點:(1)加密解密消耗使得token認證比session-cookie更消耗性能。(2)token比sessionId大,更佔帶寬。
  3. 二者對比,它們的區別顯而易見:(1)token認證不侷限於cookie,這樣就使得這種認證方式能夠支持多種客戶端,而不只是瀏覽器。且不受同源策略的影響。(2)不使用cookie就能夠規避CSRF攻擊。(3)token不須要存儲,token中已包含了用戶信息,服務器端變成無狀態,服務器端只須要根據定義的規則校驗這個token是否合法就行。這也使得token 的可擴展性更強。

JWT(JSON Web Token)

  JWT 的原理是,服務器認證之後,生成一個 JSON 對象,這個JSON對象確定不能裸傳給用戶,那誰均可以篡改這個對象發送請求。所以這個JSON對象會被服務器端簽名加密後返回給用戶,返回的內容就是一張令牌,之後用戶每次訪問服務器端就帶着這張令牌。
  這個JSON對象可能包含的內容就是用戶的信息,用戶的身份以及令牌的過時時間。面試

JWT的組成部分

  在該網站JWT,能夠解碼或編碼一個JWT。一個JWT形如: redis


  它由三部分組成:Header(頭部)、Payload(負載)、Signature(簽名)。

  1. Header部分是一個JSON對象,描述JWT的元數據。通常描述信息爲該Token的加密算法以及Token的類型。{"alg": "HS256","typ": "JWT"}的意思就是,該token使用HS256加密,token類型是JWT。這個部分基本至關於明文,它將這個JSON對象作了一個Base64轉碼,變成一個字符串。Base64編碼解碼是有算法的,解碼過程是可逆的。頭部信息默認攜帶着兩個字段。
  2. Payload 部分也是一個 JSON 對象,用來存放實際須要傳遞的數據。有7個官方字段,還能夠在這個部分定義私有字段。通常存放用戶名、用戶身份以及一些JWT的描述字段。它也只是作了一個Base64編碼,所以確定不能在其中存放祕密信息,好比說登陸密碼之類的。
  3. Signature是對前面兩個部分的簽名,防止數據篡改,若是前面兩段信息被人修改了發送給服務器端,此時服務器端是可利用簽名來驗證信息的正確性的。簽名須要密鑰,密鑰是服務器端保存的,用戶不知道。算出簽名之後,把 Header、Payload、Signature 三個部分拼成一個字符串,每一個部分之間用"點"(.)分隔,就能夠返回給用戶。
JWT的特色
  1. JWT 默認是不加密,但也是能夠加密的。生成原始 Token 之後,能夠用密鑰再加密一次。
  2. JWT 不加密的狀況下,不能將祕密數據寫入 JWT。
  3. JWT 不只能夠用於認證,也能夠用於交換信息。有效使用 JWT,能夠下降服務器查詢數據庫的次數。
  4. JWT 的最大缺點是,因爲服務器不保存 session 狀態,所以沒法在使用過程當中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期以前就會始終有效,除非服務器部署額外的邏輯。
  5. JWT 自己包含了認證信息,一旦泄露,任何人均可以得到該令牌的全部權限。爲了減小盜用,JWT 的有效期應該設置得比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證。
  6. 爲了減小盜用,JWT 不該該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。
JWT驗證用戶登陸
//前端代碼
//axios的請求攔截器,在每一個request請求頭上加JWT認證信息
axios.interceptors.request.use(
    config => {
        const token = window.localStorage.getItem("token");
        if (token) {
        // 判斷是否存在token,若是存在的話,則每一個http header都加上token
        // Bearer是JWT的認證頭部信息
            config.headers.common["Authorization"] = "Bearer " + token;
        }
        return config;
    },
    err => {
        return Promise.reject(err);
    }
);
//登陸方法:在將後端返回的JWT存入localStorage
async login() {
    const res = await axios.post("/login-token", {
        username: this.username,
        password: this.password
    });
    localStorage.setItem("token", res.data.token);
},
//登出方法:刪除JWT
async logout() {
    localStorage.removeItem("token");
},
async getUser() {
    await axios.get("/getUser-token");
}
複製代碼
//後端代碼
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
//用來簽名的密鑰
const secret = "it's a secret";

router.post("/login-token", async ctx => {
  const { body } = ctx.request;
  //登陸邏輯,略,即查找數據庫,若該用戶和密碼合法,即將其信息生成一個JWT令牌傳給用戶
  const userinfo = body.username;
  ctx.body = {
    message: "登陸成功",
    user: userinfo,
    // 生成 token 返回給客戶端
    token: jwt.sign(
      {
        data: userinfo,
        // 設置 token 過時時間,一小時後,秒爲單位
        exp: Math.floor(Date.now() / 1000) + 60 * 60
      },
      secret
    )
  };
});

//jwtAuth這個中間件會拿着密鑰解析JWT是否合法。
//而且把JWT中的payload的信息解析後放到state中,ctx.state用於中間件的傳值。
router.get(
  "/getUser-token",
  jwtAuth({
    secret
  }),
  async ctx => {
    // 驗證經過,state.user
    console.log(ctx.state.user);
    ctx.body = {
      message: "獲取數據成功",
      userinfo: ctx.state.user.data 
    };
  }
)
//這種密碼學的方式使得token不須要存儲,只要服務端能拿着密鑰解析出用戶信息,就說明該用戶是合法的。
//若要更進一步的權限驗證,須要判斷解析出的用戶身份是管理員仍是普通用戶。
複製代碼

OAuth

  三方登入主要基於OAuth 2.0。OAuth協議爲用戶資源的受權提供了一個安全的、開放而又簡易的標 準。與以往的受權方式不一樣之處是OAuth的受權不會使第三方觸及到用戶的賬號信息(如用戶名與密碼), 即第三方無需使用用戶的用戶名與密碼就能夠申請得到該用戶資源的受權,所以OAuth是安全的。咱們常見的提供OAuth認證服務的廠商有支付寶、QQ、微信。這樣的受權方式使得用戶使用門檻低,能夠更好的推廣本身的應用。
  OAuth相關文章推薦阮一峯老師的一系列文章OAuth 2.0 算法

OAuth認證流程

  OAuth就是一種受權機制。數據的全部者告訴系統,贊成受權第三方應用進入系統,獲取這些數據。系統從而產生一個短時間的進入令牌(token),用來代替密碼,供第三方應用使用。
  OAuth有四種獲取令牌的方式,無論哪種受權方式,第三方應用申請令牌以前,都必須先到系統備案,說明本身的身份,而後會拿到兩個身份識別碼:客戶端 ID(client ID)和客戶端密鑰(client secret)。這是爲了防止令牌被濫用,沒有備案過的第三方應用,是不會拿到令牌的。
  在先後端分離的情境下,咱們常使用受權碼方式,指的是第三方應用先申請一個受權碼,而後再用該碼獲取令牌。數據庫

GitHub第三方登陸示例

  咱們用例子來理清受權碼方式的流程。

  1. 在GitHub中備案第三方應用,拿到屬於它的客戶端ID和客戶端密鑰。

  在github-settings-developer settings中建立一個OAuth App。並填寫相關內容。填寫完成後Github會給你一個客戶端ID和客戶端密鑰。


2. 此時在你的第三方網站就能夠提供一個Github登陸連接,用戶點擊該連接後會跳轉到Github。這一步拿着客戶端ID向Github請求受權碼code。

const config = {
  client_id: '28926186082164bbea8f',
  client_secret: '07c4fdae1d5ca458dae3345b6d77a0add5a785ca'
}

router.get('/github/login', async (ctx) => {
  var dataStr = (new Date()).valueOf();
  //重定向到認證接口,並配置參數
  var path = "https://github.com/login/oauth/authorize";
  path += '?client_id=' + config.client_id;

  //轉發到受權服務器
  ctx.redirect(path);
})
複製代碼
  1. 用戶跳轉到Github,輸入Github的用戶名密碼,表示用戶贊成使用Github身份登陸第三方網站。此時就會帶着受權碼code跳回第三方網站。跳回的地址在建立該OAuth時已經設置好了。http://localhost:3000/github/callback
  2. 第三方網站收到受權碼,就能夠拿着受權碼、客戶端ID和客戶端密鑰去向Github請求access_token令牌。
  3. Github收到請求,向第三方網站頒發令牌。
  4. 第三方網站收到令牌,就能夠暫時擁有Github一些請求的權限,好比說拿到用戶信息,拿到這個用戶信息以後就能夠構建本身第三方網站的token,作相關的鑑權操做。
router.get('/github/callback', async (ctx) => {
  console.log('callback..')
  const code = ctx.query.code;
  const params = {
    client_id: config.client_id,
    client_secret: config.client_secret,
    code: code
  }
  let res = await axios.post('https://github.com/login/oauth/access_token', params)
  const access_token = querystring.parse(res.data).access_token
  res = await axios.get('https://api.github.com/user?access_token=' + access_token)
  console.log('userAccess:', res.data)
  ctx.body = ` <h1>Hello ${res.data.login}</h1> <img src="${res.data.avatar_url}" alt=""/> `

})
複製代碼

參考

cookie,session傻傻分不清楚?
把cookie聊清楚
先後端常見的幾種鑑權方式
前端面試查漏補缺--(十) 前端鑑權
談前端權限
OAuth 2.0 的四種方式

相關文章
相關標籤/搜索