jwt学习

什么是JWT

JWT全称JSON Web Token :通过json对象在服务器和客户端直接传递信息的一种开放标准。通过算法对json对象进行签名,防止信息被篡改。官网:https://jwt.io/introduction/

用途

授权 :用户登陆后获得token,允许用户带token 去访问资源 api等 .
信息交换:利用jwt的签名,可以确定信息有没有被修改,也可以知道信息是谁发的。

结构

JWT token的形式是:xxx.yyy.zzz,如下图一个token例子:
在这里插入图片描述
JWT由3部分组成:
1.Header : 是一个json, alg:生成token的算法类型;typ:token 类型 ,直接指明为JWT,例如下面就是一个header
alg算法类型由很多种,具体看官网,用途会在3.Signature 中说明。
在这里插入图片描述
最后把这个 json串经过Base64Url编码,形成JSON Web令牌的第1部分。
2.Payload:也是一个json,用来存放实际需要传递的数据。这些数据有3中

  • Registered claims:JWT默认的几种数据,都是可选,包括:
字段说明用途
iss签发人
exp过期时间例如:用于权限或者登陆失效时间
sub主题
aud观众
nbf生效时间
iat签发时间
jti编号
  • Public claims:用户自定义的数据,例如邮箱啊用户名啊 (JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
  • Private claims:为了在双方之间共享信息而创建的自定义声明
    最后把这个 json串经过Base64Url编码,形成JSON Web令牌的第2部分。
    3.Signature:利用header 部分指明的算法,把header和payload以及密码进行编码。对前两部分的签名,防止数据篡改。 公式如下图所示
    在这里插入图片描述
    最后得到的签名就是,形成JSON WebToken的第3部分。
    4.最后把1 2 3部分的到字符串用”.“链接起来就得到了一个JWT。

用途

最常用的是在登陆场景中,jwt 包括以下步骤:
在这里插入图片描述
1.用户请求登陆,把用户名和密码发送给服务器,确认用户名和密码正确后,服务器会利用上面的方法,把用户名或者邮箱放在payload里面,生成一个jwt token。特别强调:不要把密码等敏感信息放在token中,因为jwt不加密,就是个base64编码的字符串,很容易解码。
2.服务器会把第一步中得到的token返回给客户端。这时客户端需要保存起来。可以储存在 Session 里面,也可以储存在 localStorage。
3.当用户想访问有权限控制的页面或者api 或者资源时,就要把2步保存的token放在请求的头信息Authorization字段里面。
在这里插入图片描述
或者放在请求body中。这次服务器会从header里面拿到token,验证token是否正确是否过期,解析token获得用户。

实践

下面是利用nodejs+express+mysql+docker+jsonwebtoken 做一个简单的登陆验证系统,用到的技术如下:
jsonwebtoken: 说明文档
mysql: 自己安装麻烦,利用docker起了一个mysql的服务,具体步骤参照 https://juejin.im/post/5babba8e5188255c960c3c63

  1. 利用上面的教程起了一个mysql容器,新建了一个book-be的数据库,以及一个user-info的表,如下图所示在这里插入图片描述
  2. 利用nodejs+express起一个端口为8081的服务,教程链接地址:https://www.runoob.com/nodejs/nodejs-express-framework.html,关键代码如下
const express = require("express");
const app = express();

// 静态文件
app.use("/public", express.static(__dirname + "/public"))
app.listen(8081, () => {
    console.log("访问地址为http://localhost:8081")
})

app.get("/login", (req, res) => {
    res.sendFile( __dirname + "/app/login.html");
})

app.get("/", (req, res) => {
    res.sendFile(__dirname + "/app/index.html");
})

}


  1. 前端登陆页面,页面实现就不用讲了,主要是获取表单值,然后发送登陆请求到后端,登陆成功后把后端返回的token存在localStorage中,并跳转到首页。
    在这里插入图片描述
    在这里插入图片描述
  2. 后端登陆api,后端获取前端传递的值,查询数据库的用户名和密码匹不匹配,如果匹配就生成jwt token,返回token。关键代码如下:
const jsonBodyParser = bodyParser.json();

app.post("/login-action", jsonBodyParser, (req, res) => {
    const response = {
        "name": req.body.name,
        "password": req.body.password
    };
    getUserByUserName(response.name).then((result) => {
        if (result.length > 0 && result[0].password === response.password) {
            // 生成webtoken
            const token = creareJWT(result[0].name, result[0].email);
            res.send({token});
        } else {
            res.send({err:"登陆失败,用户名或密码不对"});
        }
    }, err => {
        console.warn(err)
       // res.send(err)
        res.send({err:err.message})
    })
})

