iT邦幫忙

2023 iThome 鐵人賽

DAY 11
1
自我挑戰組

初探全端之旅: 以MERN技術建立個人部落格系列 第 11

[Day11] 登入、註冊API開發和HTTP Authentication(JWT)

  • 分享至 

  • xImage
  •  

進入到第11天,發現時間不多了,所以不像之前觀念部分會補充那麼多,相關參考資料還是會放在最底下,但內容大部份會把實作的程式直接放上來。
/images/emoticon/emoticon33.gif

大綱

  1. Authentication(身份驗證)和Authorization(授權)是什麼?
  2. JWT(JSON Web Token)介紹
  3. 登入註冊API開發

1. Authentication(身份驗證)和Authorization(授權)是什麼?

這兩個名詞我自己蠻容易搞混的,所以這邊紀錄一下兩者差異

認證(Authentication):

定義:確認某人或某事的身份。
目的:確保用戶(或系統)是他們聲稱的身份。
方法:通常通過使用戶名和密碼、雙因素認證、生物識別(如指紋或臉部識別)等方式來完成。當用戶首次嘗試訪問系統或應用程式時,他們首先需要進行身份認證。
範例:當你登錄網站時,你首先需要輸入你的用戶名和密碼。這個過程就是認證。

授權(Authorization):

定義:確定已認證的主體可以訪問什麼以及他們可以做什麼。
目的:確保用戶(或系統)只能訪問他們應該訪問的資源或功能。
方法:通常基於角色、權限或其他策略來決定。一旦用戶通過認證,系統將根據用戶的角色或權限來決定他們可以訪問哪些資源或執行哪些操作。
範例:在企業網絡中,一位員工可能有許可權訪問某些文件,但不能訪問財務記錄。這個過程就是授權。
(會在下個章節實作)

總之,認證是關於確定"你是誰"而授權是關於"你可以做什麼"。兩者通常會連續發生,首先是認證,然後再來是授權。

2. JWT(JSON Web Token)簡介

JSON Web Token (JWT) 是一個開放標準 (RFC 7519),它定義了一種緊湊且自成一體的方式,用於作為 JSON 物件安全地在各方之間傳輸資訊。這些資訊可以被驗證和信任,因為它是數位簽名的。JWT 可以使用秘密(透過 HMAC 演算法)進行簽名,或者使用 RSA 或 ECDSA 的公私鑰對。 -來源:JWT官網

簡單來說,它是 JSON 格式的加密字串,其中包含敏感信息,它使我們能夠驗證不同服務間的發送者。

JWT 的結構可以分為三部分:

  • Header:標頭通常由兩部分組成:token 的類型(JWT)和簽名或加密使用的算法,例如 HMAC SHA256 或 RSA。

  • Payload:要攜帶的資料,例如使用者資訊與時間戳記,也可以用來指定 token 的過期時間。

  • Signature:簽名是為了驗證訊息在傳遞過程中有沒有被篡改,以及確認發送方的身份。簽名部分是由 header、payload和一個秘密密鑰雜湊後生成的。

詳細資料請見:
1.JWT介紹
2.什麼是JWT

3.登入和註冊API開發

使用者登入到取得JWT流程圖

https://ithelp.ithome.com.tw/upload/images/20230925/20136558heIfon9aEL.jpg

圖片來源

以下是使用者登入到取得JWT (JSON Web Token) 的過程:

  1. 使用者送出認證資料(Login):通常是帳號和密碼。
  2. 伺服器驗證:伺服器收到使用者送出的認證資料後,會根據其儲存的資料進行驗證。
  3. 生成JWT:如果認證成功,伺服器會生成一個JWT。
  4. 發送JWT:一旦JWT被生成,伺服器會將其回傳給使用者,通常是透過HTTP回應的主體或設置在HTTP的Authorization標頭中。
  5. 儲存JWT:客戶端收到JWT後,可以將它存儲在Cookie、LocalStorage或其他合適的地方,以便之後發送請求使用。
  6. 使用JWT訪問受保護資源:當使用者想要訪問一個受到保護的資源(例如API端點)時,他們必須在其請求中帶上JWT,通常是放在HTTP的Authorization標頭中。
  7. 伺服器驗證JWT:當伺服器收到一個請求並附帶JWT時,它會使用公鑰(或相同的私鑰,取決於使用的算法)來驗證Token的完整性和有效性。如果驗證成功,伺服器會處理這個請求;如果失敗,則回傳錯誤。

