Golang入門
大家都知道http請求是無狀態的,但是web應用中又需要記住登錄信息,方便後續的操作。 於是,程序員們想到了一個方法:請求的時候拿着一個令牌,服務器認證這個令牌,如果通過校驗纔會響應數據。
具體的流程如下: 1. 客戶端帶着用戶名和密碼訪問`login`接口 2. 服務器收到請求後校驗用戶名和密碼,校驗正確後,服務器發送一個`HttpResponse`響應到客戶端,其中包含`Set-Cookie`的頭部 3. 客戶端發起非登錄請求時,假如服務器給了 set-cookie,瀏覽器會自動在請求頭中添加 `cookie` 4. 服務器接收請求,分解 `cookie`,驗證信息,覈對成功後返回 response 給客戶端
const Koa = require('koa') const app = new Koa() app.use(async(ctx)=>{ if(ctx.url=== '/login'){//①①①①①①①①① ... const { username, password } = ctx.body; /** 數據庫查詢用戶,校驗成功後設置cookie * ...其他操作 */ ctx.cookies.set( //②②②②②②②②② 'username',username,{ domain:'localhost', // 寫cookie所在的域名 path:'/index', // 寫cookie所在的路徑 maxAge:1000*60*60*24, // cookie有效時長 expires:new Date('2019-2-12'), // cookie失效時間 httpOnly:false, // 是否只用於http請求中獲取 overwrite:false // 是否允許重寫 } ) ctx.body = {code:0,msg:'cookie is set'} }else{ //③③③③③③③③③③③③ let hasCookies = ctx.cookies.get('username'); //④④④④④④④④④④④④ if( hasCookies ){ /** 處理請求返回結果 * let data = await database.table.find({}) * ctx.body = { * code:0, * data:data, * msg:'success' * } */ } else { /** * 找不到cookies邏輯處理 * ctx.body = { * code:-1, * msg:'failed,has no cookies' * } */ } } }) app.listen(3000,()=>{ console.log('server is starting at port 3000') }) 複製代碼
session
上述cookie的方式可以滿足業務需求,但是存在明顯的缺點:
- 不安全,cookie存放在客戶端,容易發生CSRF攻擊
- 每次都需要傳輸
- 容量受限制,所存的字符有限
-
雖然設置
httpOnly
,但是使用document.cookie
還是可以讀取到的,容易篡改泄露
基於以上原因,程序員們又想到了另一種解決方案:session
具體流程和cookie一樣。不同的是,服務器校驗登錄信息正確後,一是將session存入數據庫(或者內存中),另一個是將session對應的key即(externalKey)寫入到cookie。
const Koa = require('koa'); const Koa_Session = require('koa-session'); const session_signed_key = ["some secret hurr"]; // 這個是配合signed屬性的簽名key const session_config = { key: 'koa:sess', /** cookie的key。 (默認是 koa:sess) */ maxAge: 4000, /** session 過期時間,以毫秒ms爲單位計算 。*/ autoCommit: true, /** 自動提交到響應頭。(默認是 true) */ overwrite: true, /** 是否允許重寫 。(默認是 true) */ httpOnly: true, /** 是否設置HttpOnly,如果在Cookie中設置了"HttpOnly"屬性,那麼通過程序(JS腳本、Applet等)將無法讀取到Cookie信息,這樣能有效的防止XSS攻擊。 (默認 true) */ signed: true, /** 是否簽名。(默認是 true) */ rolling: true, /** 是否每次響應時刷新Session的有效期。(默認是 false) */ renew: false, /** 是否在Session快過期時刷新Session的有效期。(默認是 false) */ }; // 實例化 const app = new Koa(); const session = Koa_Session(session_config, app) app.keys = session_signed_key; // 使用中間件,注意有先後順序 app.use(session); app.use(ctx => { const databaseUserName = "testSession"; const databaseUserPasswd = "noDatabaseTest"; // 對/favicon.ico網站圖標請求忽略 if (ctx.path === '/favicon.ico') return; if (!ctx.session.logged) { // 如果登錄屬性爲undefined或者false,對應未登錄和登錄失敗 // 設置登錄屬性爲false ctx.session.logged = false; // 取請求url解析後的參數對象,方便比對 // 如?nickname=post修改&passwd=123解析爲{nickname:"post修改",passwd:"123"} let query = ctx.request.query; // 判斷用戶名密碼是否爲空 if (query.nickname && query.passwd) { // 比對並分情況返回結果 if (databaseUserName == query.nickname) { // 如果存在該用戶名 // 進行密碼比對並返回結果 ctx.body = (databaseUserPasswd == query.passwd) ? "登錄成功" : "用戶名或密碼錯誤"; ctx.session.logged = true; } else { // 如果不存在該用戶名 ctx.body = "用戶名不存在"; } } else { ctx.body = "用戶名密碼不能爲空"; } } else { /**ctx.body = "已登錄" */ } } ); app.listen(3000); 複製代碼
Token
用session方式鑑權,解決了部分問題,但是仍存在不少缺點:
- cookie+session 在跨域場景表現並不好(不可跨域,domain 變量,需要複雜處理跨域)
- 如果是分佈式部署,需要做多機共享 Session 機制(成本增加)
- 查詢 Session 信息可能會有數據庫查詢操作
於是程序員們又想到了一個解決辦法: JWT(JSON Web Token)
JWT的原理是,服務器認證以後,生成一個 JSON 對象,發回給用戶,就像下面這樣:
{ "姓名": "森林", "角色": "搬磚工", "到期時間": "2020年1月198日16點32分" } 複製代碼
用戶與服務端通信的時候,都要發回這個 JSON 對象。服務器完全只靠這個對象認證用戶身份。爲了防止用戶篡改數據,服務器在生成這個對象的時候,會加上簽名。
服務器就不保存任何 session 數據了,也就是說,服務器變成無狀態了,從而比較容易實現擴展。
const Koa = require('koa') const router = require('koa-router') const jwt = require('jsonwebtoken') const jwtAuth = require('koa-jwt') const secret = 'itisvalue' const bodyParser = require('koa-bodyparser') const static = require('koa-static') const app = new Koa() app.use(static(__dirname +'/')) app.use(bodyParser()) //配置session router.post('/users/login-token', async ctx => { const {body} = ctx.request const userinfo = body.username /** 登錄邏輯處理...*/ ctx.body = { message:'login success', user:userinfo, token:jwt.sign({ data:userinfo, exp:Math.floor(Date.now() / 1000) +60*60 //token過期時間,秒爲單位 }, secret) } }) //檢查token是否合法 router.get('/users/getUser-token', jwtAuth({secret}), async ctx => { //驗證通過 ctx.body = { message:'get user datainfo success', userinfo:ctx.state.user.data } }) 複製代碼