iT邦幫忙

2022 iThome 鐵人賽

DAY 17
3

Day17 自己做一個價值幾十萬的動態網站

第十七課:完成需要會員制授權與認識cookie與JWT TOKEN懶人包

挑戰設立管理員admin並認識token與實作

上一天我們完成了所有的API測試與連接,我們現在要來讓這些API進行分類,有一些是我們管理員可以操作的,有一些是需要註冊後的一般用戶可以使用的,有一些就算沒有登入也是可以觀看的,在釐清這些分類與如何實作前,我們要先來介紹JWT

JWT介紹,JSON Web Token



知道JWT大概是什麼東西後,要利用他來配合我們Api來分授權等等動作,所以首先要先知道我們要產生憑證等東西,比如說產生一個管理員的憑證(辨識名牌)我們需要做的是在他登入的那一刻開始就產生專屬他的token

將loginApi登入產生Token並記錄在cookie

這邊我們將實作如何透過登入來產生個人的用戶Token就像識別證,並之後會利用這個識別證來判別哪些行為是可以操作哪些會被限制,目前Api都沒有相對限制權限,一樣要安裝jwt插件,所以要先

npm i jsonwebtoken
import jwt from "jsonwebtoken"


產生一段複雜的隨機金鑰 或是直接講就是一串亂碼變數只有你自己知道的,這樣最安全,可以使用openssl,使用方式就是一樣打開terminal打上

openssl rand -base64 32


openssl rand -base64 32 random隨機縮寫 為產生隨機密鑰得概念
那一樣密鑰一般來說不會亂給的情況,我們可以把他跟mongoDB的連線環境變數放在.env一起,都定義為自身環境變數的一種。

//account 張號可以輸入 信箱與使用者姓名
export const login = async (req,res,next)=>{
    const loginData = req.body
    try{
    const userData =  await User.findOne({username:loginData.account}) || await User.findOne({email:loginData.account});
    if(!userData)return(next(errorMessage(404,"沒有此使用者")))
    const isPasswordCorrect = await bcrypt.compare(loginData.password,userData.password)
    if(!isPasswordCorrect)return(next(errorMessage(404, "輸入密碼錯誤")))
    //這邊雖然知道是密碼錯誤、但也可以輸入為 "輸入帳號密碼錯誤" 來防止有心人破解密碼
    // 現在要來處理我們登入以後產生一個專屬於這個用戶的TOKEN憑證
    const token = jwt.sign({id: userData._id, isAdmin: userData.isAdmin },"secretkey") //process.env.JWT就是你自己知道並設立的金鑰
    res
    .cookie('JWT_token',token,{
        httpOnly: true
    })
    .status(200).json(`${userData.username}登入成功`)

    }catch(error)
    {
        next(errorMessage(500, "登入失敗",error))
    }
}


我們就成功產生一組Token了

使用Token來處理Api授權動作 jwt.verify

首先我們要先創立一個專門處理JWT_Token的資料夾,在裡面我們要把cookie的Token解碼,所以我們如果要從我們的Api folder主動抓取cookie要用到的是express裡面的cookie-parser,所以我們要先去index.js裡面新增cookieParser

npm i cookie-parser
import cookieParser from "cookie-parser"
app.use(cookieParser())

要新增這個才能讓express可以抓取到cookie,這樣我們下面的req就可以使用req.cookie來先做第一步抓取cookie的動作,並我們要先使用jwt.verify來先把我們的token解碼

const JWT_Token =(req,res,next)=>{
const token = req.cookies.JWT_token;//在index.js 使用app.use(cookieParser()) 來抓取
//沒抓到Token就顯示
if(!token) return next(errorMessage(401, "請先登入"))
jwt.verify(token, process.env.JWT,(err,payload)=>{//cookie解碼
if(err) return next(errorMessage(403, "TOKEN無效,解開JWT失敗"))
req.userData = payload;//解碼後應該是我們一開始sign的user.id與user.isadmin
next()
})
}

callback function名稱解釋