API 設計

https://ithelp.ithome.com.tw/upload/images/20230926/20136558PLLJXUbxLt.jpg

了解上面的大致流程後,我們要來開發API了

安裝套件和建立UserSchema

先來安裝會使用到的套件bcryptjs - 加密套件jsonwebtokenexpress-validator

npm install bcryptjs jsonwebtoken  express-validator

接著先建立User的Schema,先在models資料夾底下新增User.js

// models/User.js

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
    //使用者姓名
    fullName:{
        type: String,
        required: true
    },
    //電子郵件,不可與其他人重複
    email:{
        type: String,
        required: true,
        unique: true
    },
    //密碼
    password: {
        type: String,
        required: true
    },
    //註冊日期
    joinDate: {
        type: Date,
        default: Date.now //使用預設值
    },
    //個人介紹
    bio: {
        type: String,
        default: ''  //使用空字串,註冊時不需要此欄位,當進入到使用者介面時才可以更新
    },
    //大頭貼
    profileImage: {
        type: String,
        default: ''  //使用空字串,註冊時不需要此欄位,當進入到使用者介面時才可以更新
    }
});

module.exports = User = mongoose.model('user',UserSchema);

設定jwt的私鑰

我們在現在config/default.json裡新增jwt秘鑰名稱

{
    "mongoURI": "<資料庫連結資料>",
    "jwtSecret": "secrettoken"
}

登入API開發

接著到controllers底下建立auth-controller.js,將我們剛剛安裝的套件和Usermodel引入

//controllers/auth-controller.js

const config = require('config'); //引入剛剛設定的秘鑰位置
const bcrypt = require('bcryptjs'); 
const jwt = require('jsonwebtoken');
const { check , validationResult }  =  require('express-validator'); 
const User = require('../models/User');
const HttpError = require('../models/http-error');


const login = async(req, res, next) => {
    
    // 定義驗證規則
    const validationChains =  [
        // 檢查'email'欄位是否為有效的電子郵件格式
        check('email','請輸入有效的電子郵件').isEmail(),
        // 檢查'password'欄位是否存在
        check('password','密碼為必填欄位.').exists()
    ];

    // 執行上述的所有驗證規則
    await Promise.all(validationChains.map(validation => validation.run(req)));

    // 取得驗證結果
    const errors = validationResult(req);
    
    // 如果驗證結果有錯誤,則拋出400的HttpError
    if(!errors.isEmpty()){
        return next(new HttpError('驗證錯誤,請檢查輸入資料', 400));
    }

    const { email , password } = req.body;

    try {
        // 驗證使用者是否存在
        let user = await User.findOne({email});

        // 若使用者不存在,則拋出Error
        if(!user){
            return next(new HttpError('無效的資料,請檢查帳號密碼是否正確', 400));
        }

        // 驗證密碼是否匹配
        const isMatch = await bcrypt.compare(password, user.password);

        // 若密碼不匹配,則拋出400的HttpError
        if(!isMatch){
            return next(new HttpError('無效的資料,請檢查帳號密碼是否正確', 400));
        }
     
        // 建立要用於jsonwebtoken的資料模型
        const payload = {
            user: {
                id: user.id,
                role: user.role
            }
        }

        // 生成jsonwebtoken
        jwt.sign(
            payload,
            config.get('jwtSecret'), //取得秘鑰
            {expiresIn: '12h'}, //設定token失效時間
            (err, token) => {
                if(err) throw err;
                // 將token回傳給客戶端
                res.json({ 
                    token, 
                });
            }
        )

    } catch (err) {
         if (!(err instanceof HttpError)) {
            console.error(err.message);
            err = new HttpError('Server error', 500);
        }
        next(err);
    }
}

