先後端常見的幾種鑑權方式

1、四種鑑權方式

目前咱們經常使用的鑑權有四種:javascript

  • HTTP Basic Authentication
  • session-cookie
  • Token 驗證
  • OAuth(開放受權)

2、HTTP Basic Authentication

這種受權方式是瀏覽器遵照http協議實現的基本受權方式,HTTP協議進行通訊的過程當中,HTTP協議定義了基本認證容許HTTP服務器對客戶端進行用戶身份證的方法。html

認證過程:前端

一、 客戶端向服務器請求數據,請求的內容多是一個網頁或者是一個ajax異步請求,此時,假設客戶端還沒有被驗證,則客戶端提供以下請求至服務器:java

Get /index.html HTTP/1.0 
  Host:www.google.com
複製代碼

二、 服務器向客戶端發送驗證請求代碼401,(WWW-Authenticate: Basic realm=」google.com」這句話是關鍵,若是沒有客戶端不會彈出用戶名和密碼輸入界面)服務器返回的數據大抵以下:node

HTTP/1.0 401 Unauthorised 
  Server: SokEvo/1.0 
  WWW-Authenticate: Basic realm=」google.com」 
  Content-Type: text/html 
  Content-Length: xxx
複製代碼

三、 當符合http1.0或1.1規範的客戶端(如IE,FIREFOX)收到401返回值時,將自動彈出一個登陸窗口,要求用戶輸入用戶名和密碼。jquery

四、 用戶輸入用戶名和密碼後,將用戶名及密碼以BASE64加密方式加密,並將密文放入前一條請求信息中,則客戶端發送的第一條請求信息則變成以下內容:ios

Get /index.html HTTP/1.0 
  Host:www.google.com 
  Authorization: Basic d2FuZzp3YW5n
複製代碼

注:d2FuZzp3YW5n表示加密後的用戶名及密碼(用戶名:密碼 而後經過base64加密,加密過程是瀏覽器默認的行爲,不須要咱們人爲加密,咱們只須要輸入用戶名密碼便可)git

五、 服務器收到上述請求信息後,將 Authorization 字段後的用戶信息取出、解密,將解密後的用戶名及密碼與用戶數據庫進行比較驗證,如用戶名及密碼正確,服務器則根據請求,將所請求資源發送給客戶端程序員

效果: 客戶端未未認證的時候,會彈出用戶名密碼輸入框,這個時候請求時屬於 pending 狀態,當用戶輸入用戶名密碼的時候客戶端會再次發送帶 Authentication 頭的請求。github

認證成功:

server.js

let express = require("express");
let app = express();
 
  app.use(express.static(__dirname+'/public'));
 
  app.get("/Authentication_base",function(req,res){
    console.log('req.headers.authorization:',req.headers)
    if(!req.headers.authorization){
      res.set({
        'WWW-Authenticate':'Basic realm="wang"'
      });
      res.status(401).end();
    }else{
      let base64 = req.headers.authorization.split(" ")[1];
      let userPass = new Buffer(base64, 'base64').toString().split(":");
      let user = userPass[0];
      let pass = userPass[1];
      if(user=="wang"&&pass="wang"){
        res.end("OK");
      }else{
        res.status(401).end();
      }
 
    }
 
  })
 
  app.listen(9090)
複製代碼

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>HTTP Basic Authentication</title>
  </head>
  <body>
    <div></div>
    <script src="js/jquery-3.2.1.js"></script>
    <script> $(function(){ send('./Authentication_base'); }) var send = function(url){ $.ajax({ url : url, method : 'GET', }); } </script>
  </body>
</html>
複製代碼

優勢:   基本認證的一個優勢是基本上全部流行的網頁瀏覽器都支持基本認證。基本認證不多在可公開訪問的互聯網網站上使用,有時候會在小的私有系統中使用(如路由器網頁管理接口)。後來的機制HTTP摘要認證是爲替代基本認證而開發的,容許密鑰以相對安全的方式在不安全的通道上傳輸。   程序員和系統管理員有時會在可信網絡環境中使用基本認證,使用Telnet或其餘明文網絡協議工具手動地測試Web服務器。這是一個麻煩的過程,可是網絡上傳輸的內容是人可讀的,以便進行診斷。

缺點:   雖然基本認證很是容易實現,但該方案建立在如下的假設的基礎上,即:客戶端和服務器主機之間的鏈接是安全可信的。特別是,若是沒有使用SSL/TLS這樣的傳輸層安全的協議,那麼以明文傳輸的密鑰和口令很容易被攔截。該方案也一樣沒有對服務器返回的信息提供保護。   現存的瀏覽器保存認證信息直到標籤頁或瀏覽器被關閉,或者用戶清除歷史記錄。HTTP沒有爲服務器提供一種方法指示客戶端丟棄這些被緩存的密鑰。這意味着服務器端在用戶不關閉瀏覽器的狀況下,並無一種有效的方法來讓用戶註銷。

3、session-cookie

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

3.2 session

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

3.3 session-cookie認證

  1. 服務器在接受客戶端首次訪問時在服務器端建立seesion,而後保存seesion(咱們能夠將seesion保存在 內存中,也能夠保存在redis中,推薦使用後者),而後給這個session生成一個惟一的標識字符串,而後在 響應頭中種下這個惟一標識字符串。
  2. 簽名。這一步經過祕鑰對sid進行簽名處理,避免客戶端修改sid。(非必需步驟)
  3. 瀏覽器中收到請求響應的時候會解析響應頭,而後將sid保存在本地cookie中,瀏覽器在下次http請求的請求頭中會帶上該域名下的cookie信息。
  4. 服務器在接受客戶端請求時會去解析請求頭cookie中的sid,而後根據這個sid去找服務器端保存的該客戶端的session,而後判斷該請求是否合法。

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

