三种常见鉴权方式
- 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);
单点登录
...