QX-AI初始化中...
暂无预设简介,请点击下方生成AI简介按钮。
接口 接口即API,是前后端通信的桥梁.用于实现前后端通信
在NodeJS中,一个接口就是服务中的一个路由规则
API给客户端返回结果通常是 JSON 格式
后端直接使用ejs、pug渲染好页面再返回是前后端不分离的表现,后端(HTTP服务)通过API将数据交给前端(网页、APP、小程序),后端只需考虑提供前端所需要的数据,而不用管数据的渲染,API 是通用的,不同的类型的前端使用同一个API就能渲染出多种页面。
接口的组成 接口的组成会在API文档中详细说明,随机图片验证码API文档
请求方式 GET / POST
接口地址 URL
请求参数
响应结果、格式
RESTful API RESTful API 设计指南—阮一峰 RESTful API 是一种接口的规范,为了减少前后端的沟通成本 同一个URL路径(可带上路由参数)不同的请求方法代表不同的功能,且语义要相符,API 返回的状态码也要相符
操作
请求类型
URL
返回
新增
POST
/song
返回新生成的歌曲信息
删除
DELETE
/song/10
返回一个空文档
覆盖修改
PUT
/song/10
返回更新后的歌曲信息
局部修改
PATCH
/song/10
返回更新后的歌曲信息
获取所有
GET
/song
返回歌曲列表数组
获取单个(id)
GET
/song/10
返回单个歌曲信息
状态码语义:404 找不到资源、403 禁止访问、500服务器内部错误
当然通常是POST亿把梭哈
json-server json-server 是一个 JS 编写的全局工具包,可以快速搭建 RESTful API 服务,也用于前端临时搭建接口使用
安装:npm i -g json-server
新建一个json文件
1 2 3 4 5 6 7 8 { "song" : [ { "id" : 1 , "name" : "干杯" , "singer" : "五月天" } , { "id" : 2 , "name" : "当" , "singer" : "动力火车" } , { "id" : 3 , "name" : "不能说的秘密" , "singer" : "周杰伦" } ] }
使用:以 JSON 文件所在文件夹作为工作目录 ,执行该命令,端口默认3000 1 json-server --watch <文件名>.json
命令行的提示:
1 2 3 4 5 6 7 8 9 10 11 \{^_^}/ hi! Loading 01 .json Done Resources http : Home http :
接口测试工具 常用接口测试工具: apipost ,apifox ,postman
apipost 基本使用:
Header 设置请求头
Query 设置查询字符串
Body 设置请求体
none 没有内容
form-data 表单形式数据
x-www-form-urlencoded Query-String形式的数据
raw 原生请求体,json格式数据
记账本添加API 因为API即请求方法+路由,所以去修改routes文件夹下的路由,将原来的index.js页面路由放到web文件夹,新建账单相关接口路由account.js,放到api文件夹
然后在app.js中使用路由,接口路径添加上api前缀
/app.js 1 2 3 4 var accountRouter = require ('./routes/api/account' );app.use ('/api' , accountRouter);
account.js总览,相同路径、不同请求方法、带路由参数来实现增删改查接口,符合RESTful API接口规范
接口的设计:
接口通常返回的都是 JSON
一个接口通常由响应编号 code 、响应信息 msg 、响应数据 data 组成
即使出现错误或失败也是通过响应编号去体现错误,而不设置状态响应码
响应编号,字符串,四个0表示成功,非0(如:1001)表示失败
响应信息,字符串,如:查找成功、删除成功
/routes/api/account.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 var express = require ('express' );var router = express.Router ();const moment = require ('moment' );const AccountModel = require ('../../models/AccountModel' );router.get ('/' , function (req, res, next ) { AccountModel .find ().sort ({ date : -1 }) .then (data => { res.json ({ code : '0000' , msg : '读取成功' , data : data }) }) .catch (err => { console .log (err); res.json ({ code : '1001' , msg : '读取失败' , data : null }) }) }); router.get ('/:id' , (req, res ) => { let { id } = req.params ; AccountModel .findById (id) .then (data => { res.json ({ code : '0000' , msg : '读取成功' , data : data }); }) .catch (err => { res.json ({ code : '1004' , msg : '读取失败' , data : null }) }) }) router.post ('/' , function (req, res, next ) { AccountModel .create ({ ...req.body , date : moment (req.body .date ).toDate () }) .then (data => { console .log (data); res.json ({ code : '0000' , msg : '添加成功' , data : data }) }) .catch (err => { console .log (err); res.json ({ code : '1002' , msg : '添加失败' , data : null }) }); }); router.delete ('/:id' , (req, res ) => { let id = req.params .id ; AccountModel .deleteOne ({ _id : id }) .then (data => { console .log (data); res.json ({ code : '0000' , msg : '删除成功' , data : data }) }) .catch (err => { console .log (err); res.json ({ code : '1003' , msg : '删除失败' , data : null }) }) }); router.patch ('/:id' , (req, res ) => { let { id } = req.params ; AccountModel .updateOne ({ _id : id }, req.body ) .then (data => { res.json ({ code : '0000' , msg : '更新成功' , data : data }) }) .catch (err => { res.json ({ code : '1005' , msg : '更新失败' , data : null }) }) }) module .exports = router;
会话控制 每一个 HTTP 请求都是一个会话,但 HTTP 是一种无状态的协议,它没有办法区分不同的请求、不同的会话是否来自于同一个客户端,即无法区分、识别用户
所以需要后端实现会话控制来区分用户,如使用注册登陆来区分用户,以此保护不同用户的数据安全
常见的会话控制技术:cookie 、session 、token
cookie cookie 是 HTTP 服务器发送到用户浏览器并保存在本地的一小块数据,按域名划分,本质上是Key-Value键值对
浏览器向服务端发送请求时,会自动携带对应域名下的所有 cookie 设置在请求头的 Cookie 属性中
服务端通过响应报文的 set-cookie 在浏览器设置cookie
设置cookie cookie()
在响应报文set-cookie中携带cookie
设置浏览器关闭时自动销毁的cookie:
1 2 3 res.cookie ('name' , 'chuckle' ); Set -Cookie : name=chuckle; Path =/
设置一段时间后过期的cookie,设置时maxAge单位毫秒,响应报文中Max-Age单位秒
1 2 3 4 res.cookie ('user' , 'giggles' ,{maxAge : 60000 }); Set -Cookie : user=giggles; Max -Age =60 ; Path =/; Expires =Sun , 23 Apr 2023 01 :12 :07 GMT
后续请求头中Cookie属性携带cookie,Key-Value
1 Cookie : name=chuckle; user=giggles
删除cookie clearCookie()
通过设置对应cookie的过期时间为1970年,使cookie过期自动删除
1 2 res.clearCookie ('name' ); res.clearCookie ('user' );
响应报文 1 2 Set -Cookie : name=; Path =/; Expires =Thu , 01 Jan 1970 00 :00 :00 GMT Set -Cookie : user=; Path =/; Expires =Thu , 01 Jan 1970 00 :00 :00 GMT
获取cookie 需要安装 cookie-parser 是一个中间件
使用cookie-parser 1 2 3 4 const cookieParser = require ('cookie-parser' );app.use (cookieParser ())
设置完中间件后,通过 req.cookies 即可获取所有cookie
1 2 3 4 5 console .log (req.cookies );
cookie安全 1、httpOnly 设置cookie时带上 httpOnly 属性,客户端脚本代码(js)尝试读取该cookie,浏览器将返回一个空字符串作为结果
HttpOnly是包含在http返回头Set-Cookie里面的一个附加的属性
1 2 3 res.cookie ('name' , 'chuckle' ,{httpOnly : true }); Set -Cookie : name=chuckle; Path =/; HttpOnly
2、加密cookie ,设置cookie时带上 signed 属性,并给 cookieParser(secret) 中间件传入一个字符串secret用于加密,获取时使用 req.signedCookies
1 2 3 4 5 6 const secret = 'qx' ;app.use (cookieParser (secret)) res.cookie ('number' ,'123456' ,{signed : true ,maxAge : 60 *1000 }); console .log (req.signedCookies );
在浏览器端查看该cookie,可以看到已经被加密
1 number : s%3A123456.gnY %2Bp%2BEFLYanFxZP9eAYnlyW9IkToo4KlHZyIJ3DJgc
session session 在服务器端保存当前访问用户的相关信息(用户名、用户id、邮箱等)
作用: 识别用户
登陆例子: 当用户输入账号密码传给服务端后,会去数据库查找是否正确,如果正确,服务端会给这个用户创建一个session对象,保存当前用户的基本信息,该对象还会生成并保存一个唯一session_id会话id(sid,在数据库中字段名是_id),然后服务端会将sid以cookie形式返回给浏览器,浏览器之后的请求就会带上这个sid,服务端可以通过这个sid在一堆session对象数组中识别是哪个用户。
使用session 安装 express-session 和 connect-mongo 两个包
express-session用于将session存到内存中,connect-mongo可以将session存到数据库中,方便查看
设置中间件进行配置:
后续的增删改查操作,中间件会自动匹配好当前请求是哪个用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const session = require ("express-session" );const MongoStore = require ('connect-mongo' );app.use (session ({ name : 'sid' , secret : 'chuckle' , saveUninitialized : false , resave : true , store : MongoStore .create ({ mongoUrl : 'mongodb://127.0.0.1:27017/test' }), cookie : { httpOnly : true , maxAge : 1000 * 60 }, }))
设置session 模拟登陆情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 app.get ('/login' , (req, res ) => { let uname = req.query .username ; if (req.query .username === 'chuckle' && req.query .password === '123456' ){ req.session .username = uname; res.send ('登陆成功' ); return ; } res.send ('登陆失败' ); }); Set -Cookie : sid=s%3A-5tzIG5ZCDk4JaT7POAgODadySTDZVur.H7 %2Fh5xrNRApVa%2Fl1J4fynkE48fREOM1yfLjdZd68bps; Path =/; Expires =Sun , 23 Apr 2023 03 :16 :14 GMT ; HttpOnly
数据库中:
读取session 检测session是否存在,不存在则让用户登陆
1 2 3 4 5 6 7 8 9 10 11 app.get ('/' , (req, res ) => { if (req.session .username ){ res.send ('Welcome' ); return ; } res.send ('还未登陆' ); });
销毁session destroy()
,如用户主动退出登陆
1 2 3 4 5 6 7 app.get ('/logout' , (req, res ) => { req.session .destroy (()=> { res.send ('退出成功' ) }) });
cookie与session区别
存在的位置 cookie:浏览器端 session:服务端
安全性 cookie 是以明文的方式存放在客户端的,安全性相对较低 session 存放于服务器中,所以安全性相对较好
网络传输量 cookie 设置内容过多会增大报文体积, 会影响传输效率 session 数据存储在服务器,只是通过 cookie 传递 id,所以不影响传输效率
存储限制 浏览器限制单个 cookie 保存的数据不能超过 4K ,且单个域名下的存储数量也有限制(165) session 数据存储在服务器中,所以没有这些限制
记账本-注册登陆 效果: 不同用户登陆后显示对应用户的账单,没有登陆就跳转到登录页面
大概步骤:
先在app.js中导入session操作相关的包,并设置中间件
创建注册登陆的ejs页面
创建用户文档模型,后续通过其将用户信息写入数据库
修改账单文档模型,添加一个userID字段来保存账单对应用户的_id,用以区分账单属于哪个用户
创建注册登陆的路由 auth.js,使用 md5 对密码进行加密
创建中间件 checkLoginMiddleware.js 来检测用户是否登陆
修改index.js路由,应用检测登陆的中间件,并根据业务需要调整部分代码
业务逻辑:
用户注册后将用户名和经过md5加密后的密码保存在数据库users集合中,并且每次新注册会检测用户名是否已经存在
登陆时获取客户端传来的用户名和密码去数据库中找是否有对应的用户,因为保存在数据库中的密码是经过md5加密的,所以要将密码经过一次md5加密后再去查找,同一字符串经过md5加密后结果是相同的
用户存在即登陆成功,则将用户信息(用户名和_id)写入session
在新增账单记录时,从session中找到当前用户的_id,然后保存到账单文档的userID字段
渲染账单列表时,只渲染属于当前用户的账单文档,对比当前登陆用户的_id和每个文档userID是否相同
退出登陆即销毁当前的session
对于从请求头cookie中获得sid然后找到对应的session,并提取用户的信息,是express-session包内中间件自动完成的,后续业务只需要req.session.<属性名>即可获取当前用户的信息
数据库:
先在app.js中导入session操作相关的包,并设置中间件
app.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const session = require ("express-session" );const MongoStore = require ('connect-mongo' );const { HOST , PORT , NAME } = require ('./config' );app.use (session ({ name : 'sid' , secret : 'chuckle' , saveUninitialized : false , resave : true , store : MongoStore .create ({ mongoUrl : `mongodb://${HOST} :${PORT} /${NAME} ` }), cookie : { httpOnly : true , maxAge : 1000 * 60 * 60 * 24 * 7 }, }))
创建注册登陆的ejs页面
/views/auth/reg.ejs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!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 > 添加记录</title > <link rel ="stylesheet" href ="/css/page.css" /> </head > <body > <div class ="content" > <div class ="content-box" > <div class ="title" > <h2 > 注册</h2 > </div > <div class ="home" > <a href ="/login" > 去登陆</a > </div > <form action ="/reg" method ="post" autocomplete ="off" > <div class ="form-item" > <label for ="username" > 用户名</label > <input class ="control" type ="text" name ="username" id ="username" required /> </div > <div class ="form-item" > <label for ="password" > 密码</label > <input class ="control" type ="text" name ="password" id ="password" required /> </div > <hr /> <button > 注册</button > </form > </div > </div > </body > </html >
/views/auth/login.ejs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!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 > 添加记录</title > <link rel ="stylesheet" href ="/css/page.css" /> </head > <body > <div class ="content" > <div class ="content-box" > <div class ="title" > <h2 > 登陆</h2 > </div > <div class ="home" > <a href ="/reg" > 去注册</a > </div > <form action ="/login" method ="post" autocomplete ="off" > <div class ="form-item" > <label for ="username" > 用户名</label > <input class ="control" type ="text" name ="username" id ="username" required /> </div > <div class ="form-item" > <label for ="password" > 密码</label > <input class ="control" type ="text" name ="password" id ="password" required /> </div > <hr /> <button > 登陆</button > </form > </div > </div > </body > </html >
/public/css/page.css 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 body { font-family :"Microsoft YaHei" ,微软雅黑; color : #363636 ; } *{ padding : 0 ; margin : 0 ; box-sizing : border-box; text-decoration : none; } hr{ margin : 20px auto; border : 0 ; border-top : 1px solid rgb (220 , 220 , 220 ); } .content { max-width : 600px ; margin : 0 auto; } .content-box { display : flex; flex-direction : column; flex-wrap : wrap; align-content : center; margin : 0 10px ; } .content-box >*{ width : 100% ; } .home { margin-top : 10px ; padding-left : 20px ; display : flex; justify-content : space-between; } .home a { color : rgb (33 , 70 , 181 ); } .title { padding : 20px 0 ; border-bottom : 1px solid rgb (220 , 220 , 220 ); } .title h2 { font-size : 30px ; font-weight : 500 ; } .content-box form { margin-top : 10px ; } .form-item { margin-bottom : 15px ; } .form-item label { display : block; height : 32px ; line-height : 32px ; font-size : 18px ; } .form-item >*{ width : 100% ; } .form-item >.control { padding : 6px 12px ; height : 36px ; font-size : 16px ; line-height : 36px ; color : #363636 ; border : 1px solid #ccc ; border-radius : 4px ; -webkit-border-radius : 4px ; -moz-border-radius : 4px ; -ms-border-radius : 4px ; -o-border-radius : 4px ; transition : all 0.3s ; -webkit-transition : all 0.3s ; -moz-transition : all 0.3s ; -ms-transition : all 0.3s ; -o-transition : all 0.3s ; outline : none; font-family :"Microsoft YaHei" ,微软雅黑; } .form-item >.control :focus { border-color : #66afe9 ; -webkit-box-shadow : inset 0 1px 1px rgba (0 ,0 ,0 ,.075 ), 0 0 8px rgba (102 , 175 , 233 , .6 ); box-shadow : inset 0 1px 1px rgba (0 ,0 ,0 ,.075 ), 0 0 8px rgba (102 , 175 , 233 , .6 ); } .form-item >textarea .control { padding : 8px 12px ; height : 100px ; line-height : 1 ; resize : none; } .content-box form >button { width : 100% ; padding : 6px 12px ; margin-bottom : 15px ; border : 1px solid rgb (57 , 162 , 204 ); background : rgb (37 , 173 , 204 ); border-radius : 4px ; -webkit-border-radius : 4px ; -moz-border-radius : 4px ; -ms-border-radius : 4px ; -o-border-radius : 4px ; font-size : 17px ; color : #fff ; transition : all 0.3s ; -webkit-transition : all 0.3s ; -moz-transition : all 0.3s ; -ms-transition : all 0.3s ; -o-transition : all 0.3s ; } .content-box form >button :hover { background : rgb (32 , 153 , 190 ); }
创建用户文档模型,后续通过其将用户信息写入数据库
/models/UserModel.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const mongoose = require ('mongoose' );let UserSchema = new mongoose.Schema ({ username : { type : String , required : true }, password : { type : String , required : true } }); let AccountModel = mongoose.model ('users' , UserSchema );module .exports = AccountModel ;
修改账单文档模型,添加一个userID字段来保存账单对应用户的_id,用以区分账单属于哪个用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const mongoose = require ('mongoose' );let AccountSchema = new mongoose.Schema ({ matter : { type : String , required : true }, date : { type : Date , required : true }, type : { type : String , enum : ['支出' , '收入' ], default : '支出' }, account : { type : Number , required : true }, remark : { type : String , default : '无' }, userID :{ type : String , required : true } }); let AccountModel = mongoose.model ('accounts' , AccountSchema );module .exports = AccountModel ;
创建注册登陆的路由 auth.js,使用 md5 对密码进行加密
/routes/web/auth.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 var express = require ('express' );var router = express.Router ();const UserModel = require ('../../models/UserModel' );const md5 = require ('md5' );router.get ('/reg' , (req, res ) => { res.render ('auth/reg' ); }) router.post ('/reg' , (req, res ) => { UserModel .findOne ({ username : req.body .username }) .then (data => { if (data) { res.render ('tip' , { msg : '用户名已存在' , url : '/reg' }); return ; } UserModel .create ({ ...req.body , password : md5 (req.body .password ) }) .then (data => { res.render ('tip' , { msg : '注册成功' , url : '/login' }); }) .catch (err => { res.status (500 ).render ('tip' , { msg : '注册失败!' , url : '/reg' }); }) }) }) router.get ('/login' , (req, res ) => { res.render ('auth/login' ); }) router.post ('/login' , (req, res ) => { let { username, password } = req.body ; if (!username || !password) { res.render ('tip' , { msg : '用户名或密码错误' , url : '/login' }); return ; } UserModel .findOne ({ username : username, password : md5 (password) }) .then (data => { if (!data) { res.render ('tip' , { msg : '用户名或密码错误' , url : '/login' }); return ; } req.session .username = data.username ; req.session ._id = data._id ; res.render ('tip' , { msg : '登陆成功' , url : '/' }); }) .catch (err => { res.status (500 ).render ('tip' , { msg : '登陆失败!' , url : '/login' }); }) }) router.post ('/logout' , (req, res ) => { req.session .destroy (() => { res.render ('tip' , { msg : '退出成功' , url : '/login' }); }); }); module .exports = router;
创建中间件 checkLoginMiddleware.js 来检测用户是否登陆
/middleware/checkLoginMiddleware.js 1 2 3 4 5 6 7 8 9 10 const checkLoginMiddleware = (req, res, next ) => { if (!req.session .username ) { return res.render ('tip' , { msg : '还未登陆!' , url : '/login' }); } next (); } module .exports = checkLoginMiddleware;
修改index.js路由,应用检测登陆的中间件,并根据业务需要调整部分代码
/routes/web/index.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 var express = require ('express' );var router = express.Router ();const moment = require ('moment' );const AccountModel = require ('../../models/AccountModel' );const checkLoginMiddleware = require ('../../middleware/checkLoginMiddleware' );router.get ('/' , (req, res )=> { res.redirect ('/account' ); }) router.get ('/account' ,checkLoginMiddleware, function (req, res, next ) { AccountModel .find ({userID : req.session ._id }).sort ({ date : -1 }) .then (data => { res.render ('index' , { content : data, moment }); }) .catch (err => { console .log (err); res.status (500 ).render ('tip' , { msg : '查找失败!' , url : '/' }); }) }); router.get ('/add' ,checkLoginMiddleware, function (req, res, next ) { res.render ('add' ); }); router.post ('/add' ,checkLoginMiddleware, function (req, res, next ) { AccountModel .create ({ ...req.body , date : moment (req.body .date ).toDate (), userID : req.session ._id }) .then (data => { console .log (data); res.render ('tip' , { msg : '添加成功!' , url : '/' }); }) .catch (err => { console .log (err); res.status (500 ).render ('tip' , { msg : '添加失败!' , url : '/' }); }); }); router.get ('/delete/:id' ,checkLoginMiddleware, (req, res ) => { let id = req.params .id ; AccountModel .deleteOne ({ _id : id }) .then (data => { console .log (data); res.render ('tip' , { msg : '删除成功!' , url : '/' }); }) .catch (err => { console .log (err); res.status (500 ).render ('tip' , { msg : '删除失败!' , url : '/' }); }) }); module .exports = router;
token token 是服务端生成并返回给 HTTP 客户端的一串加密字符串, token中保存着用户信息
作用: 实现会话控制,识别用户的身份,主要用于移动端 APP
与session区别: token将用户信息存放于客户端,而session将用户信息存放于服务端,在客户端仅存放session的sid(_id)
token工作流程:
用户填写账号和密码校验身份,校验通过后服务端响应 token,一般放在响应体中
后续发送请求时,需要手动将 token 添加在请求报文的 token 属性中,一般放在请求头中
token特点:
数据存储在客户端,服务端压力更小
数据加密、可以避免 CSRF(跨站请求伪造),相对更安全
扩展性更强:服务间可以共享,增加服务节点更简单
JWT JWT(JSON Web Token )是目前最流行的跨域认证解决方案,可用于基于 token 的身份验证
token实现身份验证有很多办法,但 JWT 使 token 的生成与校验更规范
使用 jsonwebtoken 包 来操作 token
使用:
sign()
生成token sign(用户信息数据对象, 加密字符串, 配置对象)
verify
校验解析token verify(token, 加密字符串, 回调函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const jwt = require ('jsonwebtoken' );let token = jwt.sign ({ username : 'chuckle' , id : '123456' }, 'qx' , { expiresIn : 60 }); console .log (token);let t = token;jwt.verify (token, 'qx' , (err, data ) => { if (err){ console .log ('校验失败' ); return ; } console .log (data); });
记账本-接口token 记账本的页面端已经做了会话控制、用户区分,但API接口仍然没有做约束、没区分用户,下面使用token进行会话控制
注册登陆接口 注册登陆接口,即注册成功则在数据库users集合中新建一个用户文档,登陆成功则返回一个token
在api路由文件夹中新建auth.js,作为注册登陆接口的路由
/routes/api/auth.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 var express = require ('express' );var router = express.Router ();const UserModel = require ('../../models/UserModel' );const md5 = require ('md5' );const jwt = require ('jsonwebtoken' );const config = require ('../../config' );router.post ('/reg' , (req, res ) => { UserModel .findOne ({ username : req.body .username }) .then (data => { console .log (data); if (data) { res.json ({ code : '2003' , msg : '用户名已存在' , data : null }) return ; } UserModel .create ({ ...req.body , password : md5 (req.body .password ) }) .then (data => { res.json ({ code : '0000' , msg : '注册成功' , data : { username : req.body .username } }) }) .catch (err => { res.json ({ code : '2004' , msg : '注册失败' , data : null }) }) }) }) router.post ('/login' , (req, res ) => { let { username, password } = req.body ; if (!username || !password) { res.json ({ code : '2001' , msg : '用户名或密码错误' , data : null }) return ; } UserModel .findOne ({ username : username, password : md5 (password) }) .then (data => { if (!data) { res.json ({ code : '2001' , msg : '用户名或密码错误' , data : null }) return ; } let token = jwt.sign ({ username : data.username , _id : data._id }, config.token_secret , { expiresIn : 60 * 24 * 7 }); res.json ({ code : '0000' , msg : '登陆成功' , data : { token : token } }) }) .catch (err => { res.json ({ code : '2002' , msg : '登陆出错' , data : null }) }) }) router.post ('/logout' , (req, res ) => { res.json ({ code : '0000' , msg : '退出成功' , data : null }) }); module .exports = router;
其中token的加密字符串 token_secret 保存在配置文件中
/config.js 1 2 3 4 5 6 7 8 9 const config = { HOST : '127.0.0.1' , PORT : 27017 , NAME : 'test' , session_secret : 'chuckle' , token_secret : 'chuckle' } module .exports = config;
业务接口 有了token后,要对 account.js 里原有的业务接口进行修改,并添加一个中间件对token进行校验,校验完后将token中的用户信息(username和userID)写入req中
中间件 checkTokenMiddleware.js
middleware/checkTokenMiddleware.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const jwt = require ("jsonwebtoken" );const config = require ('../config' );const checkTokenMiddleware = (req, res, next ) => { let token = req.get ('token' ); if (!token) { res.json ({ code : '2008' , msg : '缺失token' , data : null }) return ; } jwt.verify (token, config.token_secret , (err, data ) => { if (err) { res.json ({ code : '2009' , msg : 'token校验失败' , data : null }) return ; } req.username = data.username ; req.userID = data._id ; next (); }) } module .exports = checkTokenMiddleware;
在业务接口中应用中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 var express = require ('express' );var router = express.Router ();const moment = require ('moment' );const AccountModel = require ('../../models/AccountModel' );const checkTokenMiddleware = require ('../../middleware/checkTokenMiddleware' );router.get ('/' , checkTokenMiddleware, function (req, res, next ) { AccountModel .find ({userID : req.userID }).sort ({ date : -1 }) .then (data => { res.json ({ code : '0000' , msg : '读取成功' , data : data }) }) .catch (err => { console .log (err); res.json ({ code : '1001' , msg : '读取失败' , data : null }) }) }); router.get ('/:id' , checkTokenMiddleware, (req, res ) => { let { id } = req.params ; AccountModel .findById (id) .then (data => { res.json ({ code : '0000' , msg : '读取成功' , data : data }); }) .catch (err => { res.json ({ code : '1004' , msg : '读取失败' , data : null }) }) }) router.post ('/' , checkTokenMiddleware, function (req, res, next ) { AccountModel .create ({ ...req.body , date : moment (req.body .date ).toDate (), userID : req.userID }) .then (data => { console .log (data); res.json ({ code : '0000' , msg : '添加成功' , data : data }) }) .catch (err => { console .log (err); res.json ({ code : '1002' , msg : '添加失败' , data : null }) }); }); router.delete ('/:id' , checkTokenMiddleware, (req, res ) => { let id = req.params .id ; AccountModel .deleteOne ({ _id : id }) .then (data => { console .log (data); res.json ({ code : '0000' , msg : '删除成功' , data : data }) }) .catch (err => { console .log (err); res.json ({ code : '1003' , msg : '删除失败' , data : null }) }) }); router.patch ('/:id' , checkTokenMiddleware, (req, res ) => { let { id } = req.params ; AccountModel .updateOne ({ _id : id }, req.body ) .then (data => { res.json ({ code : '0000' , msg : '更新成功' , data : data }) }) .catch (err => { res.json ({ code : '1005' , msg : '更新失败' , data : null }) }) }) module .exports = router;
最后在app.js中应用路由
1 2 3 4 var authApiRouter = require ('./routes/api/auth' );app.use ('/api' , authApiRouter)
记账本完全体 记账本演示:KeepingBook
github地址:KeepingBook-vercel
API文档:KeepingBook-api