一、什么是token(理论)
-
解决http短连接,无状态管理的问题。
-
Jeb web token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开发标准,JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些科瓦ide其他业务逻辑所必须的声明信息。
👍传统的session认证
基于session认证所显露的问题
Session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言,seesion都是保存在内存中,而随着认证用户的增多,服务器的开销会明显增大。
扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求这台服务器上,这个才能拿到授权,这样在分布式的应用上,相应的限制了负载均衡的能力,这也意味着限制了应用的扩展能力。
CSRF:因为基于cookie来进行用户识别的,cookie如果截获,用户就会很容易受到跨站请求伪造的攻击。
👍基于token认证的鉴权机制
基于token的鉴权机制不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
token鉴权流程如下
优点:
- 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
- JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息
- 便于传输,jwt的构成非常简单,就是一个字符串,字节占用很小,所以它是。
- 非常便于传输的,它不需要在服务器保存会话信息,所以易于应用的扩展。
👍JWT的结构
JWT 是由三段信息构成的,将三段信息文本用.链接一起就构成了JWT字符串,就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
- 第一部分我们称它为头部(header)
- 第二部分我们称其为载荷(payload)
- 第三部分是签证(signature)
header
jwt的头部承载着两部分的信息:
- 声明类型
- 声明加密的算法(通常直接使用
HMAC
SHA256
)
完整的头部就像下面这样的JSON
{
'typ':'JWT',
'alg':'HS256'
}
将头部进行base64加密(该加密是可以对称解密的),构成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
base64加密后变成一个完整的字符串。
payload
载荷就是存放有效信息的地方,这个名字是指飞机上承载的货品,有效信息包含三个部分
-
标准中注册的声明
- iss:jwt签发者
- sub:jwt所面向的用户
- aud:接收jwt的一方
- exp:jwt的过期时间
- nbf:定义在什么时间之前,这过期时间必须大于签发时间
- iat:jwt的签发时间
- jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
-
公共的声明
- 公共的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分客户端可解密。
-
私有的声明
-
例如,定义一个payload json:
{ "sub":"1234567890", "name":"john Doe", admin:true }
然后将其进行base64加密,得到jwt的第二部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt 的第三部分是一个签证信息,这个签证信息由三部分组成
- header(base64后的)
- payload(base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用,使用.连接组成的字符串,然后通过header声明的加密方式加密secret组合加密,然后就构成了jwt的第三部分。
var encodedString=base64UrlEncode(header)+'.'+base64UrlEncode(payload)
var signature = HMACHA256(encodedString,'secret')
将这三部分用 . 连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIi wiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
👍客户端获取到token之后
-
Token
token是服务端生成的一串字符串,以做客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。token可以设置在cookie或者headers中,都可以。
-
公参
公共参数,一般放在headers中,让所有的请求都带上这个参数,服务器会对他做一些处理,比如常用的比如:会在headers中设置app的版本,用于服务器进行接口的版本兼容。
-
客户端对于token的存储方式
-
存储在webStorage中,每次调用接口都当成一个字段传给后台
-
存储在cookie中,让他自动发送,缺点就是不能跨域
- 将token存放在cookie中可以指定 httponly,来防止被Javascript读取,也可以指定secure,来保证token只在HTTPS下传输。缺点是不符合Restful最佳实践,容易受到CSRF攻击。
- CSRF就是恶意攻击者盗用已经认证过的用户信息,以用户信息名义进行一些操作〈如发邮件、转账、购买商品等等)。由于身份已经认证过,所以目标网站会认为操作都是真正的用户操作的。CSRF并不能拿到用户信息,它只是
盗用用户凭证
去进行操作。
-
拿到以后存储在webStorage中,每次调用接口的时候放在HTTP请求头的
Authorization
字段里- 将token存放在webStorage中,可以通过js来访问,这样会导致很容易受到xss攻击,如果js脚本被盗用,攻击者就可以轻易访问你的网站, webStroage作为一种储存机制,在传输过程中不会执行任何安全标准
- xss攻击是一种注入代码攻击,恶意攻击者在目标网站上注入script代码,当访问者浏览王网站的时候通过执行注入的script代码达到
窃取用户信息,盗用用户身份
等。
-
二、如何使用token?(案例)
以下使用两个小demo来简述后端生成token到客户端获取token到携带token去发送请求获取数据的整体流程。
后台服务环境基于node,需要安装的第三方npm包如下
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2", //web服务框架
"jsonwebtoken": "^9.0.1", //生成和验证token
"mysql": "^2.18.1", //数据库
"svg-captcha": "^1.4.0" //生成验证码
}
}
👍demo1:模拟前后端生成,获取和验证token
成功获取到token,并且检验成功。
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
</head>
<body>
<button id="btn">点我发送请求,认证</button>
<script>
btn.addEventListener('click',()=>{
let config={
headers:{
'Authorizition':localStorage.getItem('token')//从缓存中取
}
}
axios.get('http://localhost:3000/verifytoken',config).then(res=>{
console.log(res)
})
})
// dom元素加载完就发送请求
document.addEventListener('DOMContentLoaded',function(res){
axios.get('http://localhost:3000/gettoken').then((res)=>{
console.log(res)
//从res中解析token,并且存在localStorage 供后续请求使用
let token=res.data.token
console.log(token)
localStorage.setItem('token',token) //放到缓存
})
})
</script>
</body>
</html>
服务端代码(出现的问题和解决的方法)
出现问题
1.跨域问题,
2.客户端请求头设置参数,后端没有处理的问题
服务端设置响应控制头,cors方法防止跨域 (什么是cors方法?⏩点击此处查看交互专栏里的上一篇文章)
// 设置响应头,cors方法,防止跨域
app.use((req,res,next)=>{
// 允许任何来源跨域
res.header('Access-Control-Allow-Origin','*')
// 如果请求中设置了请求头,那么这个首部是必要的
res.header('Access-Control-Allow-Headers','*')
next()
})
app.use(express.static('public'))
结果:成功获取到token
整体代码展示
// 模拟 加载页面拿到token,再向后台发请求时验证token
// 生成和验证token
const jwt=require('jsonwebtoken')
// 生成验证码
const captcha=require('svg-captcha')
// 启动web服务的框架
const express=require('express')
// 定义密钥
const key='my key'
// 启动服务
const app=express()
// 设置响应头,cors方法,防止跨域
app.use((req,res,next)=>{
// 允许任何来源跨域
res.header('Access-Control-Allow-Origin','*')
// 如果请求中设置了请求头,那么这个首部是必要的
res.header('Access-Control-Allow-Headers','*')
next()
})
app.use(express.static('public'))
// 基于jsonwebtoken模块,生成token,将token返回给客户端
app.get('/gettoken',(req,res)=>{
// 声明携带的载荷
let payload={
name:'dema',
userId:1003,
exp:Date.now()/1000+3600*24 // 设置过期时间
}
// sign方法用于办法token
let token=jwt.sign(payload,key)
res.send({
code:0,
result:'ok',
token
})
})
// 验证token
app.get('/verifytoken',(req,res)=>{
// 获取Authorization消息头
// console.log(req)
let token=req.headers.authorization
// 验证token,获取token中存储的payload数据
jwt.verify(token,key,(err,decoded)=>{
console.log(err) //若验证失败,则err将不是null
console.log(decoded)
res.send('ok')
})
})
app.listen(3000,()=>{
console.log('server is running....')
})
👍demo2:获取token,携带token去验证输入的验证码是否正确
实现的步骤
-
- 加载页面,客户端获取token,并存入本地缓存
-
- 加载页面,获取验证码图片并渲染到页面
-
- 客户端再次发送请求,验证码+携带token,如果得到的响应是验证码输入成功,即为验证码验证成功
实现的效果
前端代码完整展示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
</head>
<input type="text" id="text">
<div id="img">
<!-- svg -->
</div>
<button id="btn">点我认证</button>
<script>
// 验证验证码
btn.addEventListener('click',()=>{
let config={
headers:{
'Authorizition':localStorage.getItem('token')//从缓存中取
}
}
axios.get('http://localhost:3000/resgister?ucode='+text.value,config).then(res=>{
console.log(res)
})
})
// dom元素加载完就发送请求
document.addEventListener('DOMContentLoaded',function(res){
axios.get('http://localhost:3000/gettoken').then((res)=>{
let token=res.data.token
localStorage.setItem('token',token)
console.log('token',token)
})
})
// 页面加载完获取验证码
window.onload=function(){
axios.get('http://localhost:3000/getcode').then((res)=>{
let svg=res.data.svg
console.log('svg',svg)
let img=document.getElementById('img')
img.innerHTML=svg
})
}
</script>
</body>
</html>
服务端代码完整展示
// 生成和验证token
const jwt=require('jsonwebtoken')
// 生成验证码
const captcha=require('svg-captcha')
// 启动web服务的框架
const express=require('express')
// 定义密钥
const key='my key'
// 启动服务
const app=express()
// 设置响应头,cors方法,防止跨域
app.use((req,res,next)=>{
// 允许任何来源跨域
res.header('Access-Control-Allow-Origin','*')
// 如果请求中设置了请求头,那么这个首部是必要的
res.header('Access-Control-Allow-Headers','*')
next()
})
app.use(express.static('public'))
//模拟redis数据库
let redis={} //模拟redis数据库
// 基于jsonwebtoken模块,生成token,将token返回给客户端
let token
app.get('/gettoken',(req,res)=>{
// 声明携带的载荷
let payload={
name:'dema',
userId:1003,
exp:Date.now()/1000+3600*24 // 设置过期时间
}
// sign方法用于办法token
token=jwt.sign(payload,key)
res.send({
code:0,
result:'ok',
token
})
})
// 接收请求,返回验证码图片
app.get('/getcode',(req,res)=>{
// 生成验证码
let cap=captcha.create() //生成一个验证码图片
console.log('生成的token',token)
// token验证成功
// 缺一步,把正确答案和token做配对存入redis里
redis[token]=cap.text
console.log('当前redis',redis)
res.send({
svg:cap.data
})
})
// 验证token,验证验证码是否一致
app.get('/resgister',(req,res)=>{
// 获取用户输入的验证码
let ucode=req.query.ucode
// 从请求头获取token,验证成功,执行后续业务
let token=req.headers.authorizition
// 验证token,获取token中存储的payload数据
jwt.verify(token,key,(err,decoded)=>{
console.warn(err)
if(err!=null){// 验证失败
res.send('刷新重试')
return
}
})
//token验证成功
let answer=redis[token]
if(ucode.toLocaleLowerCase()==answer.toLocaleLowerCase()){
res.send('验证码输入正确')
}else{
res.send('验证码输入错误')
}
})
app.listen(3001,()=>{
console.log('server is running....')
})