一、登录功能(跟上一篇博客模块对接数据库方法类似)
//controller/user.js
const loginCheck = (username,password) => {
//先用假数据
// if (username === 'zhangsan' && password === '123') {
// return true
// }
// return false
let sql = `select username, realname from users where username='${username}' and password='${password}'`
return exec(sql).then(rows => {
return rows[0] || {}
})
}
//router/user.js
//登录
if (method === "POST" && req.path === "/api/user/login") {
const postData = req.body
const {username,password} = postData
const result =loginCheck(username,password)
return result.then(data => {
if (data.username) {
return new SuccessModel()
}
return new ErrorModel('登录失败')
})
}
//app.js
//处理user路由
const userResult = handleUserRouter(req, res)
if (userResult) {
userResult.then(userData => {
res.end(
JSON.stringify(userData)
)
})
return
}
// const userData = handleUserRouter(req, res)
// if (userData) {
// res.end(
// JSON.stringify(userData)
// )
// return
// }
二、完善登录功能前期知识点及准备
- 核心:登录校验&登录信息存储
- cookie和session
- session写入redis(内存数据库) 补:mysql是硬件数据库
- 开发登录功能,和前端联调(用到nginx反向代理)
什么是cookie
- 存储在浏览器的一段字符串(最大5kb)
- 跨域不共享(访问不同的域名,之间的cookie不共享)
- 格式如k1 = v1 , k2 = v2;因此可以存储结构化数据
- 每次发送http请求,会将请求域的cookie一起发送给server
- server 可以修改cookie并返回给浏览器
- 浏览器中也可以通过javascript修改cookie(有限制 比如server端可以将某一段cookie进行锁死,防止浏览器进行修改)
- server 端nodejs操作cookie
//获取cookie (字符串)
const cookieStr = req.headers.cookie || '' //格式 k1 = v1;k2 = v2
- 回到项目中,在app.js中的serverHandle()方法添加获取cookie
//解析cookie
req.cookie = {}
const cookieStr = req.headers.cookie || ''//k1=v1;k2=v2
cookieStr.split(';').forEach(item =>{ //item 格式是 k1 =v1
if (!item) {
return
}
const arr = item.split('=')
const key = arr[0].trim()
const val = arr[1].trim()
req.cookie[key] = val
})
- 测试
三、使用cookie完善登录验证
- 思路:用户登录成功后,会将用户名存放在cookie中,并返回到客户端。当对博客进行增删查改时,会查看cookie中是否存在用户名来进行判断是否登录,防止用户绕过登录,对博客进行不法操作。
- 为了方便调试,重新创建一条GET和其他路径的请求
// router/user.js
//登录验证的测试
if (method === 'GET' && req.path === '/api/user/login-test') {
if (req.cookie.username) {
return Promise.resolve(new SuccessModel())
}
return Promise.resolve(new ErrorModel('尚未登录'))
}
- 测试
如果我们在浏览器手动添加cookie中的username字段值为zhangsan时(这是个弊端,下面内容会对其进行解决)
- 修改真正登录路由
//登录
if (method === "POST" && req.path === "/api/user/login") {
const postData = req.body
const { username, password } = postData
const result = login(username, password)
return result.then(data => {
if (data.username) {
//操作cookie
res.setHeader('Set-Cookie',`username=${data.username};path=/`)
//path=/所有网页都生效
return new SuccessModel()
}
return new ErrorModel('登录失败')
})
}
-
此时,就暴露出一个问题就是客户端可以对cookie中的username进行修改,伪造身份,绕过登录,直接对博客进行操作。
-
解决方法:服务端对cookie进行限制。
-
操作cookie的过期时间
//获取cookie过期时间 router/user.js
const getCookieExpires = () => {
const d =new Date()
//当前时间加上一天
d.setTime(d.getTime() + (24 * 60 * 1000))
console.log('d.toGMTString() is',d.toGMTString())
return d.toGMTString()
}
- 在设置cookie时添加过期时间
四、使用session再完善登录验证
接着第三点使用cookie完成登录功能时,会暴露出一个问题:username暴露出来,很危险。(cookie尽量不要存放比较敏感的信息)
- 如何解决:cookie中存储userid,server端对应username
- 解决方案:session,即server端存储用户信息
- 代码演示(在app.js文件中)
//session数据
const SESSION_DATA ={} //全局环境
//serverHandle()方法中
//解析 session
let needSetCookie = false //是否需要设置cookie中的userid
let userId = req.cookie.userid
if (userId) {//cookie存在该用户的userid (理解为客户端存放了userid)
if (!SESSION_DATA[userId]) {//sessin没有存放username
SESSION_DATA[userId] = {}//赋值为空对象
}
} else {//cookie不存在该用户的userid (理解为客户端没有存放了userid)
console.log(3)
needSetCookie = true
userId =`${Date.now()}_${Math.random()}`//设置成随机字符串
SESSION_DATA[userId] = {}
}
req.session = SESSION_DATA[userId] //每一个用户对应一个名为userId的session
- 设置cookie (在app.js路由转发,返回数据后进行设置)
//处理user路由
const userResult = handleUserRouter(req, res)
if (userResult) {
userResult.then(userData => {
if (needSetCookie) {
//操作cookie
res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
}
res.end(
JSON.stringify(userData)
)
})
return
}
//blog路由也是要设置cookie,跟user路由一样
- 因为app.js已经设置了cookie,因此原先路由层的设置cookie不用设置,应该将数据(从数据库中取出)存放在seesion中,以便后期判断用户是否登录成功
//登录
if (method === "POST" && req.path === "/api/user/login") {
const postData = req.body
const { username, password } = postData
// const username = req.query.username
// const password = req.query.password
const result = login(username, password)
return result.then(data => {
if (data.username) {
//操作cookie
//res.setHeader('Set-Cookie', `username=${data.username}; path=/; httpOnly; expires=${getCookieExpires()}`)//path=/所有网页都生效
req.session.username = data.username
req.session.realname = data.realname
return new SuccessModel()
}
return new ErrorModel('登录失败')
})
}
- 测试路径
//登录验证的测试
if (method === 'GET' && req.path === '/api/user/login-test') {
if (req.session.username) {
return Promise.resolve(new SuccessModel({session:req.session}))
}
return Promise.resolve(new ErrorModel('尚未登录'))
}
}
- 进行测试
五、使用redis最终完善登录验证
(1)到目前为止还存在一个问题是:
- 目前session直接是js变量,放在nodejs进程内存中
- 第一,进程内存内存有限,访问量过大,内存暴增怎么办?
- 第二,正式上线运行是多线程,进程之间内存无法共享
(2)解决方案 redis
- web server 最常见的缓存数据库,数据存放在内存中(读写快,价格贵,存储少,一断电内存就丢失)
- 相对于mysql(硬盘数据库,价格便宜,存储大,读写慢),访问速度快
(3)为何 session 适合用redis
- session 访问频繁,对性能要求极高
- session可不考虑断电丢失数据的问题(临时数据)
- session数据量不是很大(相对于mysql中存放的数据)
(4)为何网站数据不适合用redis
- 操作频率不是太高(相对于session操作)
- 断电不能丢失,必须保留
- 数据量太大,内存成本太高
5.1、用redis存储session(小案例)
-
在命令行中启动 redis 输入命令 redis-server.exe redis.windows.conf
-
在项目中下载redis插件
-
代码
const redis = require('redis')
//创建客户端
const redisClient = redis.createClient(6379,'127.0.0.1')
//报错处理
redisClient.on('error', err => {
console.error(err)
})
//测试
redisClient.set('username','zhangsan',redis.print)
redisClient.get('username',(err,val) => {
if (err) {
console.error(err)
}
console.log('val',val)
//退出
redisClient.quit()
})
5.2、回到项目中使用nodejs连接redis-封装工具函数
- 配置redis参数(conf/db.js文件)
let REDIS_CONF //redis
if (env === "dev") {
REDIS_CONF = {
REDIS_CONFport:6379,
host:'127.0.0.1'
}
}
if (env === "production") {
//线上配置
}
module.exports = {
REDIS_CONF
}
- 封装redis为工具函数
// db/redis.js
const {REDIS_CONF} = require('../conf/db')
const redis = require('redis')
//创建 redis 客户端
const redisClient = redis.createClient(REDIS_CONF.port,REDIS_CONF.host)
//错误处理
redisClient.on('error',err => {
console.error(err)
})
//设置值
function set(key,val) {
if (typeof val === 'object') {
val=JSON.stringify(val)
}
redisClient.set(key, val, redis.print)
}
//获取值
function get(key) {//异步
const promise = new Promise((resolve, reject) => {
redisClient.get(key, (err,val) => {
if (err) {
reject(err)
return
}
if (val == null) {
resolve(null)
return
}
try {
resolve(
//可能是对象被转换为字符串(存入时对象是已字符串的形式存入的)
JSON.parse(val)
)
} catch (ex) {
resolve(val)
}
})
})
return promise
}
module.exports = {
set,
get
}
- session存入redis(将app.js原来涉及session的去掉)
//解析session
//serverHandle()方法中
let needSetCookie = false
let userId = req.cookie.userid
if (!userId) {
/*userId 并不存在,产生一个随机数存入到 redis 并且在返回信息到客户端时将该 userId 存入到 cookie
这一步操作的目的在于说:
用户知道未登录后,重新在当前页面登录时,会携带 cookie 发送给服务器端,
此时,服务端可以通过 userId 获取到 redis 中的值(可能有值,也可能为空对象)
*/
needSetCookie = true
userId = `${Date.now()}_${Math.random()}`
//初始化 redis 中 session 值
set(userId,{})
}
//获取session
req.sessionId = userId
get(req.sessionId).then(sessionData => {
if (sessionData == null) {
//初始化 redis 中 session 值
set(req.sessionId,{})
//设置 session
req.session = {}
}else {
req.session = sessionData
}
})
- 将从数据库mysql取出的信息同步到数据库Redis
//同步到 redis (router/user.js)
set(req.sessionId,req.session)
此时,在app.js中的serverHandle()方法中有两个promise任务,获取session中的get和解析post请求参数
如果按照第二种方式(即原程序一样)会报错
- 程序并不能先原先设想一样,先执行从redis中获取session,并将其存入到session中(从上往下执行)
- 至于程序中promise任务执行的先后,我们并不知道
解决方法:第一种方法(通过链式来按照原先自己设想的逻辑)
- 测试