对于服务端渲染和前后端分离两种开发模式,分别有着不同的身份认证方案:
- 服务端渲染推荐使用 Session 认证机制
- 前后端分离推荐使用 JWT 认证机制 (使用依据:Session 认证需要配合 Cookie,而 Cookie 默认不支持跨域访问)
# 一、普通认证方式
常规的认证方式:简单的判断登录条件然后跳转。
- app.js
var userRouter = require('./routes/users')
var loginRouter = require('./routes/login')
var backendRouter = require('./routes/backend')
app.use('/api', userRouter) // api路由
app.use('/login', loginRouter) // 登录页面
app.use('/backend', backendRouter) // 后台页面
# 1、页面显示:
① 登录页面
- routes / login.js(普通路由)
router.get('/', function (req, res, next) {
res.render('login')
})
- views / login.html
<!-- 略(登录页面、发起fetch请求:'/api/login') -->
<script>
login.onclick = () => {
fetch('/api/login', {
method: 'POST',
body: JSON.stringify({
username: username.value,
password: password.value
}),
headers: {
'Content-Type': 'application/json'
}
})
.then((res) => res.json())
.then((res) => {
// console.log(res)
// 页面✨跳转
if (res.ok === 1) {
location.href = '/backend'
} else {
alert('用户名密码不匹配!')
}
})
}
</script>
② 后台页面
- routes / backend.js(普通路由)
router.get('/backend', function (req, res, next) {
res.render('layout')
})
- view / layout.html
……
# 2、登录跳转:
- routes / users.js(api 路由)
// 增删改查
……
// 登录校验
router.post('/login', userController.login)
- controller / userController.js
const UserController = {
addUser: async (req, res) => {
const { username, password } = req.body
const data = await userService.login(username, password)
// 简单✨校验
if (data.length === 0) {
res.send({
ok: 0
})
} else {
res.send({
ok: 1
})
}
}
}
- services / userService.js
const UserService = {
login: () => {
return UserModel.find({
username,
password
})
}
}
# 3、问题
上面我们费了老大的劲最后是为了进入 backend.html 页面,结果用户不登录直接输入后台地址,仍然可以直接 🤔 进入 backend.html 页面。
为了解决上面的问题,我们可以使用 登录鉴权 解决问题。
# 二、Session 认证机制
# 1、HTTP 的无状态性
由于 HTTP 协议的无状态性(客户端的每次 HTTP 请求都是独立的,服务器不会主动保留 HTTP 请求的状态)。
# 2、Cookie
为了区分不同的请求,在 Web 中有我们通常会接触到 Cookie,通过它实现的认证方式类似于超市、商场中的会员卡
。
① 简介
Cookie 是存储在浏览器中的不超过 4kb 的字符串。由 内容(键值对) + 其他可选属性组成(有效期、安全性、使用范围)
router.get('/', (req, res) => {
// 1、设置Cookie
res.cookie('username', 'lencamo')
// 2、读取Cookie
console.log(req.cookies)
res.send()
})
② 问题
由于浏览器也提供了读写 Cookie 的 API,所以 Cookie 很容易被伪造(安全性有待考量)。
// 1、设置Cookie
document.cookie = 'username=lencamo'
// 2、读取Cookie
javascript: alert(document.cookie)
// 【当然还可以通过 开发者工具的Application、浏览器地址前的图标查看】
# 3、Session
单独使用 Cookie 时,由于浏览器而造成一些安全问题。对应的解决方案为:会员卡
+ 刷卡机认证
。
相当于:客户端的 cookie(sessionid)是一把钥匙,服务端的 session 是门。
重点
- Cookie 存储在客户端
- Session 存储在服务端的内存中(当然也可以存在数据库中)
由于 session 认证过程复杂,我们可以借助 express-session 包辅助开发。
① 下载
npm i express-session
# npm i connect-mongo
使用 express-session 包后,相关的验证、设置 cookie 等操作将自进行,不用自己编写。浏览器会自动带有 cookie 给后端,后端会自动的对其进行校验。
② 配置
- app.js
// 导入session中间件
const session = require('express-session')
// const MongoStore = require('connect-mongo')
// 注册session中间件
app.use(
session({
// 1、session基础配置(secret、resave、saveUninitialized必写)
name:'test-session'
secret: 'keyboard cat',
resave: true, // 若重新设置session后🤔,会自动重新计算过期时间
saveUninitialized: true, //是否初始一个cookie
cookie: {
maxAge:1000*60*60 //过期时间
secure: fase //为true时,只有https协议才能访问cookie(浏览器)
}
// 2、配置存储✨session的数据库
// store: MongoStore.create({
// mongourl: 'mongodb://127.0.0.1/ren_Session',
// ttl: 1000*60*10 //过期时间
// })
})
)
有 express-session 包中的配置项 secure,我们可以得知,此时是无法通过
document.cookie
的方式直接从本地获取的
# 4、示例
下面以前面普通认证方式的案例为例进行代码升级:
① 登录时在后台设置 session
- controller / userController.js
const UserController = {
addUser: async (req, res) => {
const { username, password } = req.body
const data = await userService.login(username, password)
// 简单✨校验
if (data.length === 0) {
res.send({
ok: 0
})
} else {
// 设置session🚩对象{}
// (简单的说:就是通过为当前session对象添加一个起标识作用字段,来间接生成session对象)
req.session.user = data[0]
res.send({
ok: 1
})
}
}
}
② 页面显示前提
- routes / backend.js(普通路由)
router.get('/backend', function (req, res, next) {
// 判断是否处于登录状态
if (req.session.user) {
res.render('layout')
} else {
res.redirect('/login')
}
})
注意:在后台设置的 session 对象默认存在内存中,一旦服务器重启,现有 session 数据会丢失,这时我们就有必要了解 connect-mongo 包。
# 5、示例升级
在进入后台 layout 页面后,在 cookie 过期后(在不刷新的前提下),仍然能操作后台页面的接口路由。
① 问题分析:
仅仅在 routes / backend.js 中做过期校验是行不通的。
② 解决问题:
直接写一个总的 cookie 校验中间件。
- app.js
app.use((req, res, next) => {
// 1、白名单放行
if (req.url.includes('login')) {
next()
return
}
// 2、session 过期校验
if (req.session.user) {
// 重置session🤔(让express-session 包的resave: true配置生效)
req.session.date = Date.now()
next()
} else {
// ① 是接口路由:对api请求返回错误码(前端使用axios拦截器处理即可)
// ② 不是接口路由:模块渲染(重定向)
req.url.includes('api') ? res.status(401).send({ ok: 0 }) : res.redirect('/login')
// 【上面是由于案例的特殊性(采用两掺方式)而做出的操作🚩:
// - 若纯后端前提模板——重定向即可,
// - 若纯前后端分离——返回错误码即可】
}
})
若发起的是 api 请求,后端是无法操作页面跳转的(由于浏览器限制,虽然得到了重定向到 login 页面请求,但不会跳转),需要前端进行跳转操作。
- views / layout.html
<body>
<h2>后台系统-用户管理业务</h2>
……
<button id="exit">退出登录</button>
<!-- ✨方式一:像最前面的登录校验跳转一样,对fetch请求响应进行判断 -->
<script>
//所有的请求都要加if后内容🤔
……
exit.onclick = () => {
fetch('/api/logout')
.then((res) => res.json())
.then((res) => {
if (res.ok === 1) {
location.href = '/login'
}
})
}
</script>
<!-- ✨方式二:直接使用axios拦截器 -->
<script>
reqAxios.interceptors.response.use(
function (response) {
return response
},
function (error) {
if (error.response.status === 401) {
$router.push('/login')
// 弹窗提示
Message.error('用户身份以过期!!')
}
return Promise.reject(error)
}
)
</script>
</body>
③ 退出登录功能
- routes/users.js
//销毁✨session(看不习惯,可以向前面一样进行封装)
router.post('/logout', (req, res) => {
req.session.destroy(() => {
res.send({ ok: 1 })
})
})
# 6、session 存储
# 三、JWT 认证机制
图 1:
图 2:
当然上面的 Session 认证机制也不是万能的,也有自己的缺点:
- 占用内存/空间
- 跨站请求伪造(CSRF 攻击)
- ……
① 跨站请求伪造:
登录 a 网站成功后会自动携带 cookie,在没有退出登录的情况下关闭网站,此时 cookie 仍然有效;若 b 网站此前挂载了一个携带 cookie 的 a 网站的链接,那 😢……
② 解决问题:
既然浏览器自动携带 Cookies 存储不安全,那:
直接将信息手动存到 Application 中的 Local Storage 中
并且在要求后端对信息进行 HMAC-SHA256 加密处理。
# 1、JWT
JWT(JSON Web Token)是目前最流行的跨域认证解决方案。由它的全称可以知道:它能将各部分的 JSON 生成 Token 字符串。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
① 简介:
JWT 字符串通常由三部分组成:header(头部,标识,加密方式)、payload(负载,就是用户数据)、secret(密钥)
② 缺点:
- 签名(token)可以直接在浏览器的 Application 中的 Local Storage 中直接拿到 🤔。
- 由于信息存储在浏览器,相应的会占有更多的带宽(流量)。
- token 无法在服务端注销,对应的遇到劫持问题时会相当棘手。
# 2、代码演示
① 下载
npm i jsonwebtoken
② 封装一个模块
- util / JWT.js
const jwt = require('jsonwebtoken')
const secret = '<秘钥>'
const JWT = {
// 1、生成token字符串
generate(value, expires) {
// 使用sign函数
return jwt.sign(
{
// data: '<数据>'
data: value
},
secret,
{
// expiresIn: '<有效期>'
expiresIn: expires
}
)
},
// 2、解密数据
verify(token) {
try {
return jwt.verify(token, secret)
} catch (error) {
return false
}
}
}
module.exports = JWT
③ 使用尝试
- routes / demo.js
const token = JWT.generate({ name: 'lencamo' }, '10s')
// 尝试在8秒后拿取数据
setTimeout(() => {
var decoded = JWT.verify(token)
console.log(decoded)
}, 8000)
# 3、示例
① 登录时在后台生成、发送 token
- controller / userController.js
const UserController = {
addUser: async (req, res) => {
const { username, password } = req.body
const data = await userService.login(username, password)
// 简单✨校验
if (data.length === 0) {
res.send({
ok: 0
})
} else {
// 改为:🚩设置token签名
// console.log(data[0]) // 要确保payload应该是一个普通对象
// 1、生成token
const token = JWT.generate(
{
_id: data[0]._id,
username: data[0].username
},
'2h'
)
// 2、发送token到客户端(header方式)
res.header('Authorization', token)
res.send({
ok: 1
})
}
}
}
Chrome 高阶调试:在页面跳转时,若要保留 Network、Console 面板的信息,只需要勾选 Chrome 提供 的 Preserve Log 选项即可,
② 服务端登录过期校验
- app.js
app.use((req, res, next) => {
// 1、白名单放行
if (req.url.includes('login')) {
next()
return
}
// 2、token 过期校验
// 思考?ES6语法
// const token = req.headers['authorization'] && req.headers['authorization'].split(" ")[1]
const token = req.headers['authorization']?.split(' ')[1]
// 是否有token
if (token) {
const payload = JWT.verify(token)
// 负载是否有效
if (payload) {
// 重新生成(计算)token✨过期时间
const newToken = JWT.generate(
{
_id: payload._id,
username: payload.username
},
'2h'
)
res.header('Authorization', newToken)
next()
} else {
res.status(401).send({ errCode: -1, errInfo: 'token过期' })
}
} else {
next()
}
})
同理:浏览器操作页面跳转(下面直接使用 axios 拦截器演示)
③ 客户端页面跳转
- views / login.html 或者 views / layout.html
<head>
<script>
axios.interceptors.request.use(
function (config) {
// 发送token✨(用于服务端校验)
const token = localStorage.getItem('token')
config.headers.Authorization = `Bearer ${token}`
return config
},
function (error) {
return Promise.reject(error)
}
)
axios.interceptors.response.use(
function (response) {
// 客户端存储🚩token
// console.log(response.headers)
const { authorization } = response.headers
authorization && localStorage.setItem('token', authorization)
return response
},
function (error) {
// token过期✨处理
if (err.response.status === 401) {
localStorage.removeItem('token')
localtion.href = '/login'
}
return Promise.reject(error)
}
)
</script>
</head>
<body>
……
<script>
login.onclick = () => {
aixos
.post('/api/login', {
username: username.value,
password: password.value
})
.then((res) => {
// console.log(res) ——> config、data、headers
if (res.data.ok === 1) {
// 存储token(使用🍗axios统一存储即可)
location.href = '/backend'
} else {
alert('用户名密码不匹配!!')
}
})
}
</script>
</body>
④ 退出功能
- views/backend.html
<body>
<h2>后台系统-用户管理业务</h2>
……
<button id="exit">退出登录</button>
<script>
// 移除✨token
exit.onclick = () => {
localStorage.removeItem('token')
localtion.href = '/login'
}
</script>
</body>
# 4、拓展
不使用 jsonwebtoken 包的 verify()方法进行 token 过期验证,使用 express-jwt 方式:
- 下载
npm i [email protected] #(注意:最新版本有部分改变,建议安装当前指定版本)
- app.js
const expressJWT = require('express-jwt')
const jwtConfig = require('./config/jwt.config')
// 1、注册解析token的中间件(token过期校验、白名单放行)
app.use(expressJWT({ secret: jwtConfig.jwtSecretKey }).unless({ path: [/^\/api\//] }))
// ……
// 错误级别中间件
app.use(function (err, req, res, next) {
// 2、express-jwt:捕获身份认证失败的错误
if (err.name === 'UnauthorizedError')
return res.send({
status: 401,
message: '无效的token!'
})
// 其他未知错误
res.send({
status: 500,
message: '未知错误!'
})
})
【用户登录】 →