3.4 redis

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

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

3.5 用戶登陸認證

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

在koa中作一個驗證的中間件,在須要驗證的接口中使用該中間件。

//前端代碼
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: "登出系統"
  }
})
複製代碼

4、Token

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

4.1 Token認證流程

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

4.2 Token和session的區別

session-cookie的缺點: (1)認證方式侷限於在瀏覽器中使用,cookie 是瀏覽器端的機制,若是在app端就沒法使用 cookie。 (2)爲了知足全局一致性,咱們最好把 session 存儲在 redis 中作持久化,而在分佈式環境下,咱們可能須要在每一個服務器上都備份,佔用了大量的存儲空間。 (3)在不是 Https 協議下使用 cookie ,容易受到 CSRF 跨站點請求僞造攻擊。

token的缺點: (1)加密解密消耗使得 token 認證比 session-cookie 更消耗性能。 (2)tokensessionId 大,更佔帶寬。

二者對比,它們的區別顯而易見: (1)token 認證不侷限於 cookie ,這樣就使得這種認證方式能夠支持多種客戶端,而不只是瀏覽器。且不受同源策略的影響。 (2)不使用 cookie 就能夠規避CSRF攻擊。 (3)token 不須要存儲,token 中已包含了用戶信息,服務器端變成無狀態,服務器端只須要根據定義的規則校驗這個 token 是否合法就行。這也使得 token 的可擴展性更強。

4.3 JWT(JSON Web Token)

基於 token 的解決方案有許多,經常使用的是JWTJWT 的原理是,服務器認證之後,生成一個 JSON 對象,這個 JSON 對象確定不能裸傳給用戶,那誰均可以篡改這個對象發送請求。所以這個 JSON 對象會被服務器端簽名加密後返回給用戶,返回的內容就是一張令牌,之後用戶每次訪問服務器端就帶着這張令牌。

這個 JSON 對象可能包含的內容就是用戶的信息,用戶的身份以及令牌的過時時間。

4.3.1 JWT的組成部分

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

它由三部分組成: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 三個部分拼成一個字符串,每一個部分之間用"點"(.)分隔,就能夠返回給用戶。
4.3.2 JWT的特色
  1. JWT 默認是不加密,但也是能夠加密的。生成原始 Token 之後,能夠用密鑰再加密一次。
  2. JWT 不加密的狀況下,不能將祕密數據寫入 JWT。
  3. JWT 不只能夠用於認證,也能夠用於交換信息。有效使用 JWT,能夠下降服務器查詢數據庫的次數。
  4. JWT 的最大缺點是,因爲服務器不保存 session 狀態,所以沒法在使用過程當中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期以前就會始終有效,除非服務器部署額外的邏輯。
  5. JWT 自己包含了認證信息,一旦泄露,任何人均可以得到該令牌的全部權限。爲了減小盜用,JWT 的有效期應該設置得比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證。
  6. 爲了減小盜用,JWT 不該該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。
4.3.3 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不須要存儲,只要服務端能拿着密鑰解析出用戶信息,就說明該用戶是合法的。
//若要更進一步的權限驗證,須要判斷解析出的用戶身份是管理員仍是普通用戶。
複製代碼

5、OAuth(開放受權)

OAuth(Open Authorization)是一個開放標準,容許用戶受權第三方網站訪問他們存儲在另外的服務提供者上的信息,而不須要將用戶名和密碼提供給第三方網站或分享他們數據的全部內容,爲了保護用戶數據的安全和隱私,第三方網站訪問用戶數據前都須要顯式的向用戶徵求受權。咱們常見的提供OAuth認證服務的廠商有支付寶,QQ,微信。

OAuth協議又有1.0和2.0兩個版本。相比較1.0版,2.0版整個受權驗證流程更簡單更安全,也是目前最主要的用戶身份驗證和受權方式。

關於OAuth相關文章,能夠查看 OAuth 2.0 的一個簡單解釋理解OAuth 2.0OAuth 2.0 的四種方式

5.1 OAuth認證流程

OAuth就是一種受權機制。數據的全部者告訴系統,贊成受權第三方應用進入系統,獲取這些數據。系統從而產生一個短時間的進入令牌(token),用來代替密碼,供第三方應用使用。

OAuth有四種獲取令牌的方式,無論哪種受權方式,第三方應用申請令牌以前,都必須先到系統備案,說明本身的身份,而後會拿到兩個身份識別碼:客戶端 ID(client ID)和客戶端密鑰(client secret)。這是爲了防止令牌被濫用,沒有備案過的第三方應用,是不會拿到令牌的。

在先後端分離的情境下,咱們常使用受權碼方式,指的是第三方應用先申請一個受權碼,而後再用該碼獲取令牌。

5.2 GitHub第三方登陸示例

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

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

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

  1. 此時在你的第三方網站就能夠提供一個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=""/> `

})
複製代碼

OAuth受權的登錄流程圖:

6、原文

www.lishuaishuai.com/nodejs/1167…

相關文章
相關標籤/搜索