前言
前后端未分离以前,⻚⾯都是通过后台来渲染的,能不能访问到⻚⾯直接由后台逻辑判断。前后端分离以后,⻚⾯的元素由⻚⾯本身来控制,所以⻚⾯间的路由是由前端来控制了。当然,仅有前端做权限控制是远远不够的,后台还需要对每个接⼝做验证。
为什么前端做权限控制是不够的呢?因为前端的路由控制仅仅是视觉上的控制,前端可以隐藏某个⻚⾯或者某个按钮,但是发送请求的⽅式还是有很多,完全可以跳过操作⻚⾯来发送某个请求。所以就算前端的权限控制做的⾮常严密,后台依旧需要验证每个接⼝。
前端的权限控制主要有三种:路由控制(路由的跳转)、视图控制(按钮级别)和请求控制(请求拦截器)。前端做完权限控制,后台还是需要验证每⼀个接⼝,这就是鉴权。现在前后端配合鉴权的⽅式主要有以下⼏种:
- session-cookie
- Token验证(JWT)
- OAuth(开放授权)
Session/Cookie
cookie
Http协议是⼀个⽆状态的协议,服务器不会知道到底是哪⼀台浏览器访问了它,因此需要⼀个标识⽤来让服务器区分不同的浏览器。cookie就是这个管理服务器与客户端之间状态的标识。
cookie的原理是,浏览器第⼀次向服务器发送请求时,服务器在response头部设置Set-Cookie字段,浏览器收到响应就会设置cookie并存储,在下⼀次该浏览器向服务器发送请求时,就会在request头部⾃动带上Cookie字段,服务器端收到该cookie⽤以区分不同的浏览器。当然,这个cookie与某个⽤户的对应关系应该在第⼀次访问时就存在服务器端,这时就需要session了。
session
session是会话的意思,浏览器第⼀次访问服务端,服务端就会创建⼀次会话,在会话中保存标识该浏览器的信息。它与 cookie的区别就是session是缓存在服务端的,cookie 则是缓存在客户端,他们都由服务端⽣成,为了弥补Http协议⽆状态的缺陷。
session-cookie认证
原理:
- 服务器在接受客户端⾸次访问时在服务器端创建 seesion,然后保存seesion(我们可以将seesion保存在内存中,也可以保存在redis中,推荐使⽤后者),然后给这个session⽣成⼀个唯⼀的标识字符串,然后在响应头中设置这个唯⼀标识字符串。
- 签名。这⼀步通过秘钥对sid进⾏签名处理,避免客户端修改sid。(⾮必需步骤)
- 浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求的请求头中会带上该域名下的cookie信息。
- 服务器在接受客户端请求时会去解析请求头cookie中的 sid,然后根据这个sid去找服务器端保存的该客户端的 session,然后判断该请求是否合法。
session的签名使用
安装
cnpm i koa-session -S 使用
//引入
const koaSession = require('koa-session');
// 模拟签名
// app.keys 签名的 cookie 密钥数组,用来对cookie进行签名
app.keys = ['session secret'];
//设置签名
const SESSION_CONFIG = {
key: 'alex', //设置 cookie 的 key 名字
maxAge: 86400, //设置有效期
httpOnly: true, //仅服务器修改,防止客户端篡改签名
signed: true, //签名cookie
}
// 参数:配置 需要添加配置的实例
app.use(koaSession(SESSION_CONFIG, app));
//测试 session-cookie
app.use(async ctx => {
let n = ctx.session.count || 0;
ctx.session.count = ++n;
ctx.body = `第${n}次访问`;
})
将session的数据存入到redis
redis主要用于存储缓存性的数据.
安装
首先电脑上也要下载 redis.
cnpm i redis koa-redis -S 使用
//引入 redis
const redis = require('redis');
//导入仓库
const redisStore = require('koa-redis');
//redis创建客户端
const client = redis.createClient(6379, 'localhost');
//配置
//设置签名
const SESSION_CONFIG = {
key: 'alex', //设置 cookie 的 key 名字
maxAge: 86400, //设置有效期
httpOnly: true, //仅服务器修改,防止客户端篡改签名
signed: true, //签名cookie
store: redisStore({
...client
}), //配置缓存到redis
}
// 参数:配置 需要添加配置的实例
app.use(koaSession(SESSION_CONFIG, app));
//测试 session-cookie
app.use(async ctx => {
let n = ctx.session.count || 0;
ctx.session.count = ++n;
ctx.body = `第${n}次访问`;
//从redis中读取数据
client.keys('*', (err, keys) => {
console.log(keys);
keys.forEach(key => {
client.get(key, (err, val) => {
console.log(val);
})
});
})
})
session/cookie实现用户登录鉴权
前端通过cookie来存储用户的登录信息,后端通过session来判断用户是否登录.
前端
// 登录方法
async handleLogin() {
let res = await axios.post('/user/login', {
user: this.user,
pwd: this.pwd
});
console.log(res);
},
// 退出方法
async handleLogout() {
await axios.post('/user/logout');
},
// 获取用户信息
async getUser() {
let res = await axios.get('/user/getUser');
console.log(res);
}
user.js
const Router = require('koa-router');
//实例化并设置前缀
const router = new Router({
prefix: '/user'
});
//登录
router.post('/login', async (ctx, next) => {
// console.log(ctx.request.body);
const {
body
} = ctx.request;
console.log(body);
//验证数据库,存储
ctx.session.userInfo = body.user;
ctx.body = {
ok: 1,
message: '登录成功'
}
})
//获取用户,导航守卫,认证
//会先执行 require 导入的 auth 函数
router.get('/getUser', require('../middleware/auth'), async (ctx, next) => {
ctx.body = {
ok: 1,
message: '获取数据成功',
user: ctx.session.userInfo
}
})
//退出
router.post('/logout', async (ctx, next) => {
delete ctx.session.userInfo;
ctx.body = {
ok: 1,
message: '退出成功'
}
})
auth.js
若用户没有登录,就返回未登录信息,否则直接放行。
module.exports = async (ctx, next) => {
if (!ctx.session.userInfo) {
ctx.body = {
ok: 0,
message: '用户未登录'
}
} else {
//放行
await next();
}
}
Token+JWT认证
下载
npm i koa-jwt -S
npm i jsonwebtoken -S
登录认证
前端
async handleLogin() {
const res = await axios.post('/user/login-token', {
user: this.user,
pwd: this.pwd
})
//存储到本地
localStorage.setItem('token', res.data.token);
},
async handleLogout() {
localStorage.removeItem('token');
},
async getUser() {
await axios.get('/user/getUser-token')
}
user.js
const Router = require('koa-router');
const jwt = require('jsonwebtoken'); //生成令牌的模块
const jwtAuth = require('koa-jwt'); //认证令牌的模块
const secret = 'this is a secret'; //签名
//实例化并设置前缀
const router = new Router({
prefix: '/user'
});
//token认证
router.post('/login-token', async (ctx, next) => {
const {
body
} = ctx.request;
console.log(body);
const userInfo = body.user;
//省略...登录操作
ctx.body = {
ok: 1,
message: '登录成功',
user: userInfo,
//生成令牌
token: jwt.sign({
data: userInfo, //由于签名不是加密的,令牌不要存放敏感数据
exp: Math.floor(Date.now() / 1000) + 60 * 60, //过期时间1小时
}, secret /*签名*/ )
}
})
//获取用户信息
router.get('/getUser-token', jwtAuth( /*认证*/ {
secret
}), async (ctx, next) => {
ctx.body = {
ok: 1,
message: '数据获取成功',
userInfo: ctx.state.user.data, //在生成令牌的同时将数据保存到了state中
}
})
如果需要对更多的路由进行认证,可以将 jwtAuth({secret}) 提升到更高层级(server.js),同时,也可以通过 jwtAuth({secret}).unless([ ]) 将不需要验证的路由写入数组中
OAuth(开放授权)
有的应⽤会提供第三⽅应⽤登录,⽐如掘⾦ web 客户端提供了 微信、QQ账号登录,我们可以不⽤注册掘⾦账号,⽽可以⽤已有的微信账号登录掘⾦。看看⽤微信登录掘⾦的过程:
step1: 打开掘⾦,未登录状态,点击登录,掘⾦给我们弹出⼀个登录框,上⾯有微信、QQ登录选项,我们选择微信登录;
step2: 之后掘⾦会将我们重定向到微信的登录⻚⾯,这个⻚⾯给出⼀个⼆维码供我们扫描,扫描之后;
step3: 我们打开微信,扫描微信给的⼆维码之后,微信询问我们是否同意掘⾦使⽤我们的微信账号信息,我们点击同意;
step4: 掘⾦刚才重定向到微信的⼆维码⻚⾯,现在我们同意掘⾦使⽤我们的微信账号信息之后,⼜重定向回掘⾦的⻚⾯,同时我们可以看到现在掘⾦的⻚⾯上显示我们已经处于登录状态,所以我们已经完成了⽤微信登录掘⾦的过程
这个过程⽐我们注册掘⾦后才能登录要快捷多了。这归功于OAuth2.0 ,它允许客户端应⽤(掘⾦)可以访问我们的资源服务器(微信),我们就是资源的拥有者,这需要我们允许客户端(掘⾦)能够通过认证服务器(在这⾥指微信,认证服务器和资源服务器可以分开也可以是部署在同⼀个服务上)的认证。OAuth 2.0 提供了4种⻆⾊,资源服务器、资源的拥有者、客户端应⽤和认证服务器,它们之间的交流实现了OAuth 2.0 整个认证授权的过程。
原理图:
- 第一次模拟发起浏览器请求是获取令牌
- 第二次模拟发起浏览器请求是携带令牌获取数据
使用github实现OAuth登录授权
首先需要去github上登记一个应用
- 参考教程 前端
<a href="/user/login-github">github登录</a>
user.js
安装 axios
cnpm i axios -S
const Router = require('koa-router');
const axios = require('axios');
const querystring = require('querystring'); //引入内置的库
//实例化并设置前缀
const router = new Router({
prefix: '/user'
});
// OAuth 授权登录
const config = {
// id 和 密钥
client_id: 'ca9133d5deda0d821d4d',
client_secret: '4ede50d2e312f8526c79e83640a386ad745a23ad'
}
router.get('/login-github', async (ctx, next) => {
// 重定向到认证接⼝,并配置参数
const path = `https://github.com/login/oauth/authorize?client_id=${config.client_id}`;
// 转发到授权服务器
ctx.redirect(path);
})
router.get('/oauth/github/callback', async ctx => { //这是一个授权回调,用于获取授权码 code
const code = ctx.query.code; // GitHub 回调传回 code 授权码
// 带着 授权码code、client_id、client_secret 向 GitHub 认证服务器请求 token
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;
// 带着 token 从 GitHub 获取用户信息
// res = await axios.get('https://api.github.com/user?access_token=' + access_token);
res = await axios({
method: 'get',
url: `https://api.github.com/user`,
headers: {
accept: 'application/json',
Authorization: `token ${access_token}`
}
});
console.log('userAccess', res.data);
ctx.redirect('/hello.html');
})