参考:
1. Cookie+Session 1.1 应用 Cookie+Session :安全、易管控,但服务端有存储压力、跨域差,适合小型、前后端未分离、安全性要求高的项目。
1.2 流程
HTTP 是无状态的,也就是 HTTP请求方和响应方之间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。
但在登陆这种场景下,我们需要维护状态,因此需要对用户登录状态进行标记。
浏览器登录发送账号密码,服务端查用户库,校验用户
服务端把用户登录状态存为 Session,生成一个 sessionId
通过登录接口返回,把 sessionId set 到 cookie 上
此后浏览器再请求业务接口,sessionId 随 cookie 带上
服务端查 sessionId 校验 session
成功后正常做业务处理,返回结果
即:服务端存储用户身份信息(Session),客户端只存一个随机标识(SessionID)在 Cookie 里;每次请求时,客户端带 SessionID 到服务端,服务端通过 ID 查 Session 确认身份。
1.3 实现 安装1:npm i express-session@1 - 支持 session
安装2:npm i connect-mongo@4 - 支持 session 存储到 mongo
版本兼容 node18.20+, mongo3.6+, mongoose6.13+, express-session1.18+, connect-mongo4.6.0
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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 var session = require ('express-session' )const MongoStore = require ("connect-mongo" ); ...var app = express (); ... app.use (cookieParser ()); app.use (express.static (path.join (__dirname, 'public' ))); app.use (session ({ name : 'jerry-system' , secret : 'hello123world456' , cookie : { maxAge : 1000 * 60 * 60 , secure : false }, resave : true , saveUninitialized : true , rolling : true , store : MongoStore .create ({ mongoUrl : 'mongodb://127.0.0.1:27017/jerry_session' , ttl : 1000 * 60 * 10 }) })) app.use ((req, res, next ) => { if (req.url .includes ("login" )) { next () return } if (req.session .user ) { req.session .mydate = Date .now () next () } else { req.url .includes ("api" ) ? res.status (401 ).json ({ ok : -1 }) : res.redirect ("/login" ) } }) app.use ('/' , indexRouter); app.use ('/api' , usersRouter); app.use ('/login' , loginRouter); ...
routes/login.js
1 2 3 4 5 6 7 8 9 var express = require ('express' );var router = express.Router (); router.get ('/' , function (req, res, next ) { res.render ('login' , { title : 'Express' }); });module .exports = router;
views/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 <h1>登陆</h1> <div> <div>用户名:<input type="text" id="username"></div> <div>密码:<input type="password" id="password"></div> <div><button id="login">登陆</button></div> </div> <script> var username = document.querySelector("#username") var password = document.querySelector("#password") var login = document.querySelector("#login") // 登陆 login.onclick = () => { console.log(username.value, password.value); fetch("/api/login", { method: "POST", body: JSON.stringify({ username: username.value, password: password.value, }), headers: { "Content-Type": "application/json" } }).then(res => res.json()).then(res => { console.log(res); if (res.ok === 1) { location.href = "/" } else { alert("用户名与密码不匹配") } }) } </script>
routes/users.js
1 2 3 4 5 6 7 8 9 10 var express = require ('express' );const UserModel = require ('../model/UserModel' );const UserController = require ('../controllers/UserController' );var router = express.Router (); ... router.post ("/login" , UserController .login ); router.get ("/logout" , UserController .logout );module .exports = router;
controllers/UserController.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 const UserService = require ("../services/UserService" );const UserController = { ... login : async (req, res) => { const { username, password } = req.body const data = await UserService .login (username, password) if (data.length === 0 ) { res.send ({ ok : -1 }) } else { req.session .user = data[0 ] res.send ({ ok : 1 }) } }, logout : async (req, res) => { req.session .destroy (() => { res.send ({ ok : 1 }) }) } }module .exports = UserController
services/UserService.js
1 2 3 4 5 6 7 8 9 10 11 const UserModel = require ("../model/UserModel" );const UserService = { ... login : (username, password ) => { return UserModel .find ({ username, password }) } }module .exports = UserService
model/UserModel.js
1 2 3 4 5 6 7 8 9 10 11 12 const mongoose = require ("mongoose" )const Schema = mongoose.Schema const UserType = { username : String , password : String , age : Number }const UserModel = mongoose.model ("user" , new Schema (UserType ))module .exports = UserModel
2. JWT-JSON Web Token 2.1 应用 JWT :无状态、跨域友好、易扩展,但安全性低(拷走设置到浏览器里)、无法主动作废,适合前后端分离、分布式、多端的项目。
2.2 流程
即:服务端不存储任何用户信息,而是把用户身份(如 ID、用户名)加密成一个 Token 字符串返回给客户端;客户端存 Token(Cookie / 本地存储),每次请求带 Token,服务端解密验证即可,无需查库。
2.3 实现 安装1:npm i jsonwebtoken@9 - JWT-npm仓库和文档
即最新版 jsonwebtoken 9.x 都能兼容。
安装2:npm i axios - 需要 axios 拦截器
试一试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var jwt = require ('jsonwebtoken' );var token = jwt.sign ({ data : 'jerry' }, 'anydata-secret' , { expiresIn : '10s' }); console .log ("token->" , token);setTimeout (() => { var decoded = jwt.verify (token, "anydata-secret" ) console .log ('9s ->' , decoded); }, 9000 ) setTimeout (() => { var decoded = jwt.verify (token, "anydata-secret" ) console .log ('11s ->' , decoded); }, 11000 )
JWT.js - 工具类 util/JWT.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const jwt = require ("jsonwebtoken" )const secret = "jerry-anydata" const JWT = { generate (value, expires ) { return jwt.sign (value, secret, { expiresIn : expires }) }, verify (token ) { try { return jwt.verify (token, secret) } catch (error) { console .error (error.message ); return false } } }module .exports = JWT
验证:
1 2 3 4 5 6 7 8 9 10 11 12 var token = JWT .generate ({ name : "jerry" }, "10s" )console .log ("token->" , token);setTimeout (() => { var decoded = JWT .verify (token, "anydata-secret" ) console .log ('9s ->' , decoded); }, 9000 ) setTimeout (() => { var decoded = JWT .verify (token, "anydata-secret" ) console .log ('11s ->' , decoded); }, 11000 )
具体实现 页面上使用 script axios 引入:
1 <script src="https://cdn.jsdelivr.net/npm/axios@1.6.7/dist/axios.min.js" ></script>
登录页 views/login.ejs - 引入 axios 和 拦截器。
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 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登陆页面</title> <script src="https://cdn.jsdelivr.net/npm/axios@1.6.7/dist/axios.min.js"></script> <script> //axios 拦截器配置示例 // 请求拦截 axios.interceptors.request.use(function (config) { console.log("请求发出前,先执行的方法"); return config; }, function (error) { return Promise.reject(error); }); // 响应拦截 axios.interceptors.response.use(function (response) { console.log("响应成功后,先执行的方法"); const { authorization } = response.headers authorization && localStorage.setItem("token", authorization) return response; }, function (error) { return Promise.reject(error); }); </script> </head> <body> <h1>登陆</h1> <div> <div>用户名:<input type="text" id="username"></div> <div>密码:<input type="password" id="password"></div> <div><button id="login">登陆</button></div> </div> <script> var username = document.querySelector("#username") var password = document.querySelector("#password") var login = document.querySelector("#login") login.onclick = () => { axios.post("/api/login", { username: username.value, password: password.value, }).then(res => { console.log("axios login->", res); //token 在 res.headers.authorization if (res.data.ok === 1) { // 存储token console.log("进入首页 /"); location.href = "/" } else { alert("用户名与密码不匹配") } }) } </script> </body> </html>
登陆后跳转首页 views/index.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 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 <!DOCTYPE html> <html> <head> <title> <%= title %> </title> <link rel='stylesheet' href='/stylesheets/style.css' /> <script src="https://cdn.jsdelivr.net/npm/axios@1.6.7/dist/axios.min.js"></script> <script> //axios 拦截器配置示例 // 请求拦截 axios.interceptors.request.use(function (config) { console.log("请求发出前,先执行的方法"); // 所有请求携带 token const token = localStorage.getItem("token") config.headers.Authorization = `Bearer ${token}` //常规规范拼接 Bearer return config; }, function (error) { return Promise.reject(error); }); // 响应拦截 axios.interceptors.response.use(function (response) { console.log("响应成功后,先执行的方法"); const { authorization } = response.headers authorization && localStorage.setItem("token", authorization) return response; }, function (error) { console.log(error.response.status); if (error.response.status === 401) { localStorage.removeItem("token") location.href = "/login" } return Promise.reject(error); }); </script> </head> <body> <h1>后台系统用户管理</h1> <div><button id="logout">退出登陆</button></div> <div> <div>用户名:<input type="text" id="username"></div> <div>密码:<input type="password" id="password"></div> <div>年龄:<input type="number" id="age"></div> <div> <button id="register">注册用户</button> </div> </div> <hr> <div> <button id="update">更新用户</button> <button id="delete">删除用户</button> </div> <hr> <table border="1px"> <thead> <tr> <td>id</td> <td>用户名</td> <td>年龄</td> </tr> </thead> <tbody></tbody> </table> <script> var resigter = document.querySelector("#register") var logout = document.querySelector("#logout") var updateBtn = document.querySelector("#update") var deleteBtn = document.querySelector("#delete") var username = document.querySelector("#username") var password = document.querySelector("#password") var age = document.querySelector("#age") // 请求新增-POST resigter.onclick = () => { axios.post("/api/user", { username: username.value, password: password.value, age: age.value }).then(res => { console.log("新增:", res.data); if (res.data.ok < 0) { location.href = "/login" } }) } // 请求更新-PUT updateBtn.onclick = () => { axios.put("/api/user/696aeb1f3261b94d1d3a83e9", { username: "修改的名称", password: "修改的密码", age: 1 }).then(res => { console.log("更新:", res.data); if (res.ok < 0) { location.href = "/login" } }) } // 请求删除-DELETE deleteBtn.onclick = () => { axios.delete("/api/user/696aeb1f3261b94d1d3a83e9").then(res => { console.log("删除:", res.data); if (res.data.ok < 0) { location.href = "/login" } }) } // 请求列表-GET axios.get("/api/user?page=1&pageSize=10").then(res => { console.log("列表:", res.data); let tbody = document.querySelector("tbody") tbody.innerHTML = res.data.data.list.map(item => ` <tr> <td>${item._id}</td> <td>${item.username}</td> <td>${item.age}</td> </tr> `).join("") }) logout.onclick = () => { localStorage.removeItem("token") location.href = "/login" } </script> </body> </html>
controllers/UserController.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 UserService = require ("../services/UserService" );const JWT = require ("../util/JWT" );const UserController = { ... login : async (req, res) => { const { username, password } = req.body const data = await UserService .login (username, password) if (data.length === 0 ) { res.send ({ ok : -1 }) } else { const token = JWT .generate ({ _id : data[0 ]._id , username : data[0 ].username }, "1h" ) res.header ("Authorization" , token) res.send ({ ok : 1 }) } } }module .exports = UserController
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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 ...const JWT = require ('./util/JWT' ); var app = express (); ... app.use (cookieParser ()); app.use (express.static (path.join (__dirname, 'public' ))); app.use ((req, res, next ) => { if (req.url .includes ("login" )) { next () return } const token = req.headers ["authorization" ]?.split (" " )[1 ] console .log (req.headers ["authorization" ]); if (token) { const payload = JWT .verify (token) console .log ("当前登录用户信息: " , payload); if (payload) { const newToken = JWT .generate ({ _id : payload._id , username : payload.username }, "1h" ) res.header ("Authorization" , newToken) next () } else { res.status (401 ).send ({ errCode : -1 , errMessage : "token过期" }) } } else { next () } }) app.use ('/' , indexRouter); app.use ('/api' , usersRouter); app.use ('/login' , loginRouter); ...