nodejs鑒權(quán)

三種常見鑒權(quán)方式

  • Session/CookieToken
  • OAuth
  • SSO
session-cookie方式
//cookie原理解析
// cookie.js
const http = require("http")
http.createServer((req, res) => {
        if (req.url === '/favicon.ico') {
            res.end('')
            return
        }
        // 觀察cookie存在
        console.log('cookie:', req.headers.cookie) // 設(shè)置cookie
        res.setHeader('Set-Cookie', 'cookie1=abc;') 
        res.end('hello cookie!!')
    })
    .listen(3000)
set-cookie.png

由于cookie的明文傳輸,而且前端很容易篡改,不是很安全,另外cookie是有容量限制的,因此可以存儲(chǔ)一個(gè)編號(hào),編號(hào)對(duì)應(yīng)的內(nèi)容就可以放在服務(wù)器端。

const session = {}
//...
  if (req.url === '/favicon.ico') {
        res.end('')
        return
    }
    // 觀察cookie存在
    console.log('cookie:', req.headers.cookie) // 設(shè)置cookie
    const sessionKey = 'sid'
    const cookie = req.headers.cookie
    if (cookie && cookie.indexOf(sessionKey) > -1) {
        res.end('Come Back ')
        // 簡(jiǎn)略寫法未必具有通用性
        const pattern = new RegExp(`${sessionKey}=([^;]+);?\s*`)
        const sid = pattern.exec(cookie)[1]
        console.log('session:', sid, session, session[sid])
    } else {
        const sid = (Math.random() * 99999999).toFixed()
        // 設(shè)置cookie
        res.setHeader('Set-Cookie', `${sessionKey}=${sid};`)
        session[sid] = { name: 'laowang' }
        res.end('Hello')
    }
//...

session會(huì)話機(jī)制是一種服務(wù)器端機(jī)制,它使用類似于哈希表(可能還有哈希表)的結(jié)構(gòu)來保存信息。

原理
image.png

實(shí)現(xiàn)原理: 1. 服務(wù)器在接受客戶端首次訪問時(shí)在服務(wù)器端創(chuàng)建seesion,然后保存seesion(我們可以將 seesion保存在內(nèi)存中,也可以保存在redis中,推薦使用后者),然后給這個(gè)session生成一 個(gè)唯一的標(biāo)識(shí)字符串,然后在響應(yīng)頭中種下這個(gè)唯一標(biāo)識(shí)字符串。2. 簽名。這一步通過秘鑰對(duì)sid進(jìn)行簽名處理,避免客戶端修改sid。(非必需步驟)3. 瀏覽器中收到請(qǐng)求響應(yīng)的時(shí)候會(huì)解析響應(yīng)頭,然后將sid保存在本地cookie中,瀏覽器在下次http請(qǐng)求的請(qǐng)求頭中會(huì)帶上該域名下的cookie信息,4. 服務(wù)器在接受客戶端請(qǐng)求時(shí)會(huì)去解析請(qǐng)求頭cookie中的sid,然后根據(jù)這個(gè)sid去找服務(wù)器端保存的該客戶端的session,然后判斷該請(qǐng)求是否合法。

koa中的session使用

koa是一個(gè)新的Web框架,致力于成為Web應(yīng)用和api開發(fā)領(lǐng)域中的一個(gè)更小,更富有表現(xiàn)力,更健壯的基石,是express的下一代基于node.js的web框架 ,完全使用Promise并配合async來實(shí)現(xiàn)異步。
特點(diǎn): 輕量 無捆綁 中間件架構(gòu) 優(yōu)雅的api設(shè)計(jì) 增強(qiáng)錯(cuò)誤處理
// 安裝: npm i koa koa-session -S

const Koa = require('koa')
const app = new Koa()
const session = require('koa-session')
// 簽名key keys作用 用來對(duì)cookie進(jìn)行簽名 
app.keys = ['some secret'];
// 配置項(xiàng)
const SESS_CONFIG = {
    key: 'kkb:sess', // cookie鍵名 
    maxAge: 86400000, // 有效期,默認(rèn)一天 
    httpOnly: true, // 僅服務(wù)器修改 
    signed: true, // 簽名cookie
};
// 注冊(cè)
app.use(session(SESS_CONFIG, app));
// 測(cè)試 app.use(ctx => {
app.use(ctx => {
    if (ctx.path === '/favicon.ico') return; // 獲取
    let n = ctx.session.count || 0;
    // 設(shè)置
    ctx.session.count = ++n;
    ctx.body = '第' + n + '次訪問';
});
app.listen(3000)

