三種常見鑒權(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)

由于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)來保存信息。
原理

實(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)

哈希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>

利用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)證

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)原理解析
- Bearer Token包含三個(gè)組成部分:令牌頭、payload、哈希eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NzY5NjEzNCwiaWF0Ij oxNTY3NjkyNTM0fQ.OzDruSCbXFokv1zFpkv22Z_9A JGCHG5fT_WnEaf72EA
第三個(gè)參數(shù) ??? base64 可逆
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NjM5OTc3MSwiaWF0Ij oxNTY2Mzk2MTcxfQ.nV6sErzfZSfWtLSgebAL9nx2wg-LwyGLDRvfjQeF04U - 簽名:默認(rèn)使用base64對(duì)payload編碼,使用hs256算法對(duì)令牌頭、payload和密鑰進(jìn)行簽名生成 哈希
- 驗(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)登錄
...