Cookie&Session,登陸的那些小事兒~

爲何須要登陸態?

由於須要識別用戶是誰,不然怎麼在網站上看到我的相關信息呢?javascript

爲何須要登陸體系?

由於HTTP是無狀態的,什麼是無狀態呢?前端

就是說這一次請求和上一次請求是沒有任何關係的,互不認識的,沒有關聯的。java

咱們的網站都是靠HTTP請求服務端得到相關數據,由於HTTP是無狀態的,因此咱們沒法知道用戶是誰。mysql

因此咱們須要其餘方式保障咱們的用戶數據。ios

固然了,這種無狀態的的好處是快速。git

什麼叫保持登陸狀態?

好比說我在百度A頁面進行了登陸,可是不找個地方記錄這個登陸態的話。 那我去B頁面,個人登陸態怎麼保持呢?難道要url攜帶嗎?這確定是不安全的。你讓用戶再登陸一次?登個鬼,再見👋 用戶體驗不友好。github

因此咱們須要找個地方,存儲用戶的登陸數據。這樣能夠給用戶良好的用戶體驗。可是這個狀態通常是有保質期的,主要緣由也是爲了安全。redis

爲了解決這個問題,Cookie出現了。sql

Cookie

Cookie的做用就是爲了解決HTTP協議無狀態的缺陷所做的努力。數據庫

Cookie是存在瀏覽器端的。也就是能夠存儲咱們的用戶信息。通常Cookie 會根據從服務器端發送的響應的一個叫作Set-Cookie的首部字段信息, 通知瀏覽器保存Cookie。當下次發送請求時,會自動在請求報文中加入Cookie 值後發送出去。固然咱們也能夠本身操做Cookie。

以下圖所示(圖來源《圖解HTTP》)

Cookie

這樣咱們就能夠經過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,
}
複製代碼

坑點注意注意

一、注意跨域問題

二、處理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'
        })
    }
})
複製代碼

源碼

以上的代碼只是貼了核心的,源碼以下

前端後端

若有錯誤,請指教😜

相關文章
相關標籤/搜索