訪問http://localhost:3000/

image.png

哈希Hash - SHA MD5
  • 把一個(gè)不定長(zhǎng)摘要定長(zhǎng)結(jié)果 -摘要 yanglaoshi -> x -雪崩效應(yīng)

使用聲明一個(gè)變量的方式存儲(chǔ)session的這種方式,實(shí)際上就是存儲(chǔ)在內(nèi)存中,當(dāng)用戶訪問量增大的時(shí)候,就會(huì)導(dǎo)致內(nèi)存暴漲,而且如果服務(wù)器關(guān)機(jī),那么駐留在內(nèi)存中的session就會(huì)清空,第三點(diǎn)是服務(wù)器采用多機(jī)器部署,用戶不一定每次都會(huì)訪問到同一臺(tái)機(jī)器,基于這三種情況我們需要把session保存在一個(gè)公共的位置,不能保存在內(nèi)存中,這時(shí)候我們想到使用redis。

使用redis存儲(chǔ)session

redis是一個(gè)高性能的key-value數(shù)據(jù)庫,Redis 與其他 key - value 緩存產(chǎn)品有以下三個(gè)特點(diǎn):

  • Redis支持?jǐn)?shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保存在磁盤中,重啟的時(shí)候可以再次加載進(jìn)行使用。
  • Redis不僅僅支持簡(jiǎn)單的key-value類型的數(shù)據(jù),同時(shí)還提供list,set,zset,hash等數(shù)據(jù)結(jié)構(gòu)的存儲(chǔ)。
  • Redis支持?jǐn)?shù)據(jù)的備份,即master-slave模式的數(shù)據(jù)備份。
優(yōu)勢(shì)
  • 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
  • 豐富的數(shù)據(jù)類型 – Redis支持二進(jìn)制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 數(shù)據(jù)類型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功執(zhí)行要么失敗完全不執(zhí)行。單個(gè)操作是原子性的。多個(gè)操作也支持事務(wù),即原子性,通過MULTI和EXEC指令包起來。
  • 豐富的特性 – Redis還支持 publish/subscribe, 通知, key 過期等等特性。
// npm install redis -S
// redis.js
const redis = require('redis');
const client = redis.createClient(6379, 'localhost');
client.set('hello', 'This is a value');
client.get('hello', function (err, v) {
    console.log("redis get ", v);
})
// koa-redis.js
const redisStore = require('koa-redis');
const redis = require('redis')
const redisClient = redis.createClient(6379, "localhost");
const wrapper = require('co-redis'); //為了在中間件中使用redisStore
const client = wrapper(redisClient);
app.use(session({
    key: 'kkb:sess',
    store: redisStore({ client }) // 此處可以不必指定client
}, app));
app.use(async (ctx, next) => {
    const keys = await client.keys('*')
    keys.forEach(async key =>
        console.log(await client.get(key))
    )
    await next()
})

為什么要將session存儲(chǔ)在外部存儲(chǔ)中,Session信息未加密存儲(chǔ)在客戶端cookie中瀏覽器cookie有長(zhǎng)度限制

一個(gè)登錄鑒權(quán)驗(yàn)證的小李子??

//index.js
const Koa = require('koa')
const router = require('koa-router')()
const session = require('koa-session')
const cors = require('koa2-cors')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const app = new Koa();

//配置session的中間件
app.use(cors({
    credentials: true
}))
app.keys = ['some secret'];

app.use(static(__dirname + '/'));
app.use(bodyParser())
app.use(session(app));

app.use((ctx, next) => {
    if (ctx.url.indexOf('login') > -1) {
        next()
    } else {
        console.log('session', ctx.session.userinfo)
        if (!ctx.session.userinfo) {
            ctx.body = {
                message: "登錄失敗"
            }
        } else {
            next()
        }
    }
})

