NodeJS实现JWT原理
- 1.会话管理
- 2.session和cookies
- 3.JWT定义
- 4.JWT原理
- 5.JWT认证流程
- 6.代码实现(Vue+NodeJS+MySql)
1.会话管理
我们用nodejs为前端或者其他服务提供resful接口时,http协议他是一个无状态的协议,有时候我们需要根据这个请求的上下文获取具体的用户是否有权限,针对用户的上下文进行操作。所以出现了cookies session还有jwt这几种技术的出现, 都是对HTTP协议的一个补充。使得我们可以用HTTP协议+状态管理构建一个的面向用户的WEB应用。
2.session和cookies
个人理解,cookie是靠session_id完成服务端与客户端的通信的,当我们第一次登录时,服务端会去寻找我们的登录信息,然后将登录信息回写到cookie中,而session_id就是我们寻找登录信息的一个标识。
有几点需要注意一下:
1.cookie数据存放在客户的浏览器上,session数据放在服务器上。
2.cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
3.session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
4.单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制。
5.cookie在多个站点下会存在跨域问题。
3.JWT定义
JWT全称(json web token),就是我们开发中常用到的Token。
4.JWT原理
既然Token是对Http协议的一个补充,可以说代替Cookie,那么Token也是要存储用户信息的,而在cookie中都是以键值对形式存储的:比如(name=‘lgh’; )一定是以分号加一个空格结尾,而Token一般是服务端返回给客户端的一个JSON,但是我们与服务端通信的时候,往往这个JSON会加上签名(也就是一串你不认识的英文,服务端认识)。
5.JWT认证流程
浏览器发起请求登陆,携带用户名和密码;
服务端根据用户名和明码到数据库验证身份,根据算法,将用户标识符打包生成 token,
服务器返回JWT信息给浏览器,JWT不应该包含敏感信息,这里有一个加密过程,这是很重要的一点。
浏览器发起请求获取用户资料,把刚刚拿到的 Token一起发送给服务器,一般放在header里面。
服务器发现数据中有 Token,服务端对Token解密,然后去对比,验证通过。
服务器返回该用户的用户资料。
服务器可以在payload设置过期时间, 如果过期了,可以让客户端重新发起验证。
6.代码实现(Vue+NodeJS+MySql)
场景:比如我现在正在做的一个商城项目,一般首页,产品页,给客户看得界面,都是不需要身份校验的,我们逛淘宝京东,就算没有登录我们也能浏览商品,但是当我们点购买,点击购物车一些入口时,这时候是需要身份校验的,你必须登录了才能看自己购物车信息,才能购买商品。还有一点,cookie存储在客户端,我们会话窗口一关,cookie就没了,然后我们再去点购物车,服务端肯定需要我们再次登录。Token就是解决了这个问题。
1.连接数据库
这里我就简单写了两张表,一张user,一张cart
2.搭建服务器
app.js
const express = require('express');
const bodyParser = require('body-parser');
const cors=require("cors");
// 引入路由模块(用户)
const user=require("./routes/users");
const jwt=require("./jwt.js")
const app = express();
app.listen(8080);
app.use(cors({
origin:['http://localhost:8081'],
credentials:true
}));
3.连接池
pool.js
//创建mysql连接池
const mysql = require('mysql');
const pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: '',
database: 'user',
connectionLimit: 10
});
//把创建好的连接池导出
module.exports = pool;
4.JWT模块(Token)
进入路由模块之前,需要用到一个签名工具。(私钥加密、公钥解密)
这里我也写好了
// 引入模块依赖
const fs = require('fs');
const path = require('path');
const jwt = require('jsonwebtoken');
//生成token
function generateToken(data){
// 随机生成一个时间戳
let created = Math.floor(Date.now() / 1000);
// 读取硬盘上的私钥文件,利用jwt对其签名,这里会生成token
let cert = fs.readFileSync(path.join(__dirname, './pem/rsa_private_key.pem'));//私钥 可以自己生成
let token = jwt.sign({
data, // 需要加密的用户信息
exp: created + 60 * 60,// 设置token过期时间
}, cert, { algorithm: 'RS256'}); // 这是一个签名算法,这里不过多介绍
return token;
}
// 校验token
function verifyToken(token) {
let cert = fs.readFileSync(path.join(__dirname, './pem/rsa_public_key.pem'));//公钥 可以自己生成
let res;
try {
if(token!==undefined){
let result = jwt.verify(token, cert, { algorithms: ['RS256']}) || { };
res = result.data || { };
}
} catch (e) {
res = e;
}
return res;
}
module.exports = { generateToken, verifyToken };
加密解密模块就完成了,我们需要把方法暴露出来用module.exports方法
5.路由模块(接口)
user.js
const express=require("express");
const router=express.Router();
const pool=require("../pool");
const jwt=require("../jwt.js")
// 登录接口
router.post("/islogin",(req,res)=>{
var { uname,upwd,remember}=req.body;
var sql="select * from user_user where uname=? and binary upwd=?";
pool.query(sql,[uname,upwd],(err,result)=>{
err&&console.log(err);
if(result.length>0){
res.write(JSON.stringify({
ok:1,uname:result[0]['uname'],
remember:remember||false,
token:jwt.generateToken(result[0])
}));
}else{
res.write(JSON.stringify({ ok:0,msg:"用户名或密码错误!"}));
}
res.end();
})
})
// 购物车接口
router.get("/orders",(req,res)=>{
var aid=req.user.uid
if(aid==null){
res.write(JSON.stringify({ ok:0}));
res.end();
}else{
var sql="select * from user_order where aid=?";
pool.query(sql,[aid],(err,result)=>{
res.write(JSON.stringify({ ok:1,data:result[0]}));
res.end();
})
}
})
module.exports=router;
6.Vue模块
在Vue里面我们需要考虑两个东西,
第一个是界面(简单的做了一下)
第二个是Axios
因为在访问个人,购物车等入口时,我们需要在每次请求中带上Token向后端发请求,每次都写Token太麻烦了,所以这里就用到了Axios拦截器。
axios.js
这里存在一个逻辑问题,当用户第一次登陆才需要校验,
登陆成功之后,服务端返回一个Token
if(result.length>0){
res.write(JSON.stringify({
ok:1,uname:result[0]['uname'],
remember:remember||false,
token:jwt.generateToken(result[0])
}));
用户登陆成功,服务端返回一个Token,为了保持登陆状态可以看缓存中有没有Token,
1.登陆成功,缓存中存一个token以及Vuex中存储用户信息
2.页面关闭怎么办,发起请求时先去缓存中找Token,然后带在请求头中,如果找不到Token,发起请求时,服务端校验。
校验过程如下:用Node中间件,在还没进入到路由之前做了一个拦截。
3.如果校验失败,一定要从缓存中移除Token,知道下一次登录成功再存进去
也就是app.use('/user',user)之前
app.use((req, res, next)=>{
if (req.url != '/user/islogin' && (req.url.startsWith("/user") || req.url.startsWith("/orders"))) {
let token = req.headers.token;
let result = jwt.verifyToken(token);
// 如果考验通过就next,否则就返回登陆信息不正确
console.log(result)
if(result === undefined){
res.send({ status:403, msg:"未提供证书"})
}else if (result.name == 'TokenExpiredError') {
res.send({ status: 403, msg: '登录超时,请重新登录'});
} else if (result.name=="JsonWebTokenError"){
res.send({ status: 403, msg: '证书出错'})
} else{
req.user=result;
next();
}
} else {
next();
}
const Axios=axios.create({
baseURL:"http://localhost:8080",
withCredentials:true
})
Axios.interceptors.request.use(
config=>{
// post请求不能直接发对象参数,这里用qs模块转一下
if(config.method ==="post"){
config.data=qs.stringify(config.data)
}
if(localStorage.getItem("token")){
config.headers.token=localStorage.getItem("token");
}
if(sessionStorage.getItem("token")){
config.headers.token=sessionStorage.getItem("token");
}
return config;
},
error=>{
console.log(error);
Promise.reject(error);
}
);
Axios.interceptors.response.use(
res=>{
if(res.data.status==403){
localStorage.removeItem("token");
sessionStorage.removeItem("token");
console.log(403)
}else if(res.data.ok==-1){
alert(res.data.msg+" 请先登录 !");
console.log(-1)
}else if(res.data.token){
console.log(res.data.token)
if(res.remember==="true"){
localStorage.setItem("token",res.data.token);
}else{
sessionStorage.setItem("token",res.data.token);
}
}
return res;
},
error=>{
}
)
export default {
install: function(Vue, Option){
Vue.prototype.$axios=Axios;
}
}
效果图如下:
1.登录
登录成功后返回一个Token
2.访问购物车
在右侧看到请求头中带了一个Token,这里验证是通过了的。(代码都是从自己项目里面copy的,很久之前自己练手写的一个demo)