三種常見鑒權方式
- 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) // 設置cookie
res.setHeader('Set-Cookie', 'cookie1=abc;')
res.end('hello cookie!!')
})
.listen(3000)
由于cookie的明文傳輸,而且前端很容易篡改,不是很安全,另外cookie是有容量限制的,因此可以存儲一個編號,編號對應的內容就可以放在服務器端。
const session = {}
//...
if (req.url === '/favicon.ico') {
res.end('')
return
}
// 觀察cookie存在
console.log('cookie:', req.headers.cookie) // 設置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()
// 設置cookie
res.setHeader('Set-Cookie', `${sessionKey}=${sid};`)
session[sid] = { name: 'laowang' }
res.end('Hello')
}
//...
session會話機制是一種服務器端機制,它使用類似于哈希表(可能還有哈希表)的結構來保存信息。
原理
實現原理: 1. 服務器在接受客戶端首次訪問時在服務器端創建seesion,然后保存seesion(我們可以將 seesion保存在內存中,也可以保存在redis中,推薦使用后者),然后給這個session生成一 個唯一的標識字符串,然后在響應頭中種下這個唯一標識字符串。2. 簽名。這一步通過秘鑰對sid進行簽名處理,避免客戶端修改sid。(非必需步驟)3. 瀏覽器中收到請求響應的時候會解析響應頭,然后將sid保存在本地cookie中,瀏覽器在下次http請求的請求頭中會帶上該域名下的cookie信息,4. 服務器在接受客戶端請求時會去解析請求頭cookie中的sid,然后根據這個sid去找服務器端保存的該客戶端的session,然后判斷該請求是否合法。
koa中的session使用
koa是一個新的Web框架,致力于成為Web應用和api開發領域中的一個更小,更富有表現力,更健壯的基石,是express的下一代基于node.js的web框架 ,完全使用Promise并配合async來實現異步。
特點: 輕量 無捆綁 中間件架構 優雅的api設計 增強錯誤處理
// 安裝: 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, // 有效期,默認一天
httpOnly: true, // 僅服務器修改
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;
// 設置
ctx.session.count = ++n;
ctx.body = '第' + n + '次訪問';
});
app.listen(3000)
哈希Hash - SHA MD5
- 把一個不定長摘要定長結果 -摘要 yanglaoshi -> x -雪崩效應
使用聲明一個變量的方式存儲session的這種方式,實際上就是存儲在內存中,當用戶訪問量增大的時候,就會導致內存暴漲,而且如果服務器關機,那么駐留在內存中的session就會清空,第三點是服務器采用多機器部署,用戶不一定每次都會訪問到同一臺機器,基于這三種情況我們需要把session保存在一個公共的位置,不能保存在內存中,這時候我們想到使用redis。
使用redis存儲session
redis是一個高性能的key-value數據庫,Redis 與其他 key - value 緩存產品有以下三個特點:
- Redis支持數據的持久化,可以將內存中的數據保存在磁盤中,重啟的時候可以再次加載進行使用。
- Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
- Redis支持數據的備份,即master-slave模式的數據備份。
優勢
- 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
- 豐富的數據類型 – Redis支持二進制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 數據類型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功執行要么失敗完全不執行。單個操作是原子性的。多個操作也支持事務,即原子性,通過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有長度限制
一個登錄鑒權驗證的小李子??
//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)
//設置session
ctx.session.userinfo = body.username;
ctx.body = {
message: "登錄成功"
}
})
router.post('/logout', async (ctx) => {
//設置session
delete ctx.session.userinfo
ctx.body = {
message: "登出系統"
}
})
router.get('/getUser', async (ctx) => {
ctx.body = {
message: "獲取數據成功",
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要求服務器本身要有狀態的,這樣實現起來難度比較大的,最好是我們可以提供一種服務讓后端可以沒有狀態,雖然我們現在使用redis加一個全局的狀態保持統一,這樣比較適合通過分布式系統進行實現,所以這是token產生的一個原因,現在實際在前端應用使用cookie-session的模式已經很少了,更多的是使用token模式
token驗證
1.客戶端使用用戶名和密碼請求登錄
2.服務端收到請求,去驗證用戶名與密碼
3.驗證成功后,服務端會簽發一個令牌(token) ,再把這個token發送給客戶端
4.客戶端收到token以后可以把它存儲起來,比如放在cookie里或者local storage里
5.客戶端每次向服務端請求資源的時候需要帶著服務端簽發的token
6.服務端收到請求然后去驗證客戶端的請深圳市里面帶著的Token 如果驗證成功,就向客戶端返回請求的數據
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;
//登錄邏輯,略
//設置session
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
)
};
});
router.get(
"/getUser-token",
jwtAuth({
secret
}),
async ctx => {
// 驗證通過,state.user
console.log(ctx.state.user);
//獲取session
ctx.body = {
message: "獲取數據成功",
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的認證頭部信息
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>
- 用戶在登錄的時候,服務端生成一個Token給客戶端,客戶端后續的請求都要帶上這個token,服務端解析token來獲取用戶信息,并響應用戶的請求,token會有過期時間,客戶端登出也會廢棄token,但服務端不會有任何操作
- 與token簡單對比
- session要求服務端存儲信息,并且根據id能夠檢索,而token不需要,因為信息就在token中,這樣實現就實現了服務器端的無狀態化,在大規模的系統中,對每個請求都檢索會話信息的可能是一個復雜和耗時的過程,但另外一方面服務器要通過token來解析用戶身份也需要定義好相應的協議,比如jwt.
- session一般通過cookie來交互,而token方式更加靈活,可以是cookie,也可以是 header,也可以放在請求的內容中。不使用cookie可以帶來跨域上的便利性。
- token的生成方式更加多樣化,可以由第三方模塊來提供。
- token若被盜用,服務端無法感知,cookie信息存儲在用戶自己電腦中,被盜用風險略小。
JWT(JSON WEB TOKEN)原理解析
- Bearer Token包含三個組成部分:令牌頭、payload、哈希eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NzY5NjEzNCwiaWF0Ij oxNTY3NjkyNTM0fQ.OzDruSCbXFokv1zFpkv22Z_9A JGCHG5fT_WnEaf72EA
第三個參數 ??? base64 可逆
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NjM5OTc3MSwiaWF0Ij oxNTY2Mzk2MTcxfQ.nV6sErzfZSfWtLSgebAL9nx2wg-LwyGLDRvfjQeF04U - 簽名:默認使用base64對payload編碼,使用hs256算法對令牌頭、payload和密鑰進行簽名生成 哈希
- 驗證:默認使用hs256算法對hs256算法對令牌中數據簽名并將結果和令牌中哈希比對
OAuth(開放授權)
概念:三方登入主要基本于OAuth 2.0 OAuth協議為用戶資源的授權提供了一個案例的,開放而又簡易的標準,與以往的授權方式不同之處是OAUTH的授權不會使第三方觸及到用戶的賬號信息,如用戶名與密碼,即第三方無需使用用戶的用戶名與密碼就可以申請獲得該用戶資源的授權,因此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的認證頭部信息
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("等待認證中..");
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();
//重定向到認證接口,并配置參數
var path = `https://github.com/login/oauth/authorize?${querystring.stringify({ client_id: config.client_id })}`;
//轉發到授權服務器
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,
// 設置 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);
單點登錄
...