// 查询单个用户信息
function getUserByUserName(name) {
    return new Promise((resolve, reject) => {
        const connection = createConnetion();
        const sql = "select * from `user-info` where name=" + name;
        connection.query(sql, (err, result) => {
            connection.end();
            if (err) {
                console.log('[SELECT ERROR] - ', err.message);
                reject(err.message)
            } else {
                resolve(result);
            }
        })
    })
}

// 新建connetion
function createConnetion() {
    const connection = mysql.createConnection({
        host: "192.168.44.90",
        port: "3305",
        user: "root",
        password: "123456",
        database: "book-be"
    });

    connection.connect();
    return connection
}

// 生成JWT
function creareJWT(name, email) {
    const token = jwt.sign({name, email, exp: Math.floor(Date.now() / 1000) + 60}, 'book-fe-zw');
    return token;
}

生成token 的方法使用的jsonwebtoken库提供的sign方法,具体api如下:

jwt.sign(payload, secretOrPrivateKey, [options, callback])
//  第一参数就是你要存在jwt中的数据,在本例子中就是{name:"111",email:"111",exp: Math.floor(Date.now() / 1000) + 30} 
// 第二个参数就是你的密码或者私钥
//第3个参数就是返回函数,成功或者失败
  1. 前端首页的功能是,通过登陆获得的token,向服务器发请求,获得所有用户的信息。界面如下
    在这里插入图片描述
    实现代码如下:
    jwt在服务器端认证是通过token,所有需要认证的后端接口都需要传递token,为了避免每一个请求都要重复这样的步骤,最好写一个拦截器,拦截请求, 给请求加header,仿照这个教程https://juejin.im/post/5d5ccdd75188255625591357,用axios写了拦截器,代码如下
// 从localStorage中获得token
function getToken() {
    return localStorage.getItem("book-fe");
}

//  刷新token
function refresh() {
    return instance.get('/refresh').then(res => res.data)
}

// 创建一个axios实例  这个实例带get post 方法
const instance = axios.create({
    baseURL: '/api',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + getToken() // headers塞token
    }
})

instance.setToken = (token) => {
    instance.defaults.headers['Authorization'] = 'Bearer ' + token;
    localStorage.setItem('book-fe', token)
}

// 拦截请求,如果token 不存在就返回登陆页面
let isRefreshing1 = false; //解决多个请求重复的问题
instance.interceptors.request.use(
    config => {
        if(!isRefreshing1) {
            isRefreshing1 = true;
            if (!sessionStorage.getItem("book-fe") || sessionStorage.getItem("book-fe") === "") {  // 判断是否存在token,如果存在的话,则每个http header都加上token
                alert("您还没有登陆, 将为您跳转到登陆页面");
                location.href="/login";
                isRefreshing = false;
                return null;
            }
        }

        return config;
    },
    err => {
        return Promise.reject(err);
    });

// 是否正在刷新的标记
let isRefreshing = false; //解决多个请求多次refresh的问题
// 重试队列,每一项将是一个待执行的函数形式
let requests = []

// 拦截返回的数据
instance.interceptors.response.use(response => {
    // 接下来会在这里进行token过期的逻辑处理
    if(response.data.err === "jwt expired") {
        // 获取当前失败的请求
        const config = response.config
        // 说明token过期了,刷新token
        if (!isRefreshing) {
            isRefreshing = true
            return refresh().then(res => {
                const token = res.token
                instance.setToken(token)
                // 重置一下配置
                config.headers['Authorization'] = 'Bearer ' + token;
                config.baseURL = ''; // url已经带上了/api,避免出现/api/api的情况
                // 已经刷新了token,将所有队列中的请求进行重试
                requests.forEach(cb => cb(token))
                // 重试完了别忘了清空这个队
                requests = []
                // 重试当前请求并返回promise
                return instance(config)
            }).catch(res => {
                console.error('refreshtoken error =>', res)
                window.location.href = '/'
            }).finally(() => {
                isRefreshing = false
            })
        } else {
            // 正在刷新token,返回一个未执行resolve的promise
            return new Promise((resolve) => {
                // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
                requests.push((token) => {
                    config.baseURL = ''
                    config.headers['Authorization'] = 'Bearer ' + token;
                    resolve(instance(config))
                })
            })
        }

    }
    return response
}, error => {
    console.log(error)
    location.href="/login";
    return Promise.reject(error);
})

 window.onload = () => {
            instance.get("/users").then(result =>{
                console.warn(result);
                if(result.data.err) {
                    document.getElementById("content").innerHTML = `登陆权限有问题:${result.data.err},请重新<a href='/login'>登陆</a>`
                } else {
                    let arr = "";
                    for (let user of result.data.users) {
                        arr += `<div>姓名:${user.name}   |     邮箱:${user.email}</div>`
                    }
                    document.getElementById("content").innerHTML = arr;
                }
            })

            instance.get("/get-user-info").then(result => {
                console.warn(result);
                if(result.data.err) {
                    console.warn(result.data.err);
                } else {
                    document.getElementById("username").innerHTML = result.data.name;
                }
            })

            document.getElementById("loginout").addEventListener("click", loginOut);
        }