然後我們這邊要將JWT_Token整個大函數來當作callback函數應用的一種,因為我們這邊有兩個目的,第一個是認證token為使用者用戶,第二個是認證為管理員權限,所以這兩個的共同點都是一開始都要先通過我們的JWT_Token來先解碼

並先來處理verifyUser與verifyAdmin的權責釐清

然後了解到分類後我們就可以來完成verifyUser得部分,其實可以發現我們在JWT_Token函數時,就已經完成了verifyUse函數需要的所有東西,包括確認登入這件事情,就已經把一般用戶與還沒登入的用戶做出第一步的分類,但上圖可以發現,我們會利用到的verifyUser就是上面藍色一般使用者的權限,主要都是用來修改自己的個人資料,要防止修改其他人的,所以我們可以在verifyUser新增一個條件,如果是自己的資料才能做修改、刪除讀取等等的。






最後就是將verifyToken完成並如上圖所示將權限加入ApiRoutes中
JWT_Token的verifyUser與verifyAdmin

export const verifyUser =(req,res, next)=>{
    JWT_Token(req,res,()=>{ //req.params.id 是user的id
        const apiUserId = req.params.id
        if(req.userData.id == apiUserId || req.userData.isAdmin){next()}
        //next 讓他可以往下執行到實際要做的RoutesController
        else{next(errorMessage(403, "只能修改個人自己的權限或是你不是管理員"))}
    })
}
export const verifyAdmin =(req,res, next)=>{
    JWT_Token(req,res,()=>{
        if(req.userData.isAdmin){next()}
        //如果是管理員就往下,代表管理員token認證成功
        else{next(errorMessage(403,"你沒有管理員權限"))}
    })
}

ApiRoute示範加入verifyUser與verifyAdmin 他應該會自動在上放import,沒有的話記得加

const router = express.Router()
//更新user
router.put("/:id",verifyUser,updateUser)
//刪除
router.delete("/:id",verifyUser,deletedUser)
//讀取 單一用戶資料
router.get("/:id",verifyUser,getUser)
//讀取全部用戶資料
router.get("/",verifyAdmin,getAllUsers)

然後getAllUsers這邊也可以記得加上verifyAdmin防止一般人能看到所有使用者,與上面這邊的/:id到時候都會是verifyUser裡面所抓到的req.params.id,最後最後就要來測試token在insomnia的Api有沒有效果。

token權限在insomnia測試


測試中找bug



測試中找bug,這邊isAdmin這錯誤,很明顯是跳過了我們前面一開始先設置的條件要先登入再去確認說是不是一般用戶或是管理員身份,但卻會發現我們在使用登入時,token解碼都是正確的,代表說只是條件子句的錯誤回報沒有成功觸發,並使用console.log去更確定問題,就可以發現只是next沒有成功觸發response,代表說我們要往上尋找看是不是next函數上導入有問題,這樣一來也可以更了解next使用上,當作變數要觸發要記得導入,小細節無傷大雅卻都環環相扣。

export const verifyUser =(req,res, next)=>{
    JWT_Token(req,res,next,()=>{ //req.params.id 是user的id
        //補上了第三個變數next 才能在驗證時先觸發
        const apiUserId = req.params.id
        if(req.userData.id == apiUserId || req.userData.isAdmin){next()}
        //next 讓他可以往下執行到實際要做的RoutesController
        else{next(errorMessage(403, "只能修改個人自己的權限或是你不是管理員"))}
    })
}
export const verifyAdmin =(req,res, next)=>{
    JWT_Token(req,res,next,()=>{
     //補上了第三個變數next 才能在驗證時先觸發
        if(req.userData.isAdmin){next()}
        //如果是管理員就往下,代表管理員token認證成功
        else{next(errorMessage(403,"你沒有管理員權限"))}
    })
}







Liang邦友提供next()與callbackfunction正解版





備註:github連接更新Day17~30,預計11/2號午夜前更新完成。

再備註:props加入next()與callback function的命名要分開,較簡潔也不容易與搞混。

