由於須要識別用戶是誰,不然怎麼在網站上看到我的相關信息呢?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'
})
}
})
複製代碼
以上的代碼只是貼了核心的,源碼以下
若有錯誤,請指教😜