由于须要识别用户是谁,不然怎么在网站上看到我的相关信息呢?javascript
由于HTTP是无状态的,什么是无状态呢?前端
就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。java
咱们的网站都是靠HTTP请求服务端得到相关数据,由于HTTP是无状态的,因此咱们没法知道用户是谁。mysql
因此咱们须要其余方式保障咱们的用户数据。ios
固然了,这种无状态的的好处是快速。git
好比说我在百度A页面进行了登陆,可是不找个地方记录这个登陆态的话。 那我去B页面,个人登陆态怎么保持呢?难道要url携带吗?这确定是不安全的。你让用户再登陆一次?登个鬼,再见👋 用户体验不友好。github
因此咱们须要找个地方,存储用户的登陆数据。这样能够给用户良好的用户体验。可是这个状态通常是有保质期的,主要缘由也是为了安全。redis
为了解决这个问题,Cookie出现了。sql
Cookie的做用就是为了解决HTTP协议无状态的缺陷所做的努力。数据库
Cookie是存在浏览器端的。也就是能够存储咱们的用户信息。通常Cookie 会根据从服务器端发送的响应的一个叫作Set-Cookie的首部字段信息, 通知浏览器保存Cookie。当下次发送请求时,会自动在请求报文中加入Cookie 值后发送出去。固然咱们也能够本身操做Cookie。
以下图所示(图来源《图解HTTP》)
这样咱们就能够经过Cookie中的信息来和服务端通讯。
须要看起来Cookie已经达到了保持用户登陆态的效果。可是Cookie中存储用户信息,显然不是很安全。因此这个时候咱们须要存储一个惟一的标识。这个标识就像一把钥匙同样,比较复杂,看起来没什么规律,也没有用户的信息。只有咱们本身的服务器能够知道用户是谁,可是其余人没法模拟。
这个时候Session就出现了,Session存储用户会话所需的信息。简单理解主要存储那把钥匙Session_ID,用这个钥匙Session_ID再去查询用户信息。可是这个标识须要存在Cookie中,因此Session机制须要借助于Cookie机制来达到保存标识Session_ID的目的。 以下图所示。
这个时候你可能会想,那这个Session有啥用?生成了一个复杂的ID,在服务器上存储。那好像咱们本身生成一个Session_ID,存在Mysql也能够啊!没错,就是这样!
我的认为Session其实已经发展为一个抽象的概念,已经造成了业界的一种解决方案。可能它最开始出现的时候有本身规则,可是如今通过发展。随着业务的复杂,各大公司早就本身实现了方案。
Session_id你想搞成什么样,就什么样,想存在哪里就存在哪里。
通常服务端会把这个Session_id存在缓存,不会和用户信息表混在一块儿。一个是为了快速拿到Session_id。第二个是由于前面也讲到过,Session_id是有保质期的,为了安全一段时间就会失效,因此放在缓存里就能够了。常见的就是放在redis、memcached里。也有一些状况放在mysql里的,多是用户数据比较多。但都不会和用户信息表混在一块儿。
本案例适合对服务端有必定概念的同窗哦,下面仅是核心代码。
第一步就是进行数据库配置,这里我单独配置了一个文件。
由于当项目大起来,须要对开发环境、测试环境、正式的环境的数据库进行区分。
let dbConf = null;
const DEV = {
database: 'dandelion', //数据库
user: 'root', //用户
password: 'xxx', //密码
port: '3306', //端口
host: '127.0.0.1' //服务ip地址
}
dbConf = DEV;
module.exports = dbConf;
复制代码
数据库链接。
const mysql = require('mysql');
const dbConf = require('./../config/dbConf');
const pool = mysql.createPool({
host: dbConf.host,
user: dbConf.user,
password: dbConf.password,
database: dbConf.database,
})
let query = function( sql, values ) {
return new Promise(( resolve, reject ) => {
pool.getConnection(function(err, connection) {
if (err) {
reject( err )
} else {
connection.query(sql, values, ( err, rows) => {
if ( err ) {
reject( err )
} else {
resolve( rows )
}
connection.release()
})
}
})
})
}
module.exports = {
query,
}
复制代码
这里我也是单独抽离出了文件,让路由看起来更舒服,更加好管理。
const Router = require('koa-router');
const router = new Router();
const koaCompose = require('koa-compose');
const {login} = require('../controllers/login');
// 加前缀
router.prefix('/api');
module.exports = () => {
// 登陆
router.post('/login', login);
return koaCompose([router.routes(), router.allowedMethods()]);
}
复制代码
中间件注册路由。
const routers = require('../routers');
module.exports = (app) => {
app.use(routers());
}
复制代码
个人session_id生成用了koa-session2
库,存储是存在redis里的,用了一个ioredis
库。
配置文件。
const Redis = require("ioredis");
const { Store } = require("koa-session2");
class RedisStore extends Store {
constructor() {
super();
this.redis = new Redis();
}
async get(sid, ctx) {
let data = await this.redis.get(`SESSION:${sid}`);
return JSON.parse(data);
}
async set(session, { sid = this.getID(24), maxAge = 1000 * 60 * 60 } = {}, ctx) {
try {
console.log(`SESSION:${sid}`);
// Use redis set EX to automatically drop expired sessions
await this.redis.set(`SESSION:${sid}`, JSON.stringify(session), 'EX', maxAge / 1000);
} catch (e) {}
return sid;
}
async destroy(sid, ctx) {
return await this.redis.del(`SESSION:${sid}`);
}
}
module.exports = RedisStore;
复制代码
const Koa = require('koa');
const middleware = require('./middleware'); //中间件,目前注册了路由
const session = require("koa-session2"); // session
const Store = require("./utils/Store.js"); //redis
const body = require('koa-body');
const app = new Koa();
// session配置
app.use(session({
store: new Store(),
key: "SESSIONID",
}));
// 解析 post 参数
app.use(body());
// 注册中间件
middleware(app);
const PORT = 3001;
// 启动服务
app.listen(PORT);
console.log(`server is starting at port ${PORT}`);
复制代码
这里主要是根据用户的帐号密码,拿到用户信息。而后将用户uid存储到session中,并将session_id设置到浏览器中。代码不多,由于用了现成的库,人家都帮你作好了。
这里我没有把session_id设置过时时间,这样用户关闭浏览器就没了。
const UserModel = require('../model/UserModel'); //用户表相关sql语句
const userModel = new UserModel();
/** * @description: 登陆接口 * @param {account} 帐号 * @param {password} 密码 * @return: 登陆结果 */
async function login(ctx, next) {
// 获取用户名密码 get
const {account, password} = ctx.request.body;
// 根据用户名密码获取用户信息
const userInfo = await userModel.getUserInfoByAccount(account, password);
// 生成session_id
ctx.session.uid = JSON.stringify(userInfo[0].uid);
ctx.body = {
mes: '登陆成功',
data: userInfo[0].uid,
success: true,
};
};
module.exports = {
login,
};
复制代码
登陆以后其余的接口就能够经过这个session_id获取到登陆态。
// 业务接口,获取用户全部的需求
const DemandModel = require('../../model/DemandModel');
const demandModel = new DemandModel();
const shortid = require('js-shortid');
const Store = require("../../utils/Store.js");
const redis = new Store();
async function selectUserDemand(ctx, next) {
// 判断用户是否登陆,获取cookie里的SESSIONID
const SESSIONID = ctx.cookies.get('SESSIONID');
if (!SESSIONID) {
console.log('没有携带SESSIONID,去登陆吧~');
return false;
}
// 若是有SESSIONID,就去redis里拿数据
const redisData = await redis.get(SESSIONID);
if (!redisData) {
console.log('SESSIONID已通过期,去登陆吧~');
return false;
}
if (redisData && redisData.uid) {
console.log(`登陆了,uid为${redisData.uid}`);
}
const uid = JSON.parse(redisData.uid);
// 根据session里的uid 处理业务逻辑
const data = await demandModel.selectDemandByUid(uid);
console.log(data);
ctx.body = {
mes: '',
data,
success: true,
};
};
module.exports = {
selectUserDemand,
}
复制代码
一、注意跨域问题
二、处理OPTIONS多发预检测问题
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', 'http://test.xue.com');
ctx.set('Access-Control-Allow-Credentials', true);
ctx.set('Access-Control-Allow-Headers', 'content-type');
ctx.set('Access-Control-Allow-Methods', 'OPTIONS, GET, HEAD, PUT, POST, DELETE, PATCH');
// 这个响应头的意义在于,设置一个相对时间,在该非简单请求在服务器端经过检验的那一刻起,
// 当流逝的时间的毫秒数不足Access-Control-Max-Age时,就不须要再进行预检,能够直接发送一次请求。
ctx.set('Access-Control-Max-Age', 3600 * 24);
if (ctx.method == 'OPTIONS') {
ctx.body = 200;
} else {
await next();
}
});
复制代码
三、容许携带cookie
发请求的时候设置这个参数withCredentials: true
,请求才能携带cookie
axios({
url: 'http://test.xue.com:3001/api/login',
method: 'post',
data: {
account: this.account,
password: this.password,
},
withCredentials: true, // 容许设置凭证
}).then(res => {
console.log(res.data);
if (res.data.success) {
this.$router.push({
path: '/index'
})
}
})
复制代码
以上的代码只是贴了核心的,源码以下
若有错误,请指教😜