拦截器分两种:拦截requset和response,本例子在首先拦截request时判断token存不存在,不存在就跳转到登陆页面提示登陆,存在就继续response被拦截,判断reponse的返回结果是不是token 失效(这里需要前后端统一标准,用什么表示token失效),如果失效就先把请求存起来,重新发一个refresh请求返回正确的token结果,再重新发送刚刚保存起来的请求。面对多个请求采取队列形式,首先把请求存起来,用一个标示表示是不是正在刷新,第一个被拦截的请求再把标志改为 true,这样第二个请求就不会再触发 refresh,等到refresh结束后,再把存起来的请求重发。
可以通过network看到这样一个refresh过程:
在这里插入图片描述
一个请求的header
在这里插入图片描述

ps: angular自带拦截器,refresh过程可以在拦截器里进行。
下面是后端的refresh token 接口

// 刷新token
app.get("/api/refresh", (req, res) => {
    const token = req.header('Authorization').split(" ")[1];
    const decode = jwt.decode(token);
    console.warn(decode);
    res.send({token: creareJWT( decode.name,  decode.email)})
})

涉及到jsonwebtoken的一个api:

 jwt.decode(token);
  1. 后端首页,首页拿到header里面的token ,然后进行token验证,正确就继续查询数据库,不正确就返回错误,代码如下:
// jwt验证
function verifyToken(token) {
    return new Promise((resolve, reject) => {
        jwt.verify(token, 'book-fe-zw', (err, decode) => {
            if (err) {
                reject(err)
            } else {
                findInvalidByQuery(token).then(result => {
                        if (result && result.length <= 0) {
                            resolve(decode)
                        } else {
                            reject(new Error("token is invalid"))
                        }
                    }, err => {
                        reject(err)
                    }
                )
            }
        })
    })
}

// 获取全部用户接口
app.get("/api/users", (req, res) => {
    const token = req.header('Authorization').split(" ")[1];
    console.warn(token);
    getAllUser(token).then(users => {
        res.send({users});
    }, err => {
        console.log(err)
        res.send({err:err.message});
    })
})

// 查询所有用户服务
function getAllUser(token) {
    return new Promise((resolve, reject) => {
        verifyToken(token).then(result => {
            const connection = createConnetion();
            const sql = "SELECT * FROM `user-info`;";
            connection.query(sql, (err, result) => {
                connection.end();
                if (err) {
                    console.log('[SELECT ERROR] - ', err.message);
                    reject(err);
                }
                resolve(result);
            })
        }, err => {
            reject(err);
        })
    })
}

//  查询token
function findInvalidByQuery(token) {
    return new Promise((resolve, reject) => {
        const connection = createConnetion();
        const sql = "select * from `invalid-token` where token ='" + token + "'";
        connection.query(sql, (err, result) => {
            connection.end();
            if (err) {
                reject(err)
            } else {
                resolve(result)
            }
        })
    })
}

这里涉及到jsonwebtoken 的api

jwt.verify(token, secretOrPublicKey, [options, callback])
// 第一参数就是jwt
// 第二个参数就是你的密码或者共钥
//第3个参数就是返回函数,成功或者失败
  1. 登出的逻辑实现:参考https://www.geekjc.com/post/5c2d9287510938156d40e0e1 ,由于token不到过期时间就不会失效,所有要存一个token黑名单, 这里用mysql新建了invalid-token 表
    在这里插入图片描述
    在验证token时加一步,先去这个表中查询token 是否存在,要是存在就返回token失效,要是不存在就返回正确。具体的代码6。
    因此要实现登出,需要两步:
    前端:清空之前存的token
    后端:把这个登出的token存在invalid-token表中,代码如下:
/**
 * 登出
 */
app.get("/login-out", (req, res) => {
    const token = req.header('Authorization').split(" ")[1];
    addInvalidToken(token).then(result => {
        res.send({result: "of"})
    }, err => {
        console.log(err);
        res.send({err: err.message})
    })
})
// token表新增黑名单
function addInvalidToken(token) {
    return new Promise((resolve, reject) => {
        const connection = createConnetion();
        const sql = "insert into `invalid-token`(token) values(?)";
        connection.query(sql, [token], (err, result) => {
            connection.end();
            if (err) {
                console.log('[insert error] -', err.message);
                reject(err);
            } else {
                resolve(result)
            }
        })
    })
}