router.post('/login', async (ctx) => {
    const {
        body
    } = ctx.request
    console.log('body',body)
    //設(shè)置session
    ctx.session.userinfo = body.username;
    ctx.body = {
        message: "登錄成功"
    }
})
router.post('/logout', async (ctx) => {
    //設(shè)置session
    delete ctx.session.userinfo
    ctx.body = {
        message: "登出系統(tǒng)"
    }
})
router.get('/getUser', async (ctx) => {
    ctx.body = {
        message: "獲取數(shù)據(jù)成功",
        userinfo: ctx.session.userinfo
    }
})

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
//index.html
<html>

<head>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>

<body>
  <div id="app">
    <div>
      <input v-model="username">
      <input v-model="password">
    </div>
    <div>
      <button v-on:click="login">Login</button>
      <button v-on:click="logout">Logout</button>
      <button v-on:click="getUser">GetUser</button>
    </div>
    <div>
      <button onclick="document.getElementById('log').innerHTML = ''">Clear Log</button>
    </div>
  </div>
  <h6 id="log"></h6>
  </div>
  <script>
    // axios.defaults.baseURL = 'http://localhost:3000'
    axios.defaults.withCredentials = true
    axios.interceptors.response.use(
      response => {
        document.getElementById('log').append(JSON.stringify(response.data))
        return response;
      }
    );
    var app = new Vue({
      el: '#app',
      data: {
        username: 'test',
        password: 'test'
      },
      methods: {
        async login() {
          await axios.post('/login', {
            username: this.username,
            password: this.password
          })
        },
        async logout() {
          await axios.post('/logout')
        },
        async getUser() {
          await axios.get('/getUser')
        }
      }
    });
  </script>
</body>
</html>
login.png

利用session要求服務(wù)器本身要有狀態(tài)的,這樣實(shí)現(xiàn)起來難度比較大的,最好是我們可以提供一種服務(wù)讓后端可以沒有狀態(tài),雖然我們現(xiàn)在使用redis加一個(gè)全局的狀態(tài)保持統(tǒng)一,這樣比較適合通過分布式系統(tǒng)進(jìn)行實(shí)現(xiàn),所以這是token產(chǎn)生的一個(gè)原因,現(xiàn)在實(shí)際在前端應(yīng)用使用cookie-session的模式已經(jīng)很少了,更多的是使用token模式

token驗(yàn)證
image

1.客戶端使用用戶名和密碼請(qǐng)求登錄
2.服務(wù)端收到請(qǐng)求,去驗(yàn)證用戶名與密碼
3.驗(yàn)證成功后,服務(wù)端會(huì)簽發(fā)一個(gè)令牌(token) ,再把這個(gè)token發(fā)送給客戶端
4.客戶端收到token以后可以把它存儲(chǔ)起來,比如放在cookie里或者local storage里
5.客戶端每次向服務(wù)端請(qǐng)求資源的時(shí)候需要帶著服務(wù)端簽發(fā)的token
6.服務(wù)端收到請(qǐng)求然后去驗(yàn)證客戶端的請(qǐng)深圳市里面帶著的Token 如果驗(yàn)證成功,就向客戶端返回請(qǐng)求的數(shù)據(jù)

const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const bodyParser = require('koa-bodyparser')
const app = new Koa();
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");

const secret = "it's a secret";
app.use(bodyParser())
app.use(static(__dirname + '/'));

router.post("/login-token", async ctx => {
  const { body } = ctx.request;
  //登錄邏輯,略
  //設(shè)置session
  const userinfo = body.username;
  ctx.body = {
    message: "登錄成功",
    user: userinfo,
    // 生成 token 返回給客戶端
    token: jwt.sign(
      {
        data: userinfo,
        // 設(shè)置 token 過期時(shí)間,一小時(shí)后,秒為單位
        exp: Math.floor(Date.now() / 1000) + 60 * 60
      },
      secret
    )
  };
});

