--- tags: backend --- # 前端Cookie By : 鄭弘詣 --- ## Cookie 是什麼? 當你瀏覽某個網站時... * Server端 : 在Client端電腦系統(硬體、記憶體)中留下一些加密小檔案(文字檔)。 當你再次瀏覽該網站... * 系統便會去讀取這些小檔案來得知這兩個http request 是來自於同一個User。 * EX : 購物網站(淘寶、蝦皮)的登入、購物車、結帳資訊...等。 --- ## Cookie 是什麼? **Cookie != HTTP協定內容的功能 !!!** **準確來講,Cookie是一種可以跟HTTP一起使用的功能,並且被廣泛使用。** 複習HTTP : HTTP協定是屬於無狀態(Stateless)的協議,每一次發送請求到結束該連接後都不會記住任何使用者資訊。 EX: HTTP就像是記憶力 = 0 的早餐店老闆娘,永遠不會記得你是誰、點了什麼。 #### 為什麼需要Cookie? 舉例來說,如果每天買早餐的時候,老闆娘都會給你一張點餐明細,之後只要透過這張明細就能知道你點了什麼來給你相對應的餐點,是不是方便許多 ? ==Cookie = 網站上的號碼牌== --- ## Cookie 屬性 Cookie(複數形態為Cookies)是屬於一種小型的文字檔案,透過加密的方式儲存在用戶端(Client Side)上的資料。 1. Name-Value : Cookie的名稱與值。 2. expires : Cookie的保存期限。 3. path : 指定與cookie關連在一起的網頁。默認情況下是"path=/;"。 4. domain : cookie有效的網域名稱,讓相同/類似的domain可以享有同樣的cookie。默認情況下是本機。 5. secure : cookie的安全值,默認情況下是cookie是不安全的。 6. Size : Cookie資料的大小。 取得cookie資訊方法 : ==JS也是透過這document去操作== ``` javascript : document.cookie //這樣就能知道你的cookie ``` Chrome : 開發人員工具(F12)->Application->Storage->Cookies --- ## Cookie 的缺點 **安全性:** 由於cookie在HTTP中是明文傳遞的,其中包含的數據都可以被他人訪問,可能會被篡改、盜用。 ==->因此cookie通常不會放些敏感資訊!== **大小限制:** cookie的大小限制在4KB左右,若要做大量存儲顯然不是理想的選擇。 ==->太大的話就要去做session機制== 如會員機制,還要把其他也常用的資訊放在cookie裡,會放不下,所以要做session的機制,這樣才不會影響到網頁的流量。 (超過的話就要放到server端的session) **增加流量:** cookie每次請求都會被自動添加到Request Header中,無形中增加了流量。cookie信息越大,對服務器請求的時間也越長。 --- :::info 補充:圖片的快取放在你瀏覽器本身的快取機制,並會存在你的電腦裡面,跟cookie是分開的。 ::: --- ## 程式 [**需下載VScode外掛套件 - Live Server**](https://github.com/SeanZhenggg/Cookie) * name一樣,value不一樣的話,是會去覆蓋上一筆資料 * cookie是以文件形式存在本機端或瀏覽器,看前端怎麼寫 * cookie裡面只會有name跟value,沒辦法找到其他的cookie屬性 cookie-based的session 是以session機制建立並存取大量資料,存在session ID裡,之後造訪網站時,用這id去叫資料。 --- ### 參考連結 [ 不是工程師 ] Cookie 是文檔還是餅乾?簡述HTTP網頁紀錄會員資訊的一大功臣。 https://progressbar.tw/posts/91 聊一聊 cookie https://segmentfault.com/a/1190000004556040 [ 不是工程師 ] 會員系統用Session還是Cookie? 你知道其實他們常常混在一起嗎? https://progressbar.tw/posts/92 --- # 在React上使用cookie React-cookie(cookie的套件) 1. npm install 2. method * set 建立 * get 取得 * remove 移除 ---- ## Class-based 包起來後裡面就會有cookie的props ``` export default withCookies(App); ``` 類似redux的mapStateToProps,使用後App component會多一個叫做cookies的props ![](https://i.imgur.com/joqOPjc.png) 透過set 與 get簡單操作 ### Functional component 最近很火的react hook,讓functional component可以做到更多事情,建議可以去研究一下。 ==主要就是能讓function也使用state== ``` import { useCookies } from 'react-cookie'; ``` ![](https://i.imgur.com/DLahXxN.png) --- ### 實作時間! ![](https://i.imgur.com/S8o4mFp.png) 一個簡易的表單,input在輸入的同時會即時顯示你在輸入的內容(用state,也就是基本受控表單的概念) ![](https://i.imgur.com/UbxCN1v.png) 點選add member送出後,將input值存入cookie,並且清空input欄位,並在畫面中呈現出來 hello,{去抓cookie的值} 來歡迎某人的加入。 怎麼確定自己cookie有抓到? 關掉這個頁面的tab,再重開一個新的tab,重新進入網頁,你應該還是要看到 hello {某個人} 的字串,因為瀏覽器已經存取你的cookie了! #### Class-based ```javascript= import React, { Component } from 'react'; import { withCookies, Cookies } from 'react-cookie'; class Member extends Component { state = { name: this.props.cookies.get('name') || 'oldmo', inputValue: "" } handleSubmit = (e) => { e.preventDefault(); this.props.cookies.set('name', this.state.inputValue, { path: '/' }); this.setState({ name: this.state.inputValue, inputValue: "" }) } handleChange = (e) => { this.setState({ inputValue: e.target.value }) } render() { return ( <div> <form onSubmit={this.handleSubmit}> <input type="text" value={this.state.inputValue} onChange={this.handleChange}/> <input type="submit" value="Add member" /> </form> {this.state.name && <h1>Hello {this.state.name}</h1>} {this.state.inputValue&& <h1>this is name state: {this.state.inputValue}</h1>} </div> ); } } export default withCookies(Member); ``` #### Functional component ```javascript= import React, {useState} from 'react'; import { useCookies } from 'react-cookie'; function Cookie() { const [cookies, setCookies] = useCookies(["name"]); const [name, setName] = useState(""); const handleChange = (e) => { setName(e.target.value); //輸入框裡面的值 //將input裡的值連上要渲染出的東西 } const handleSubmit = (e) => { e.preventDefault(); setCookies('name',name, { path: '/' }); //將舊cookie的'name'被input的新name所取代 //path: 這個cookie在哪些路徑會被存取, '/'就是根目錄全部都存取的意思 setName(""); //表單送出時執行的function } return ( <div> <form onSubmit={handleSubmit}> <input name={cookies.name} type="text" value={name} onChange={handleChange} /> <input type="submit" value="Add member" /> {cookies.name && <h1>Hello {cookies.name}!</h1>} //渲染出cookies存取的name {name && <h1> this is name state: {name} !</h1>} </form> </div> ); } export default Cookie; ``` --- # 後端cookie # 用express實作cookie(袁浩) ## 安裝 ```shell npm install express-session npm install body-parser ``` ## 使用方法 #### 因為要從req.body取裡面的資料 ```javascript=1 app.use(bodyParser.urlencoded({ extended: true })) ``` #### 使用session ```javascript //設定session基本資料 const session = require('express-session'); app.use(session({ name: 'fespsession', resave: false, saveUninitialized:false, secret: 'testsecret', //session會產生給cookie,用來確認連線並允許cookie存取 cookie:{ maxAge: 1000* 60 * 60 *2,//2 小時 sameSite: true, secure: true } })) ``` ### 直接在session中建立變數userID 這個 `= user.id`的user是我自定義的物件 ```javascript req.session.userID = user.id; //使用者登出時,消滅session req.session.destroy(callback); //Cookie 的 Secure 屬性是強迫 Cookie 在傳輸時使用 SSL 加密機制。 //Cookie 大小限制4KB 超過的資訊應放在session。 ``` ## 參考方法 這邊放一個自動執行的middlware 若找到session有不是null的userID 將這個userID拿去搜尋出對應的user物件 將此user物件放到==共用物件locals==裡面 ```javascript app.use((req,res,next)=>{ const {userID} = req.session; if(userID){ res.locals.user = users.find(user => user.id ===userID); } next(); }) ``` :::success # res.locals 可以直接res.locals.變數A 這個變數A可以被所有middleware存取 ![](https://i.imgur.com/fNYcGLc.png) ::: ### Full code ```javascript=1 const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); const TWO_HOURS = 1000* 60 * 60 *2 const { PORT = 3000, NODE_ENV = 'development', SESS_LIFETIME = TWO_HOURS, SESS_NAME = 'sid', SESS_SECRET = 'ssh!quiet,it/is secret' } = process.env; const IN_PROD = NODE_ENV === 'production'; //user 的東西可以放在其他地方,如redis,這邊方便才先寫在這 const users=[ {id:1, name:'Alex',email:'alex@gmail.com',password:'test'}, {id:2, name:'Bach',email:'bach@gmail.com',password:'bachismusic'}, {id:3, name:'Charlie',email:'charlie@gmail.com',password:'thisischalie'}, {id:4, name:'Dima',email:'dima@gmail.com',password:'privet'}, ] const app = express(); app.use(bodyParser.urlencoded({ extended: true })); app.use(session({ name: SESS_NAME, resave: false, saveUninitialized:false, secret: SESS_SECRET, //key to sign the cookie //secret 會幫你把cookiehash成亂數,驗證你是不是同一個user cookie:{ maxAge:SESS_LIFETIME, //使用期限 sameSite: true, //要不要求同一個domain secure: IN_PROD //要不要透過ssh安全連線 } })) const redirectLogin = (req,res,next)=>{ if(!req.session.userID){ res.redirect('/login') }else{ next(); } } const redirectHome = (req,res,next)=>{ if(req.session.userID){ console.log('Connected Session: ' + req.session.name) res.redirect('/home') }else{ next(); } } //這是一個middleWare 知道有沒有人正在連,去找相應的id app.use((req,res,next)=>{ const {userID} = req.session; if(userID){ res.locals.user = users.find(user => user.id ===userID); //res.locals->express本來就有的東西 } next(); }) app.get('/',(req,res)=>{ const userID = req.session.userID; res.send(` <h1>Welcome!</h1> ${userID? ` <a href='/home'>Home</a> <form method='post' type='submit' action='/logout'> <button>Logout</button> </form>` : ` <a href='/login'>Login</a> <a href='/register'>Register</a>` } `); }) app.get('/home',t,(req,res)=>{ const {user} = res.locals res.send(` <h1>Home</h1> <a href='/'>Main</a> <ul> <li>Name:${user.name}</li> <li>Email:${user.email}</li> </ul> `); // A ? () : () -> 三元判斷式 }) app.get('/profile',(req,res)=>{ const {user} = res.locals; } ) app.get('/login',redirectHome,(req,res)=>{ res.send(` <h1>Login</h1> <form method='post' action='/login' /> <input type='email' name='email' placeholder='Email' required/> <input type='password' name='password' placeholder='Password' required/> <input type='submit' /> </form> <a href='/register'>Register</a> `) }); app.get('/register',redirectHome,(req,res)=>{ res.send(` <h1>Register</h1> <form method='post' action='/register' > <input name='name' placeholder='Name' required/> <input type='email' name='email' placeholder='Email' required/> <input type='password' name='password' placeholder='Password' required/> <input type='submit' /> </form> <a href='/login'>Login</a> `) }) app.post('/login',redirectHome,(req,res)=>{//22:33 const { email,password } =req.body;//Because we have bodyparser, we can access req object if(email && password){ const user = users.find(user => user.email === email && user.password ===password)// TODO:HASH if (user){ //create cookie req.session.userID=user.id; return res.redirect('/home'); } } res.redirect('/login'); }) app.post('/register',redirectHome,(req,res)=>{ const { name,email,password } =req.body; if(name&&email&&password)//TODO:HASH { const exists = users.some( user => user.email === email ) if(!exists){ const user = { id: users.length + 1, name , email, password } users.push(user); req.session.userID = user.id; return res.redirect('/home'); } } res.redirect('/register') //TODO:query /register?error=error.auth.email.TooShort }) app.post('/logout',redirectLogin,(req,res)=>{ req.session.destroy(err=>{ if(err){ return res.redirect('/home'); } res.clearCookie(SESS_NAME); res.redirect('/login'); }) }) app.listen(PORT, ()=>{ console.log(`Listening on http://localhost:${PORT}/`); }); ```