附上github.day17連結,並恭喜這邊Api已經完成,接下來就是會是直接的對接client Side將會將這前後端完全合體。

結論

今天我們搞定了最重要的會員制,從一開始的介紹token到後面的實作分類,這邊將會員分類,很大一部分是為了到時候後台管理的處理,以免大家都可以使用APi無權限的操作我們最重要的資料庫資料,所以token有點像是資安的第一步分級,明天也將開始進入新篇章,將Api在與我們的UI做結合,讓我們Day12~17所做的Api都能完整在網站UI介面上生效。


上一篇
「全端挑戰」User Api CRUD 與條件子句設置,bcrypt.js加密使用者密碼過程
下一篇
「全端挑戰」 proxy與同源設置、全端串接axios與useEffect介紹實作
系列文
自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
Liang
iT邦新手 4 級 ‧ 2022-10-31 22:49:11

我按照文章內容實作後發現了一個問題:
verifyUser/verifyAdmin這兩個middlewares
似乎沒有依照預期去驗證使用者是否為admin或是對應的user
只要成功登入之後,不論是否為admin都還是可以存取到所有api(ex:非admin可以getAllUser)
且user是可以隨意修改其他user的資料


本以為是我自己的實作中code有打錯
但我從github.day17連結clone下來對照測試,也發現了一樣的問題
以下是我的測試流程:
1.github.day17連結clone下來
2.先npm i,然後補上npm i cookie-parser(package.json缺少這個)
3.dotenv裡面的MONGO網址改成我自己的,
4.把account分別改成email跟usernamehttps://ithelp.ithome.com.tw/upload/images/20221031/20142358BY9abj2QbN.jpg
5.然後npm start,確認有成功連上mongoDB開始測試
6.登入一個user(非admin)後,發送請求http://localhost:5000/api/v1/users/會發現明明不是admin卻可以取得所有使用者資料,且透過http://localhost:5000/api/v1/users/:id也可以取得其他特定使用者資料,從這邊看出這兩個傢伙(arrow function)並沒有在認真工作/images/emoticon/emoticon18.gif,補上console.log再發一次request就會發現訊息沒有印出,他們的確在混!https://ithelp.ithome.com.tw/upload/images/20221031/20142358w4QsIF4g1w.jpg


arrow function沒有執行是因為JWT_Token定義上少了第四個參數callback,把它補上並呼叫,它們就不能偷懶了/images/emoticon/emoticon15.gif,原本的next()必須去掉,交給cb裡面的next()往下傳就好,不然會發生next兩次會報錯!!
https://ithelp.ithome.com.tw/upload/images/20221031/2014235833KXgyGT3r.jpg
以上是我發現的問題,不知道我的理解是不是正確?

看更多先前的回應...收起先前的回應...
K.o iT邦新手 4 級 ‧ 2022-11-01 11:35:27 檢舉

完全是正確的!大感謝Liang邦友的回覆與提供正確解方,已針對文中的錯誤去進行修正!Code review天使

K.o iT邦新手 4 級 ‧ 2022-11-01 16:22:53 檢舉

這邊補述一下 第四點 把account分別改成email跟username 這邊!
這樣設置沒有問題!但大約在Day22的時候要特別注意,因為我在設置前端UI LoginPage的欄位設置時,我是設置account欄位與password欄位,那如果改成loginData.email跟loginData.username,這邊到時候傳入得input欄位時,就要幫我再改回,email欄位、username欄位與password欄位這樣。而會使用account欄位是因為想讓使用者可以只打使用者名稱或是信箱就可以登入,就不用像註冊一樣都要打上去才能登入成功等等的,也比較符合一般網頁簡單的帳號與密碼的登入方式,postman或是insomnia的body也可以直接使用account與password來輸入測試這樣,所以看你方便這些都可以使用!

Liang iT邦新手 4 級 ‧ 2022-11-01 22:05:00 檢舉

了解/images/emoticon/emoticon12.gif

想請教一下為什麼Ver17版本找不到req.userData (verifyUser) <- 解決了

我要留言

立即登入留言