前后端的身份认证
Web开发模式
客户端渲染
优点:
-
前端耗时少
。因为服务器端负责动态的生成HTML
内容,浏览器只需要直接渲染页面即可。尤其是移动端,更加省电 -
有利于SEO
。因为服务端响应是完整的HTML
页面内容,所以爬虫更容易爬取获得信息,更利于SEO
(搜索引擎优化)。
缺点:
占用服务器资源
。服务器来完成页面的内容拼接,如果请求太多,会对服务器造成一定的访问压力。不利于前后端分离开发效率低
。使用服务端渲染,则无法进行分工合作,尤其对于前端复杂度较高的项目,不利于项目高效开发。
前后端分离
前后端分离的概念
前后端分离的开发模式,依赖Ajax技术的广泛应用。简而言之,前后端分离的Web开发模式,就是后端只负责提供API
接口,前端使用Ajax
调用接口的开发模式。
前后端分离的优缺点
优点:
开发体验好
。前端专注于UI
页面的开发,后端专注于api
的开发,且前端有更多的选择性。用户体验好
。Ajax
技术广泛应用,极大的提高了用户的体验,可以轻松的实现页面的局部刷新。减轻了服务器端的渲染压力
。
缺点:
不利于SEO
。因为完整的页面需要在客户端动态拼接完成,所有爬虫对无法爬取的页面的有效信息。(解决方案:利用Vue
,React
等前端框架的SSR
技术能够很好的解决。)
如何选择 Web 开发模式
- 比如企业级的网站,主要功能是展示而没有复杂的交互,并且需要良好的
SEO
,则这是我们就需要使用服务器端渲染。 - 而类似后台管理项目,交互性比较强,不需要考虑
SEO
,那么就可以使用前后端分离的开发模式。
身份认证
身份认证指通过一定的手段,完成对用户身份的确认
。
不同开发模式下的身份认证
服务端渲染
推荐使用Session认证机制
前后端分离
推荐使用JWT认证机制
Session 认证机制
HTTP 协议
无状态性
HTTP
协议的无状态性,指的是客户端的每次HTTP请求都是独立的
,连续多个请求之间没有直接的关系,服务器不会主动保存每次请求的状态
。
如何突破
- 使用
Cookie
Cookie
Cookie简介
Cookie概念
Cookie
是储存在用户浏览器中一段不超过4KB
的字符串。它由一个名称(Name
),一个值(Value
)和其他几个用于控制Cookie有效期
,安全性
,使用范围
的可选属性组成。
不同域名下的Cookie各自独立
,每当客户端发起请求的时候,会自动
把当前域名下的所有未过期
的Cookie一同发送到服务器。
总结cookie的几大特性:
自动发送
域名独立
过期时限
4KB限制
Cookie在身份认证中的作用
- 客户端在第一次请求服务器的时候,服务器通过
响应头的形式
,向客户端发送一个身份认证的Cookie
,客户端会自动将Cookie保存在浏览器
中。 - 随后,当客户端浏览器每次请求服务器的时候,浏览器会
自动
将身份认证相关的Cookie
,通过请求头的形式
发送给服务器,服务器即可检验客户端身份。
Cookie不具有安全性
由于Cookie
是储存在浏览器中的,而且浏览器也提供了读写Cookie的API
,因此Cookie很容易被伪造
,不具有安全性,因此不建议服务器将重要的隐私数据,通过Cookie
的形式发送给浏览器。
在 express 中使用 cookie-parser 中间件
1.下载
npm install cookie-parser
2.配置
var cookieParser = require('cookie-parser')
3.使用
// 根据是否记住来设置cookie
// 这里只记录邮箱 隐私的信息不宜记录在cookie里
if(req.body.remember === 'on'){
res.cookie('user' , {
email: req.body.email,
},{
// 设置记住时间为七天
maxAge: 7*24*60*60*1000
})
}else{
// 如果这次没有设置记住 清理 user 的 okie
res.clearCookie('user')
}
Session
session工作原理
在express
中使用 express-session
中间件
1.安装
在 Express
项目中,只需要安装 express-session
中间件,即可在项目中使用 Session
认证:
npm install express-session
2.配置
安装完成后,需要通过require
来导入,再通过app.use()
来注册session
中间件:
// 导入
var session = require('express-session')
// 配置 (路由之前)
app.use(session({
secret: 'myKeyBoard', //配置加密字符串
resave: false, //固定写法
saveUninitialized: true //固定写法
}))
3.存数据
通过req.session
来访问和使用session
对象:
// 登录成功 通过Session记录登录状态
req.session.user = data
4.读数据
通过req.session.xxx
来读数据:
//通过是否登录来渲染首页
router.get('/' , function(req , res, next){
if(req.session.user){
User.findOne({
email: req.session.user.email
}).then(function(data){
res.render('index.html' , {
user: data,
})
})
}else{
res.render('index.html' , {
user: null
})
}
})
5.清空数据
通过将指定数据设置为null
或者使用 req.session.destroy()
函数:
注意
: req.session.destroy()
只会清空当前用户的session,每个用户之间的session独立
。
// 处理退出请求
router.get('/logout' , function(req , res){
// 清除登录状态
req.session.user = null //将指定数据设置为`null`
// req.session.destroy() // 或者使用 req.session.destroy() 函数
// 刷新当前页面
// a 标签是同步请求 服务端可以重定向
res.redirect('/')
})
Session局限性
Session
认证机制需要配合Cookie才能实现
。由于Cookie默认不支持跨域访问
,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域Session认证。此时推荐JWT
身份认证。
JWT认证机制
JWT相关知识
1.JWT工作原理
用户的信息通过token
字符串的形式,保存在客户端浏览器
中。服务器通过还原token字符串
的形式来认证用户的身份。
2.JWT组成部分
-
通常由三部分组成,分别是
Header(头部)
,Payload(有效荷载)
,Signature(签名)
。 -
Payload
是真正的用户信息
,是用户信息加密过后生成的字符串。 -
Header
和Signatur
e是安全性相关
的部分,只是为了保证Token的安全性。 -
三者使用英文分隔符
.
隔开,格式如下:Header.Payload.Signature
3.JWT使用方式
-
客户端收到服务器返回的
JWT
之后,通常将它储存在localStorage
或sessionStorage
中。 -
此后,客户端每次与服务器通信,都要带上这个
JWT
字符串,从而进行身份验证。 -
推荐的做法是
把JWT放在HTTP请求头的Authorization字段中
,格式如下:Authorization: Bearer <token>
在 express 中使用 JWT
1.安装相关包
npm install jsonwebtoken express-jwt
jsonwebtoken
用于生成jwt字符串
express-jwt
用于将jwt字符串解析还原成json对象
2.导入相关包
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
3.定义 secret 密钥
为了保证JWT字符串的安全性
,防止JWT
字符串再网络传输过程中被别人破解,我们需要专门定义一个用于加密
和解密
的secret
密钥。
- 当生成
JWT
字符串的时候,需要使用secret密钥对用户的信息进行加密
,最终得到加密好的JWT
字符串。 - 当把
JWT
字符串解密还原成JSON对象的时候,需要使用secret密钥进行解密
。
const secretKey = 'mySecretKey'
4.登录后生成 token
jsonwebtoken
包提供sign()
方法,将用户的信息加密成JWT字符串,响应给客户端:
- 参数1:用户的信息对象
- 参数2:加密密钥
- 参数3:配置对象,可配置token有效期
app.post('/api/post' , function(req , res){
console.log(req.body)
//省略了登录失败的 code ...
res.send({
status:200,
message: '登录成功',
token:jwt.sign({
foo: 'bar',
} , secretKey ,{
//设置token的有效期
//'xxs' xx秒
//'xxh' xx小时
expiresIn: '30s'
})
})
})
5.将 token 字符串解析为 JSON 对象
expressJWT({secret: secretKey})
解析 Token 的中间件.unless({path : [/^\/api\//]})
用来指定哪些接口不需要访问权限
(可以自己写正则 ,多个放在数组中用逗号隔开)- 配置好后会将解析出来的数据直接挂载到
req.user
属性上
// expressJWT({secret: secretKey}) 解析 Token 的中间件
// .unless({path : [/^\/api\//]}) 用来指定哪些接口不需要访问权限(自己写正则)
// 将解析出来的数据直接挂载到 req.user 上
app.use(expressJWT({secret: secretKey , algorithms: ['HS256']}).unless({path : [/^\/api\//]}))
6. demo 代码
app.js
const express = require('express')
const bodyParser = require('body-parser')
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
const cors = require('cors')
const secretKey = 'mySecretKey'
var app = express()
// 解决跨域
app.use(cors())
//配置解析 post
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
// expressJWT({secret: secretKey}) 解析 Token 的中间件
// .unless({path : [/^\/api\//]}) 用来指定哪些接口不需要访问权限(自己写正则)
// 将解析出来的数据直接挂载到 req.user 上
app.use(expressJWT({secret: secretKey , algorithms: ['HS256']}).unless({path : [/^\/api\//]}))
app.post('/api/login' , function(req , res){
return res.send({
status:200,
message: '登录成功',
token:jwt.sign({
foo: 'bar',
} , secretKey , { expiresIn: '30s' })
})
})
app.post('/admin' , function(req , res , next){
console.log(req.user)
return res.send({
status:200,
message: 'token有效',
})
})
// 配置一个全局错误处理中间件
app.use(function(err, req, res, next){
// token解析失败导致的错误
if(err.name === 'UnauthorizedError') {
return res.status(401).json({
err_code: 500,
message: '无效的token'
})
}
// 其他原因导致的错误
return res.status(500).json({
err_code: 500,
message: err.message
})
})
app.listen(5000 , function() {
console.log('Running...')
})
前端 index.js
页面
<!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>
<script src="./node_modules/jquery/dist/jquery.min.js"></script>
</head>
<body>
<button id="btnPOST">POST</button>
<button id="btnAdmin">Admin</button>
<script>
$(function(){
$('#btnPOST').on('click' , function(){
$.ajax({
type: 'POST',
url: 'http://127.0.0.1:5000/api/login',
data: {
username: '李四'
},
success: function(res){
console.log(res)
// 将 token 储存到 localStorage 中去
localStorage.setItem('token',JSON.stringify(res.token))
}
})
})
$('#btnAdmin').on('click' , function(){
let head = null
console.log('Bearer'+" "+ localStorage.getItem('token'))
$.ajax({
type: 'POST',
url: 'http://127.0.0.1:5000/admin',
headers:{
Authorization:'Bearer'+ " " + JSON.parse(localStorage.getItem('token'))
},
data: {
username: '李四'
},
success: function(res){
console.log(res)
}
})
})
})
</script>
</body>
</html>
报错总结:
Cannot set headers after they are sent to the client
原因
:客户端发送一次请求的时候,服务器端给出了多次响应
解决
:每次响应后添加return
Error: algorithms should be set
解决
:解密加入解密算法algorithms: ['HS256']
app.use(expressJWT({secret: secretKey , algorithms: ['HS256']}).unless({path : [/^\/api\//]}))
一直报UnauthorizedError
-
有可能是token字符串设置有效期过短导致解析时发现token已经过期
-
还有可能是前端页面发送的token格式有误
正确的格式:
// 请求头 空格 token字符串 Authorization: Bearer <token>
express-jwt
源码
该部分源码来自链接.// 从options中获取token if (options.getToken && typeof options.getToken === 'function') { try { token = options.getToken(req); } catch (e) { return next(e); } //从authorization中获取token } else if (req.headers && req.headers.authorization) { // 根据空格来划分 var parts = req.headers.authorization.split(' '); if (parts.length == 2) { var scheme = parts[0]; var credentials = parts[1]; if (/^Bearer$/i.test(scheme)) { // 如果前部分是 'Bearer' 则后半部分是token token = credentials; } else { if (credentialsRequired) { return next(new UnauthorizedError('credentials_bad_scheme', { message: 'Format is Authorization: Bearer [token]' })); } else { return next(); } } //如果以上俩种方法都得不到有效的 token 则报错 } else { return next(new UnauthorizedError('credentials_bad_format', { message: 'Format is Authorization: Bearer [token]' })); } }