jsonwebtoken是如何create decode verify?

  1. create 生成,找到这个库实现的核心代码
//  这里是 转化base6eurl的方法
function base64url(string, encoding) {
  return Buffer
    .from(string, encoding)
    .toString('base64')
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
}

// 把header payload 调用base64url生成 xxx.yyy的形式
function jwsSecuredInput(header, payload, encoding) {
  encoding = encoding || 'utf8';
  var encodedHeader = base64url(toString(header), 'binary');
  var encodedPayload = base64url(toString(payload), encoding);
  return util.format('%s.%s', encodedHeader, encodedPayload);
}

function jwsSign(opts) {
  var header = opts.header;
  var payload = opts.payload;
  var secretOrKey = opts.secret || opts.privateKey;
  var encoding = opts.encoding;
  var algo = jwa(header.alg);
  var securedInput = jwsSecuredInput(header, payload, encoding);  // 得到 xxx.yyy
  var signature = algo.sign(securedInput, secretOrKey);  // 得到签名zzz
  return util.format('%s.%s', securedInput, signature); // 最后合起来就是xxx.yyy.zzz
}
  1. decode 解析
// base64url解码header
function headerFromJWS(jwsSig) {
  var encodedHeader = jwsSig.split('.', 1)[0];
  return safeJsonParse(Buffer.from(encodedHeader, 'base64').toString('binary'));
}

// base64url解码payload
function payloadFromJWS(jwsSig, encoding) {
  encoding = encoding || 'utf8';
  var payload = jwsSig.split('.')[1];
  return Buffer.from(payload, 'base64').toString(encoding);
}

function jwsDecode(jwsSig, opts) {
  opts = opts || {};
  jwsSig = toString(jwsSig);

  if (!isValidJws(jwsSig))
    return null;

  var header = headerFromJWS(jwsSig);

  if (!header)
    return null;

  var payload = payloadFromJWS(jwsSig);
  if (header.typ === 'JWT' || opts.json)
    payload = JSON.parse(payload, opts.encoding);

  return {
    header: header,
    payload: payload,
    signature: signatureFromJWS(jwsSig) // 返回的是签名
  };
}
  1. verify 验证
function securedInputFromJWS(jwsSig) {
  return jwsSig.split('.', 2).join('.');
}

function signatureFromJWS(jwsSig) {
  return jwsSig.split('.')[2];
}

function jwsVerify(jwsSig, algorithm, secretOrKey) {
  if (!algorithm) {
    var err = new Error("Missing algorithm parameter for jws.verify");
    err.code = "MISSING_ALGORITHM";
    throw err;
  }
  jwsSig = toString(jwsSig);
  var signature = signatureFromJWS(jwsSig);  获得zzz 签名
  var securedInput = securedInputFromJWS(jwsSig); 获得xxx.yyy
  var algo = jwa(algorithm);
  return algo.verify(securedInput, signature, secretOrKey);
}

// 验证的方法就是把header.payload 利用密码和算法加密生成签名,和传过来的签名对比一不一样
function createHmacVerifier(bits) {
  return function verify(thing, signature, secret) {
    var computedSig = createHmacSigner(bits)(thing, secret);
    return bufferEqual(Buffer.from(signature), Buffer.from(computedSig));
  }
}

session-cookie登陆,以及与jwt的比较?

session-cookie是最早的登陆方案,工作原理:
1.用户输入登录信息
2.服务器验证登录信息是否正确,如果正确就创建一个session,并把session存入数据库
3.服务器端会向客户端返回带有sessionID的cookie
4.在接下来的请求中,服务器将把sessionID与数据库中的相匹配,如果有效则处理该请求
5.如果用户登出app,session会在客户端和服务器端都被销毁
下面从这几个方向,对它们进行比较:
在这里插入图片描述
1.用户登陆状态:jwt是保存在客户端,session-cookie是保存在服务器端;session保存在服务器端较为安全,但是开销会明显增大;jwt 的信息不加密,不安全,因此不建议存放私密信息;
2.扩展性:多个项目公用一个登陆信息时,session-cookie方案需要每台服务器都能够读取 session;但是jwt是放在客户端的,每次请求都会把登陆信息返回给服务端,不用担心服务器端是否登陆;因此jwt的扩展行比较好;
3.安全性:sessoin-cookie方案存在CSRF(跨站请求伪造)问题和xss(跨网页脚本攻击)问题,因为浏览器会自动带上cookie信息发送给服务器端,所以比较容易伪造登陆信息。而jwt不存在这个问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值