router.get(
  "/getUser-token",
  jwtAuth({
    secret
  }),
  async ctx => {
    // 驗(yàn)證通過,state.user
    console.log(ctx.state.user);
    
    //獲取session
    ctx.body = {
      message: "獲取數(shù)據(jù)成功",
      userinfo: ctx.state.user.data 
    };
  }
)

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000)
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <div id="app">
      <div>
        <input v-model="username" />
        <input v-model="password" />
      </div>
      <div>
        <button v-on:click="login">Login</button>
        <button v-on:click="logout">Logout</button>
        <button v-on:click="getUser">GetUser</button>
      </div>
      <div>
        <button @click="logs=[]">Clear Log</button>
      </div>
      <!-- 日志 -->
      <ul>
        <li v-for="(log,idx) in logs" :key="idx">
          {{ log }}
        </li>
      </ul>
    </div>
    <script>
      axios.interceptors.request.use(
        config => {
          const token = window.localStorage.getItem("token");
          if (token) {
            // 判斷是否存在token,如果存在的話,則每個(gè)http header都加上token
            // Bearer是JWT的認(rèn)證頭部信息
            config.headers.common["Authorization"] = "Bearer " + token;
          }
          return config;
        },
        err => {
          return Promise.reject(err);
        }
      );

      axios.interceptors.response.use(
        response => {
          app.logs.push(JSON.stringify(response.data));
          return response;
        },
        err => {
          app.logs.push(JSON.stringify(response.data));
          return Promise.reject(err);
        }
      );
      var app = new Vue({
        el: "#app",
        data: {
          username: "test",
          password: "test",
          logs: []
        },
        methods: {
          async login() {
            const res = await axios.post("/login-token", {
              username: this.username,
              password: this.password
            });
            localStorage.setItem("token", res.data.token);
          },
          async logout() {
            localStorage.removeItem("token");
          },
          async getUser() {
            await axios.get("/getUser-token");
          }
        }
      });
    </script>
  </body>
</html>
  • 用戶在登錄的時(shí)候,服務(wù)端生成一個(gè)Token給客戶端,客戶端后續(xù)的請(qǐng)求都要帶上這個(gè)token,服務(wù)端解析token來獲取用戶信息,并響應(yīng)用戶的請(qǐng)求,token會(huì)有過期時(shí)間,客戶端登出也會(huì)廢棄token,但服務(wù)端不會(huì)有任何操作
  • 與token簡(jiǎn)單對(duì)比
  • session要求服務(wù)端存儲(chǔ)信息,并且根據(jù)id能夠檢索,而token不需要,因?yàn)樾畔⒕驮趖oken中,這樣實(shí)現(xiàn)就實(shí)現(xiàn)了服務(wù)器端的無狀態(tài)化,在大規(guī)模的系統(tǒng)中,對(duì)每個(gè)請(qǐng)求都檢索會(huì)話信息的可能是一個(gè)復(fù)雜和耗時(shí)的過程,但另外一方面服務(wù)器要通過token來解析用戶身份也需要定義好相應(yīng)的協(xié)議,比如jwt.
  • session一般通過cookie來交互,而token方式更加靈活,可以是cookie,也可以是 header,也可以放在請(qǐng)求的內(nèi)容中。不使用cookie可以帶來跨域上的便利性。
  • token的生成方式更加多樣化,可以由第三方模塊來提供。
  • token若被盜用,服務(wù)端無法感知,cookie信息存儲(chǔ)在用戶自己電腦中,被盜用風(fēng)險(xiǎn)略小。
JWT(JSON WEB TOKEN)原理解析
  1. Bearer Token包含三個(gè)組成部分:令牌頭、payload、哈希eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NzY5NjEzNCwiaWF0Ij oxNTY3NjkyNTM0fQ.OzDruSCbXFokv1zFpkv22Z_9A JGCHG5fT_WnEaf72EA
    第三個(gè)參數(shù) ??? base64 可逆
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NjM5OTc3MSwiaWF0Ij oxNTY2Mzk2MTcxfQ.nV6sErzfZSfWtLSgebAL9nx2wg-LwyGLDRvfjQeF04U
  2. 簽名:默認(rèn)使用base64對(duì)payload編碼,使用hs256算法對(duì)令牌頭、payload和密鑰進(jìn)行簽名生成 哈希
  3. 驗(yàn)證:默認(rèn)使用hs256算法對(duì)hs256算法對(duì)令牌中數(shù)據(jù)簽名并將結(jié)果和令牌中哈希比對(duì)

