三種常見鑒權(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是有容量限制的,因此可以存儲一個編號嫉柴,編號對應(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 ')
// 簡略寫法未必具有通用性
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會話機制是一種服務(wù)器端機制钾虐,它使用類似于哈希表(可能還有哈希表)的結(jié)構(gòu)來保存信息眼五。
原理
實現(xiàn)原理: 1. 服務(wù)器在接受客戶端首次訪問時在服務(wù)器端創(chuàng)建seesion,然后保存seesion(我們可以將 seesion保存在內(nèi)存中植康,也可以保存在redis中旷太,推薦使用后者),然后給這個session生成一 個唯一的標(biāo)識字符串,然后在響應(yīng)頭中種下這個唯一標(biāo)識字符串销睁。2. 簽名泳秀。這一步通過秘鑰對sid進行簽名處理,避免客戶端修改sid榄攀。(非必需步驟)3. 瀏覽器中收到請求響應(yīng)的時候會解析響應(yīng)頭嗜傅,然后將sid保存在本地cookie中,瀏覽器在下次http請求的請求頭中會帶上該域名下的cookie信息檩赢,4. 服務(wù)器在接受客戶端請求時會去解析請求頭cookie中的sid吕嘀,然后根據(jù)這個sid去找服務(wù)器端保存的該客戶端的session违寞,然后判斷該請求是否合法。
koa中的session使用
koa是一個新的Web框架偶房,致力于成為Web應(yīng)用和api開發(fā)領(lǐng)域中的一個更小趁曼,更富有表現(xiàn)力,更健壯的基石棕洋,是express的下一代基于node.js的web框架 挡闰,完全使用Promise并配合async來實現(xiàn)異步。
特點: 輕量 無捆綁 中間件架構(gòu) 優(yōu)雅的api設(shè)計 增強錯誤處理
// 安裝: npm i koa koa-session -S
const Koa = require('koa')
const app = new Koa()
const session = require('koa-session')
// 簽名key keys作用 用來對cookie進行簽名
app.keys = ['some secret'];
// 配置項
const SESS_CONFIG = {
key: 'kkb:sess', // cookie鍵名
maxAge: 86400000, // 有效期掰盘,默認(rèn)一天
httpOnly: true, // 僅服務(wù)器修改
signed: true, // 簽名cookie
};
// 注冊
app.use(session(SESS_CONFIG, app));
// 測試 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
- 把一個不定長摘要定長結(jié)果 -摘要 yanglaoshi -> x -雪崩效應(yīng)
使用聲明一個變量的方式存儲session的這種方式摄悯,實際上就是存儲在內(nèi)存中,當(dāng)用戶訪問量增大的時候愧捕,就會導(dǎo)致內(nèi)存暴漲奢驯,而且如果服務(wù)器關(guān)機,那么駐留在內(nèi)存中的session就會清空次绘,第三點是服務(wù)器采用多機器部署瘪阁,用戶不一定每次都會訪問到同一臺機器,基于這三種情況我們需要把session保存在一個公共的位置邮偎,不能保存在內(nèi)存中管跺,這時候我們想到使用redis。
使用redis存儲session
redis是一個高性能的key-value數(shù)據(jù)庫禾进,Redis 與其他 key - value 緩存產(chǎn)品有以下三個特點:
- Redis支持?jǐn)?shù)據(jù)的持久化伙菜,可以將內(nèi)存中的數(shù)據(jù)保存在磁盤中,重啟的時候可以再次加載進行使用命迈。
- Redis不僅僅支持簡單的key-value類型的數(shù)據(jù)贩绕,同時還提供list,set壶愤,zset淑倾,hash等數(shù)據(jù)結(jié)構(gòu)的存儲。
- Redis支持?jǐn)?shù)據(jù)的備份征椒,即master-slave模式的數(shù)據(jù)備份娇哆。
優(yōu)勢
- 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
- 豐富的數(shù)據(jù)類型 – Redis支持二進制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 數(shù)據(jù)類型操作勃救。
- 原子 – Redis的所有操作都是原子性的碍讨,意思就是要么成功執(zhí)行要么失敗完全不執(zhí)行。單個操作是原子性的蒙秒。多個操作也支持事務(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存儲在外部存儲中,Session信息未加密存儲在客戶端cookie中瀏覽器cookie有長度限制
一個登錄鑒權(quá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)的马澈,這樣實現(xiàn)起來難度比較大的,最好是我們可以提供一種服務(wù)讓后端可以沒有狀態(tài)弄息,雖然我們現(xiàn)在使用redis加一個全局的狀態(tài)保持統(tǒng)一痊班,這樣比較適合通過分布式系統(tǒng)進行實現(xiàn),所以這是token產(chǎn)生的一個原因摹量,現(xiàn)在實際在前端應(yīng)用使用cookie-session的模式已經(jīng)很少了涤伐,更多的是使用token模式
token驗證
1.客戶端使用用戶名和密碼請求登錄
2.服務(wù)端收到請求,去驗證用戶名與密碼
3.驗證成功后缨称,服務(wù)端會簽發(fā)一個令牌(token) ,再把這個token發(fā)送給客戶端
4.客戶端收到token以后可以把它存儲起來凝果,比如放在cookie里或者local storage里
5.客戶端每次向服務(wù)端請求資源的時候需要帶著服務(wù)端簽發(fā)的token
6.服務(wù)端收到請求然后去驗證客戶端的請深圳市里面帶著的Token 如果驗證成功,就向客戶端返回請求的數(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 過期時間,一小時后骂删,秒為單位
exp: Math.floor(Date.now() / 1000) + 60 * 60
},
secret
)
};
});
router.get(
"/getUser-token",
jwtAuth({
secret
}),
async ctx => {
// 驗證通過,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宁玫,如果存在的話,則每個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>
- 用戶在登錄的時候欧瘪,服務(wù)端生成一個Token給客戶端,客戶端后續(xù)的請求都要帶上這個token匙赞,服務(wù)端解析token來獲取用戶信息,并響應(yīng)用戶的請求涌庭,token會有過期時間芥被,客戶端登出也會廢棄token,但服務(wù)端不會有任何操作
- 與token簡單對比
- session要求服務(wù)端存儲信息坐榆,并且根據(jù)id能夠檢索拴魄,而token不需要,因為信息就在token中席镀,這樣實現(xiàn)就實現(xiàn)了服務(wù)器端的無狀態(tài)化匹中,在大規(guī)模的系統(tǒng)中,對每個請求都檢索會話信息的可能是一個復(fù)雜和耗時的過程豪诲,但另外一方面服務(wù)器要通過token來解析用戶身份也需要定義好相應(yīng)的協(xié)議顶捷,比如jwt.
- session一般通過cookie來交互,而token方式更加靈活屎篱,可以是cookie焊切,也可以是 header扮授,也可以放在請求的內(nèi)容中。不使用cookie可以帶來跨域上的便利性专肪。
- token的生成方式更加多樣化刹勃,可以由第三方模塊來提供。
- token若被盜用嚎尤,服務(wù)端無法感知荔仁,cookie信息存儲在用戶自己電腦中,被盜用風(fēng)險略小芽死。
JWT(JSON WEB TOKEN)原理解析
- Bearer Token包含三個組成部分:令牌頭乏梁、payload、哈希eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NzY5NjEzNCwiaWF0Ij oxNTY3NjkyNTM0fQ.OzDruSCbXFokv1zFpkv22Z_9A JGCHG5fT_WnEaf72EA
第三個參數(shù) ??? base64 可逆
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NjM5OTc3MSwiaWF0Ij oxNTY2Mzk2MTcxfQ.nV6sErzfZSfWtLSgebAL9nx2wg-LwyGLDRvfjQeF04U - 簽名:默認(rèn)使用base64對payload編碼关贵,使用hs256算法對令牌頭遇骑、payload和密鑰進行簽名生成 哈希
- 驗證:默認(rèn)使用hs256算法對hs256算法對令牌中數(shù)據(jù)簽名并將結(jié)果和令牌中哈希比對
OAuth(開放授權(quán))
概念:三方登入主要基本于OAuth 2.0 OAuth協(xié)議為用戶資源的授權(quán)提供了一個案例的,開放而又簡易的標(biāo)準(zhǔn)揖曾,與以往的授權(quán)方式不同之處是OAUTH的授權(quán)不會使第三方觸及到用戶的賬號信息落萎,如用戶名與密碼,即第三方無需使用用戶的用戶名與密碼就可以申請獲得該用戶資源的授權(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练链,如果存在的話,則每個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 過期時間奴拦,一小時后,秒為單位
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) => {
// 驗證通過绿鸣,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()); /*啟動路由*/
app.use(router.allowedMethods());
app.listen(7001);
單點登錄
...