為什麼需要登入態?
因為需要識別使用者是誰,否則怎麼在網站上看到個人相關資訊呢?
為什麼需要登入體系?
因為HTTP是無狀態的,什麼是無狀態呢?
就是說這一次請求和上一次請求是沒有任何關係的,互不認識的,沒有關聯的。
我們的網站都是靠HTTP請求服務端獲得相關資料,因為HTTP是無狀態的,所以我們無法知道使用者是誰。
所以我們需要其他方式保障我們的使用者資料。
當然了,這種無狀態的的好處是快速。
什麼叫保持登入狀態?
比如說我在百度A頁面進行了登入,但是不找個地方記錄這個登入態的話。 那我去B頁面,我的登入態怎麼保持呢?難道要url攜帶嗎?這肯定是不安全的。你讓使用者再登入一次?登個鬼,再見? 使用者體驗不友好。
所以我們需要找個地方,儲存使用者的登入資料。這樣可以給使用者良好的使用者體驗。但是這個狀態一般是有保質期的,主要原因也是為了安全。
為了解決這個問題,Cookie出現了。
Cookie
Cookie的作用就是為了解決HTTP協議無狀態的缺陷所作的努力。
Cookie是存在瀏覽器端的。也就是可以儲存我們的使用者資訊。一般Cookie 會根據從伺服器端傳送的響應的一個叫做Set-Cookie的首部欄位資訊, 通知瀏覽器儲存Cookie。當下次傳送請求時,會自動在請求報文中加入Cookie 值後傳送出去。當然我們也可以自己操作Cookie。
如下圖所示(圖來源《圖解HTTP》)
這樣我們就可以通過Cookie中的資訊來和服務端通訊。
服務端如何配合?Session!
需要看起來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裡的,可能是使用者資料比較多。但都不會和使用者資訊表混在一起。
Cookie 和 Session 的區別
登入態保持總結
- 瀏覽器第一次請求網站, 服務端生成 Session ID。
- 把生成的 Session ID 儲存到服務端儲存中。
- 把生成的 Session ID 返回給瀏覽器,通過 set-cookie。
- 瀏覽器收到 Session ID, 在下一次傳送請求時就會帶上這個 Session ID。
- 服務端收到瀏覽器發來的 Session ID,從 Session 儲存中找到使用者狀態資料,會話建立。
- 此後的請求都會交換這個 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'
})
}
})
複製程式碼
原始碼
以上的程式碼只是貼了核心的,原始碼如下
如有錯誤,請指教?