Cookie和Session的区别,Koa2+Mysql+Redis实现登录逻辑

为什么需要登录态?

因为需要识别用户是谁,否则怎么在网站上看到个人相关信息呢?

为什么需要登录体系?

因为HTTP是无状态的,什么是无状态呢?

就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。

我们的网站都是靠HTTP请求服务端获得相关数据,因为HTTP是无状态的,所以我们无法知道用户是谁。

所以我们需要其他方式保障我们的用户数据。

当然了,这种无状态的的好处是快速。

什么叫保持登录状态?

比如说我在百度A页面进行了登录,但是不找个地方记录这个登录态的话。
那我去B页面,我的登录态怎么保持呢?难道要url携带吗?这肯定是不安全的。你让用户再登录一次?登个鬼,再见👋 用户体验不友好。

所以我们需要找个地方,存储用户的登录数据。这样可以给用户良好的用户体验。但是这个状态一般是有保质期的,主要原因也是为了安全。

为了解决这个问题,Cookie出现了。

Cookie

Cookie的作用就是为了解决HTTP协议无状态的缺陷所作的努力。

Cookie是存在浏览器端的。也就是可以存储我们的用户信息。一般Cookie 会根据从服务器端发送的响应的一个叫做Set-Cookie的首部字段信息,
通知浏览器保存Cookie。当下次发送请求时,会自动在请求报文中加入Cookie 值后发送出去。当然我们也可以自己操作Cookie。

如下图所示(图来源《图解HTTP》)


Cookie

image

这样我们就可以通过Cookie中的信息来和服务端通信。

服务端如何配合?Session!

需要看起来Cookie已经达到了保持用户登录态的效果。但是Cookie中存储用户信息,显然不是很安全。所以这个时候我们需要存储一个唯一的标识。这个标识就像一把钥匙一样,比较复杂,看起来没什么规律,也没有用户的信息。只有我们自己的服务器可以知道用户是谁,但是其他人无法模拟。

这个时候Session就出现了,Session存储用户会话所需的信息。简单理解主要存储那把钥匙Session_ID,用这个钥匙Session_ID再去查询用户信息。但是这个标识需要存在Cookie中,所以Session机制需要借助于Cookie机制来达到保存标识Session_ID的目的。
如下图所示。

Session

这个时候你可能会想,那这个Session有啥用?生成了一个复杂的ID,在服务器上存储。那好像我们自己生成一个Session_ID,存在Mysql也可以啊!没错,就是这样!

个人认为Session其实已经发展为一个抽象的概念,已经形成了业界的一种解决方案。可能它最开始出现的时候有自己规则,但是现在经过发展。随着业务的复杂,各大公司早就自己实现了方案。

Session_id你想搞成什么样,就什么样,想存在哪里就存在哪里。

一般服务端会把这个Session_id存在缓存,不会和用户信息表混在一起。一个是为了快速拿到Session_id。第二个是因为前面也讲到过,Session_id是有保质期的,为了安全一段时间就会失效,所以放在缓存里就可以了。常见的就是放在redis、memcached里。也有一些情况放在mysql里的,可能是用户数据比较多。但都不会和用户信息表混在一起。

Cookie 和 Session 的区别

Cookie 和 Session 的区别

登录态保持总结

  1. 浏览器第一次请求网站, 服务端生成 Session ID。
  2. 把生成的 Session ID 保存到服务端存储中。
  3. 把生成的 Session ID 返回给浏览器,通过 set-cookie。
  4. 浏览器收到 Session ID, 在下一次发送请求时就会带上这个 Session ID。
  5. 服务端收到浏览器发来的 Session ID,从 Session 存储中找到用户状态数据,会话建立。
  6. 此后的请求都会交换这个 Session ID,进行有状态的会话。

登录流程图

登录流程图

实现案例(koa2+ 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的生成和存储

我的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;

入口文件(index.js)

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,
}

坑点注意注意

1、注意跨域问题

2、处理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();
    }
});

3、允许携带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'
        })
    }
})

源码

以上的代码只是贴了核心的,源码如下

前端后端

但是练手的项目还在开发中,网站其他功能还没有全部实现。
代码写的比较挫😶😶😶

但是登录完全没有问题的~

但是你需要提前了解Redis、Mysql、Nginx和基本的服务器操作哦!

如有错误,请指教😜

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Se...
    chinariver阅读 5,602评论 1 49
  • 写在前面 cookie和session的区别: 1、cookie数据存放在客户的浏览器上,session数据放在服...
    Pitfalls阅读 1,522评论 0 17
  • 媚媚: 你好! 以前看到过这样一句话,书信比微信更亲切,因为一张纸从一个人的笔下翻过重山,跃过万水到一个人眼前时,...
    维枷阅读 288评论 0 0
  • 如果不是那些又长又拗口的所谓王国的名字,我想我会更享受这本书。 我果然是一个没有童心和耐心的人,书中许多故事我没有...
    某叶阅读 683评论 0 0