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

蒼耳mtjj發表於2019-05-16

為什麼需要登入態?

因為需要識別使用者是誰,否則怎麼在網站上看到個人相關資訊呢?

為什麼需要登入體系?

因為HTTP是無狀態的,什麼是無狀態呢?

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

我們的網站都是靠HTTP請求服務端獲得相關資料,因為HTTP是無狀態的,所以我們無法知道使用者是誰。

所以我們需要其他方式保障我們的使用者資料。

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

什麼叫保持登入狀態?

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

所以我們需要找個地方,儲存使用者的登入資料。這樣可以給使用者良好的使用者體驗。但是這個狀態一般是有保質期的,主要原因也是為了安全。

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

Cookie

Cookie的作用就是為了解決HTTP協議無狀態的缺陷所作的努力。

Cookie是存在瀏覽器端的。也就是可以儲存我們的使用者資訊。一般Cookie 會根據從伺服器端傳送的響應的一個叫做Set-Cookie的首部欄位資訊, 通知瀏覽器儲存Cookie。當下次傳送請求時,會自動在請求報文中加入Cookie 值後傳送出去。當然我們也可以自己操作Cookie。

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

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

這樣我們就可以通過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'
        })
    }
})
複製程式碼

原始碼

以上的程式碼只是貼了核心的,原始碼如下

前端後端

如有錯誤,請指教?

相關文章