后端为什么要做鉴权
前后端分离以前,页面都是通过后台来渲染的,能不能访问页面直接由后台逻辑判断,前后端分离以后,页面元素由页面本身控制,所以页面间的路由是前端来控制的,当然,仅有前端做权限控制还是不够,后台还需要对每个接口进行验证
前端做权限控制是不够的,因为前端的路由仅仅是视觉上的控制,前端可以隐藏某个页面或者按钮,但是发送请求的方式还有很多,完全可以跳过操作页面来发送某个请求,所以就算前端的权限控制做的非常严密,后台依旧需要验证每个接口
前端的权限控制主要分三种,路由控制-跳转、视图控制-按钮级别 和 请求控制-请求拦截器
前端做完权限控制,后台还是需要验证每一个接口,这就是鉴权,现在前后端配合鉴权的方式主要有以下几种
- session-cookie
- Token验证 ( JWT )
- OAuth ( 开放验证 )
Session/Cookie
cookie
http协议是一个无状态协议,服务器不知道是哪台浏览器访问了它,因此需要一个标识来让服务器区分每个浏览器,cookie就是这个管理服务器与客户端之间的状态标识
当浏览器第一次向服务器发送请求时,服务器在res头部设置set-cookie字段,浏览器收到响应就会设置cookie并存储,在下一次该浏览器向服务器发送请求时,就会在req头部带上cookie字段,服务器收到该cookie用以区分不同的浏览器,当然,这个cookie与某个用户的段颖关系应该在第一次访问时就存在服务器端了,这时就需要session了
session
session是会话的意思,浏览器第一次访问服务端,服务端就会创建一次会话,在会话中标识该浏览器信息,它与cookie的区别是session是缓存在服务端的,cookie则是缓存在客户端,他们都由服务端生成,为了弥补http协议无状态的缺陷
session - cookie认证
session签名的使用
npm i koa-session -s
server.js
const Koa = require('koa')
const app = new Koa()
const session = require('koa-session')
// keys的签名:用来给cookie进行签名,原本为加密字符串,这里为理解简化
app.keys = ['session secret', 'anthor secret']
const SESSION_CONFIG = {
key: 'Max:sess', // 设置cookie的key名字
maxAge: 8640000, // 表示有效期,单位秒,这里为一天
httpOnly: true, // 仅服务器修改
signed: true // 签名cookie
}
// 作为中间件导入,写在静态资源目录和路由中间
app.use(session(SESSION_CONFIG, app))
// 测试
app.use(async ctx => {
// 记录每一次访问,如果空则为0
let n = ctx.session.count || 0
// 每次访问 +n
ctx.session.count = ++n
ctx.body = `第${n}次访问`
})
app.listen(3000, function(){
console.log('3000端口监听中');
})
将session存入redis数据库中
nas安装redis数据库
桌面新建 txt 写入以下配置,更改文件名为redis.conf
databases 16
maxmemory 1gb
maxmemory-policy allkeys-lru
File Station 创建目录上传配置文件
群辉Docker 注册表搜 redis,安装last版本
打开映像 → 修改项目名 → 高级设置 → 存储空间添加配置文件和data目录
本地端口与容器端口一致
环境命令写入 redis-server /usr/local/etc/redis/redis.conf
TIPS:redis数据库data数据默认就是一个/data/dump.rdb文件,需要备份的话保存缓存到此文件,备份此文件即可;需要恢复的话停掉docker redis 替换掉 data/dump.rdb即可,如图:
koa连接redis数据库
npm i redis koa-redis -s
连接nas的redis数据库失败,弃用
Token + JWT认证
token获得用户名和密码后传给服务端,服务端不需要将用户名和密码存放到redis中, 服务器会在当前路由下创建一个JWT签名秘钥,类似与一个令牌,返回给浏览器,浏览器一般会把令牌存放到localStorage中,当浏览器再次发送请求时,浏览器把令牌放在请求头给服务器端,服务器会拿这边的令牌进行对比,如果一致,直接放行,相对于session,token不需要存放到数据库中,对服务器压力小
安装
// jwt中间件
npm i koa-jwt -s
// 用于生成token下发给浏览器
npm i jsonwebtoken -s
目录如下
server.js 服务器
const Koa = require('koa')
const static = require('koa-static')
const bodyparser = require('koa-bodyparser')
const app = new Koa()
app.use(bodyparser())
app.use(static(__dirname + '/public'))
// 导入路由
const routerUser = require('./router/user')
// 注册路由
app.use(routerUser.routes()) // 中间件导入路由对象
app.use(routerUser.allowedMethods()) // 根据ctx.state 设置response响应头
app.use(async ctx => {
ctx.body = 'Hello Koa!'
})
app.listen(3000, function() {
console.log('3000端口监听中')
})
服务器端需要导入koa-bodyparser来解析请求体body内的数据
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'
})
router.post('/login_token',async (ctx, next) => {
// 从请求体中获取body
const { body } = ctx.request;
// 获取请求体内的用户名
const userInfo = body.user;
// 发送给服务器验证账号密码---
// ....
// 省略中间服务器验证账号密码操作,返回给客户端进行登录
ctx.body = {
ok: 1,
message: '登陆成功',
user: userInfo,
token: jwt.sign({ // 调用jtw的sign方法生成token令牌
data: userInfo, // 通过data属性计算出token字符串,由于签名不是加密的,令牌不要通过敏感信息生成
exp: Math.floor(Date.now() / 1000) + 60 * 60 // 设置过期时间一分钟,防止token被逆运算解密
}, secret) // sign方法第二个参数为事先声明的秘钥
}
})
// 当客户端再次发送请求时已经携带了token
// 在需要权限验证的路由,第二个参数调用jwtAuth方法并传入秘钥,验证成功则放行
// 接下来进行数据操作,并响应给客户端
router.get('/getUser_token', jwtAuth({secret}), async (ctx, next) => {
// 省略服务器请求过程,返回给客户端数据
ctx.body = {
message: '数据获取成功',
userInfo: ctx.state.user.data
}
})
module.exports = router
jsonwebtoken 负责令牌生成、koa-jwt 负责令牌认证 和 加密秘钥 在需要权限验证的路由引入模块
登录请求内通过jwt.sign方法生成,发送需要权限的请求没在路由内通过 jwtAuth({秘钥}) 来验证
如果权限验证需要更高的层级,可以把jwtAuth中间件,放到server.js内
// 引入中间件 const jwtAuth = require('koa-jwt') // 配置秘钥 const secret = 'this is a secret' // 使用中间件,传入秘钥,unless()方法排除不需要验证的路径,可以使用字符串、正则表达式或者指定请求方式 app.use(jwtAuth({secret}).unless({ path:['/user'] }))
login_token.html 登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model='user'>
<input type="password" v-model='pwd'>
<br>
<button @click="handleLogin">登录</button>
<button @click="handleLogout">退出</button>
<button @click="getUser">获取用户</button>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.0/axios.js"></script>
<script>
// 添加请求拦截器---------------------------------------
axios.interceptors.request.use((config) => {
// 对请求数据做处理,判断客户端是否存在token
const token = localStorage.getItem('token')
if (token) {
// 如果存在的话,则每个http请求都在config.headers加上token
// Bearer是jwt的认证头部信息,注意后面必须有一个字符串
config.headers.common['authorization'] = 'Bearer ' + token
}
return config
}),(err) => {
// 对请求错误的处理
return Promise.reject(err)
}
new Vue({
el: '#app',
data() {
return {
user: 'maxuan',
pwd: '123'
}
},
methods: {
// 发起登录请求
async handleLogin() {
// 发送用户名密码
const res = await axios.post('/user/login_token', {
user: this.user,
pwd: this.pwd
})
console.log(res)
// 服务器响应后,将token保存到localStorage
localStorage.setItem('token', res.data.token)
},
// 退出操作直接删除本地存储,不需要对服务器发送请求
handleLogout() {
localStorage.removeItem('token')
},
// 用户登录后本地存在令牌,这个请求会被拦截,并在当前请求config.headers内添加token令牌
// 如果在当前请求发送时本地没有令牌,服务器会响应令牌验证失败
async getUser() {
const res = await axios.get('/user/getUser_token')
console.log(res)
}
}
})
</script>
</body>
</html>
OAuth 开放授权
概念
有的应用会停工第三方应用登录,比如提供微信、QQ登录,这归功于 OAuth2.0,他允许客户端访问我们的资源服务器( 微信 ),我们就是资源的拥有者,这需要我们允许我们的客户端能够通过认证服务器 ( 在这里指微信,认证服务器和资源服务器可以分开也可以是部署在同一个服务器上 ) 的认证,很明显,OAuth2.0 提供了四种角色:
- 资源服务器
- 资源的拥有者
- 客户端应用
- 认证服务器
它们之间的交流实现了OAuth2.0 整个认证授权过程
OAuth2.0登录的原理,根据4种不同的模式有所不同,这里使用授权码模式下OAuth2.0的登录过程,其他的模式自行搜索
原理图:
注册github OAuth认证
登录github后,头像点击Setting,左侧点击下方Developer settings
点击Oauth Apps 注册一个新的应用
填写表单,测试本地地址可以填写localhost:3000,下方回调地址要在后端声明这个接口,这些信息都是可以后期修改的
注册完成后会获得一个 Client ID,点击按钮创建一个秘钥,蓝框提示现在就复制这个秘钥,之后会部分***隐藏
登录链接
点击这个链接发送github登录请求
<a href="/user/login_github">github登录</a>
配置路由
const Router = require('koa-router')
// 安装导入axios模块,用于向github发送请求
const axios = require('axios')
const router = new Router({
prefix: '/user'
})
// 配置gthub申请到的客户端id和秘钥
const config = {
client_id: '4127668463b9134d10ce',
client_secret: '6619105764703081e435e2a0b31930ca39c36beb'
}
// 客户端点击github登录,向服务器发送请求,服务器端返回重定向到github的授权登录页面-------------------
router.get('/login_github', async (ctx, next) => {
// 传入申请好的客户端id,拼接github授权页面地址
const path = `http://github.com/login/oauth/authorize?client_id=${config.client_id}`
// 重定向到github授权地址
ctx.redirect(path)
})
// 授权登录页面点击登录,github向客户端发送客户端需要访问的服务器回调请求路由地址----------------------
router.get('/oauth/github/callback',async ctx => {
const code = ctx.query.code // github回调传回的授权码
// 带着授权码code、client_id、clien_secret 向github认证服务器请求token
const params = {
client_id: config.client_id,
client_secret: config.client_secret,
code: code
}
// 服务器携带客户端id、秘钥、github授权码,向github发送这个登录请求------------------------------
let res = await axios.post('https://github.com/login/oauth/access_token', params)
// github响应中获得token
const access_token = new URLSearchParams(res.data).get('access_token')
// 请求头headers 设置 token,服务器向github发起请求获取用户信息--------------------------------
res = await axios.get('https://api.github.com/user',{
headers:{
Authorization: 'token ' + access_token
}
})
// 打印github返回的用户信息
console.log(res.data);
// 授权完成,重定向到登陆成功页面
ctx.redirect('/login_success.html')
})
module.exports = router
这是从github获取到的用户信息
{
login: 'Maxuann',
id: 62932596,
node_id: 'MDQ6VXNlcjYyOTMyNTk2',
avatar_url: 'https://avatars.githubusercontent.com/u/62932596?v=4',
gravatar_id: '',
url: 'https://api.github.com/users/Maxuann',
html_url: 'https://github.com/Maxuann',
followers_url: 'https://api.github.com/users/Maxuann/followers',
following_url: 'https://api.github.com/users/Maxuann/following{/other_user}',
gists_url: 'https://api.github.com/users/Maxuann/gists{/gist_id}',
starred_url: 'https://api.github.com/users/Maxuann/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/Maxuann/subscriptions',
organizations_url: 'https://api.github.com/users/Maxuann/orgs',
repos_url: 'https://api.github.com/users/Maxuann/repos',
events_url: 'https://api.github.com/users/Maxuann/events{/privacy}',
received_events_url: 'https://api.github.com/users/Maxuann/received_events',
type: 'User',
site_admin: false,
name: null,
company: null,
blog: '',
location: null,
email: null,
hireable: null,
bio: null,
twitter_username: null,
public_repos: 0,
public_gists: 0,
followers: 0,
following: 0,
created_at: '2020-03-31T08:45:58Z',
updated_at: '2021-10-26T07:32:48Z'
}