OAuth(開放授權(quán))

概念:三方登入主要基本于OAuth 2.0 OAuth協(xié)議為用戶資源的授權(quán)提供了一個(gè)案例的,開放而又簡(jiǎn)易的標(biāo)準(zhǔn),與以往的授權(quán)方式不同之處是OAUTH的授權(quán)不會(huì)使第三方觸及到用戶的賬號(hào)信息,如用戶名與密碼,即第三方無需使用用戶的用戶名與密碼就可以申請(qǐng)獲得該用戶資源的授權(quán),因此OAUTH是安全的

OAUTH的登錄流程

<html>

<head>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

</head>

<body>
  <div id="app">
    <button @click='oauth()'>Login with Github</button>
    <div v-if="userInfo">
      Hello {{userInfo.name}}
      <img :src="userInfo.avatar_url" />
    </div>
  </div>
  <script>

  </script>
  <script>
    axios.interceptors.request.use(
      config => {
        const token = window.localStorage.getItem("token");
        if (token) {
          // 判斷是否存在token,如果存在的話,則每個(gè)http header都加上token
          // Bearer是JWT的認(rèn)證頭部信息
          config.headers.common["Authorization"] = "Bearer " + token;
        }
        return config;
      },
      err => {
        return Promise.reject(err);
      }
    );

    axios.interceptors.response.use(
      response => {
        app.logs.push(JSON.stringify(response.data));
        return response;
      },
      err => {
        app.logs.push(JSON.stringify(response.data));
        return Promise.reject(err);
      }
    );
    var app = new Vue({
      el: "#app",
      data: {
        logs: [],
        userInfo: null
      },
      methods: {
        async oauth() {
          window.open('/auth/github/login', '_blank')
          const intervalId = setInterval(() => {
            console.log("等待認(rèn)證中..");
            if (window.localStorage.getItem("authSuccess")) {
              clearInterval(intervalId);
              window.localStorage.removeItem("authSuccess");
              this.getUser()
            }
          }, 500);
        },
        async getUser() {
          const res = await axios.get("/auth/github/userinfo");
          console.log('res:',res.data)
          this.userInfo = res.data
        }
      }
    });
  </script>
</body>
</html>
const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const app = new Koa();
const axios = require('axios')
const querystring = require('querystring')
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
const accessTokens = {}

const secret = "it's a secret";
app.use(static(__dirname + '/'));
const config = {
    client_id: '73a4f730f2e8cf7d5fcf',
    client_secret: '74bde1aec977bd93ac4eb8f7ab63352dbe03ce48',

}

router.get('/auth/github/login', async (ctx) => {
    var dataStr = (new Date()).valueOf();
    //重定向到認(rèn)證接口,并配置參數(shù)
    var path = `https://github.com/login/oauth/authorize?${querystring.stringify({ client_id: config.client_id })}`;

    //轉(zhuǎn)發(fā)到授權(quán)服務(wù)器
    ctx.redirect(path);
})

router.get('/auth/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
    const uid = Math.random() * 99999
    accessTokens[uid] = access_token

    const token = jwt.sign(
        {
            data: uid,
            // 設(shè)置 token 過期時(shí)間,一小時(shí)后,秒為單位
            exp: Math.floor(Date.now() / 1000) + 60 * 60
        },
        secret
    )
    ctx.response.type = 'html';
    console.log('token:', token)
    ctx.response.body = ` <script>window.localStorage.setItem("authSuccess","true");window.localStorage.setItem("token","${token}");window.close();</script>`;
})

router.get('/auth/github/userinfo', jwtAuth({
    secret
}), async (ctx) => {
    // 驗(yàn)證通過,state.user
    console.log('jwt playload:', ctx.state.user)
    const access_token = accessTokens[ctx.state.user.data]
    res = await axios.get('https://api.github.com/user?access_token=' + access_token)
    console.log('userAccess:', res.data)
    ctx.body = res.data
})

app.use(router.routes()); /*啟動(dòng)路由*/
app.use(router.allowedMethods());
app.listen(7001);
單點(diǎn)登錄

...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容