以上程式碼想特別補充的是,在密碼和電子郵件驗證那邊最後都是統一丟出"無效的資料,請檢查帳號密碼是否正確"的訊息,這是一個資安的考量,當使用者嘗試登入且發生失敗時,通常不要明確地告訴他們是電子郵件地址還是密碼有誤,以防止暴力破解或資料外洩。

註冊API開發

(略... 接續上方login程式碼)
const registerUser = async(req, res, next) => {

    // 驗證規則設定
    const validationChains = [
        check('fullName', '姓名為必填欄位').not().isEmpty(),
        check('email', '請輸入有效的電子郵件').isEmail(),
        check('password', '請輸入6個字元以上的密碼').isLength({ min: 6 })
    ];

    // 執行所有的驗證規則
    await Promise.all(validationChains.map(validation => validation.run(req)));

    try {
        const errors = validationResult(req);

       // 若驗證錯誤,回傳錯誤
       if (!errors.isEmpty()) {
            return res.status(400).json({
                status: 'error',
                message: '註冊失敗',
                errors: errors.array()  // 這裡會回傳一個包含所有驗證錯誤的陣列
            });
        }


        const { fullName, email, password } = req.body;

        // 檢查該電子郵件是否已存在於資料庫中
        let user = await User.findOne({ email });
        if (user) {
            return next(new HttpError('User already exists.', 400));
        }

        user = new User({
            fullName,
            email,
            password
        });

        // 使用bcrypt對密碼進行加密
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(password, salt);

        // 儲存新的使用者資訊至資料庫
        await user.save();

        //回傳註冊成功訊息
        res.status(200).json({ message: '註冊成功' }

    } catch (err) {
        // 根據錯誤的類型來決定如何處理
        if (err instanceof HttpError) {
            next(err);
        } else {
            console.error(err.message);
            next(new HttpError('Server error', 500));
        }
    }
};

exports.login = login;
exports.registerUser = registerUser;

接著回到routes資料夾裡面新增auth-routes.js

// routes/auth-routes.js
const express = require('express');
const router = express.Router();
const authControllers = require('../../controllers/auth-controller');

//@router POST api/auth/login
//@desc 使用者登入
//@access Public
router.post('/login', authControllers.login); 

//@router POST api/auth/register
//@desc 使用者註冊
//@access Public
router.post('/register',authControllers.registerUser); 

然後回到server.js

//server.js
const express = require('express');
const connectDB =  require('./config/db');
const app = express();
const port = 5000;
const postRoutes = require('./routes/posts-routes');
const authRoutes = require('./routes/api/auth-route');
const bodyParser = require('body-parser');

app.use(bodyParser.json());

connectDB();

app.use('/api/auth', authRoutes); //引入我們的auth
app.use('/api/posts', postRoutes);

app.use((err, req, res, next) => {
  //檢查是否已經向客戶端發送了HTTP header,如果已經發送了,表示已經無法再修改狀態碼和header
 if (res.headersSent) {
     return next(err);
 }
 //將錯誤的堆疊訊息(stack trace)輸出到控制台,以方便進行偵錯
 console.error(err.stack);
 
 res.status(err.status || 500);
 
 res.json({
     error: {
         message: err.message  || 'Internal Server Error'
     }
 });
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
}); 

使用Postman測試結果:

註冊失敗
https://ithelp.ithome.com.tw/upload/images/20230926/20136558obMO3kHVFk.jpg
註冊成功
https://ithelp.ithome.com.tw/upload/images/20230926/20136558W6ZmO276f7.jpg

登入失敗
https://ithelp.ithome.com.tw/upload/images/20230926/20136558acSptXiLvJ.jpg

登入成功
https://ithelp.ithome.com.tw/upload/images/20230926/20136558HLPqEGEVZi.jpg

參考資料


上一篇
[Day10] 建立文章的Schema讓文章API與真實資料庫互動
下一篇
[Day12] User API和Auth Middleware開發
系列文
初探全端之旅: 以MERN技術建立個人部落格31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言