กำหนดสิทธิ์การเข้าถึง API ด้วย JWT ใน Express เบื้องต้น

เขียนเมื่อ 4 ปีก่อน โดย Ninenik Narkdee
expressjs nodejs authentication authorization jwt

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ expressjs nodejs authentication authorization jwt

ดูแล้ว 20,452 ครั้ง


เนื้อหาต่อไปนี้ เราจะมากำหนดการเข้าถึง หรือจำกัดสิทธิ์การใช้งาน
RESTful API ที่เราได้สร้างมาตอนที่ผ่านๆ มา
    ปกติ เมื่อเราสร้าง API ขึ้นมาเรียบร้อยแล้ว หากไม่ได้จำกัดการเข้าถึง
หรือจำกัดการใช้งาน ใครๆ ก็สามารถเรียกใช้งาน API นั้นๆ ได้  การจำกัดสิทธิ์
มีประโยชน์ในกรณีที่ สมมติเช่น เราต้องการให้ API นั้นสามารถเรียกใช้งานได้เฉพาะสมาชิก
ที่ล็อกอินเข้าใช้งานแล้วเท่านั้น หรือ สมาชิกที่ล็อกอินเข้าใช้งาน ต้องเป็น 'admin'
เท่านั้นจึงจะใช้ API ในส่วนของการเพิ่ม ลบ หรือแก้ไขข้อมูล หากเป็น 'user' จะ
ทำได้แค่ดึงข้อมูลมาแสดงหรืออ่านข้อมูลเท่านั้น แบบนี้เป็นต้น
    ในที่นี้เราจะใช้ Node-JWT ซึ่งเป็น Libraries ที่ใช้งาน Token ในการลงชื่อเข้าใช้ (Signing) และยืนยัน 
สิทธิ์ (Verification) จะไม่ขอลงในรายละเอียด JWT สามารถค้นหาข้อมูลเพิ่มเติมได้ด้วยตัวเอง
    สามารถดูรูปแบบของ JWT (JSON Web Tokens) ได้ที่ https://jwt.io/
 
    รูปแบบการทำงานแบบเข้าใจอย่างง่ายคือ ในการที่จะสามารถเข้าไปใช้งาน API ได้นั้น เราต้องทำการลงชื่อ
(Signing) เพื่อขอรับ Token จาก Server ในที่นี้ก็คือ jwt หรือ json web token เป็น Token ที่ได้จากการ
ใช้คำสั่ง jwt.sign() ใช้ค่า payload กับ secretOrPrivateKey ประกอบ
    payload ก็คือ JSON String object เช่น { id:1235,name:'ebiwayo'}
    secretOrPrivateKey ข้อความ ,buffer หรือ object ที่มีรูปแบบ algorithms ต่างๆ เช่น RSA algorithm 
        ซึ่งในที่นี้เราจะใช้ค่า RSA Key (512 bit) ที่สร้างจากหน้า RSA Key enerator
        
    เมื่อได้ Token แล้ว เวลาเรียกใช้งานไปที่ routes ของ API ใดๆ เราก็จะส่งค่า Token นี้ไปใน headers 
ในค่า key เป็น 'Authorization' และ value เป็น 'Bearer <Token>' ตัวอย่างเช่น
 
 

 
 
    ใน routes ของ API ก่อนเรียกใช้งาน เช่นเมื่อเรียกมาที่ path: '/api/users' เราก็ใช้ middleware ที่ทำการยืนยันสิทธิ์
ทำการตรวจสอบค่า Token ที่ส่งมาด้วยคำสั่ง jwt.verify() ซึ่งใช้ค่า Token กับ secretOrPrivateKey เพื่อยืนยันความถูกต้อง
หากยืนยันผ่าน ก็ใช้คำสั่ง next() ไปทำงาน middleware คำสั่งในลำดับถัดไป และดึงข้อมูลมาแสดง หากไม่ผ่านการ
ตรวจสอบหรือเกิด error ขึ้นก็ให้ส่งค่า status 401 Unauthorized กลับไปยังผู้ใช้ ตามรูปแบบที่เรากำหนด
 

 

การติดตั้ง Node-JWT (jsonwebtoken)

    ก่อนใช้งาน jwt ให้เราทำการติดตั้ง JWT ด้วยคำสั่ง
 
npm install jsonwebtoken --save
 
    ดูรูปแบบการใช้งานต่างๆ เพิ่มเติมได้ที่ JWT module 
 

 

การ Verification Token

    ให้เราข้าม มาที่หัวข้อนี้ก่อน โดยจินตนาการว่าเราได้ค่า Token มาแล้ว และจะทำการ verify
    หลังจากที่เราติดตั้ง jwt module เรียบร้อยแล้ว ให้เราสร้างไฟล์ชื่อว่า authorize.js ไว้ในโฟลเดอร์ config ดังนี้
    ไฟล์ authorize.js [config/authorize.js]
const jwt = require('jsonwebtoken')  // ใช้งาน jwt module
const fs = require('fs') // ใช้งาน file system module ของ nodejs

// สร้าง middleware ฟังก์ชั่นสำหรับ verification token
const authorization = ((req, res, next) => {
    const authorization = req.headers['authorization']  // ดึงข้อมูล authorization ใน header
	// ถ้าไม่มีการส่งค่ามา ส่ง ข้อความ json พร้อม status 401 Unauthorized
    if(authorization===undefined) return res.status(401).json({
        "status": 401,
        "message": "Unauthorized"
    })   
	// ถ้ามีการส่งค่ามา แยกเอาเฉพาะค่า token จากที่ส่งมา 'Bearer xxxx' เราเอาเฉพาะ xxxx
	// แยกข้อความด้วยช่องว่างได้ array สองค่า เอา array key ตัวที่สองคือ 1 
	// array key เริ่มต้นที่ 0 จะเได้ key เท่ากับ 1 คือค่า xxxx ที่เป้น token
    const token = req.headers['authorization'].split(' ')[1]
    if(token===undefined) return res.status(401).json({ // หากไมมีค่า token
        "status": 401,
        "message": "Unauthorized"
    })   
	// ใช้ค่า privateKey เ็น buffer ค่าที่อ่านได้จากไฟล์ private.key ในโฟลเดอร์ config
    const privateKey = fs.readFileSync(__dirname+'/../config/private.key')
	// ทำการยืนยันความถูกต้องของ token
    jwt.verify(token, privateKey, function(error, decoded) {
        if(error) return res.status(401).json({ // หาก error ไม่ผ่าน
            "status": 401,
            "message": "Unauthorized"
        })   
        console.log(error)
        console.log(decoded)     
		// หากตรวจสอบยืนยันแล้ว ผ่าน
		// ตรงนี้ จะกำหนดสิทธิ์เพิ่มเติม  มีหรือไม่ก็ได้ ในกรณีนี้   เราตรวจสอบเพิ่มเติมว่า 
		// decoded.role ต้องเป็น 'admin' ด้วยถึงจะแสดงข้อมูลได้ ซึ่งค่า role นี้เราจะส่งมากับ payload
		// ถ้าไม่ผ่าน เช่นไม่ได้ส่งค่า role มาด้วยหรือ ค่า role เป็น 'user' ไม่ใช่ 'admin' ก็จะส่งค่า status 403 Forbidden
        if(decoded.role===undefined || decoded.role!=='admin') return res.status(403).json({
            "status": 403,
            "message": "Forbidden"
        })   
		// ถ้าทุกอย่างผ่าน ทุกเงื่อนไข ก็ไปทำ middleware ฟังก์ชั่นในลำดับถัดไป
        next()
    })
})

module.exports = authorization   // ส่ง middleware ฟังก์ชั่นไปใช้งาน
 
    สังเกตว่าในขั้นตอนการตรวจสอบ เราจะใช้คำสั่ง return เมื่อต้องการส่งข้อมูลกลับไปพร้อมออกจาก router
นั่นหมายความว่า จะไม่มีการทำคำสั่งที่อยู่ด้านล่างต่อ เช่น ถ้า error ก็แจ้งข้อมูล แล้วออกจาก router เป็นต้น
แต่ถ้าไม่ติดเงื่อนไข ก็จะทำคำสั่งไล่ลำดับลงมาเรื่อยๆ
    ในขั้นตอนการ verification token ด้วย secretOrPrivateKey ในที่นี้ เราใช้รูปแบบการใช้ค่า buffer ที่อ่านจากไฟล์
private.key เดี๋ยวจะสร้างไว้ในโฟลเดอร์ config 
    หากการยืนยัน token สำเร็จ เราจะได้ค่า decoded เป็น payload ที่เราส่งมาด้วย และถูกแปลงกลับมาอยู่่ในรูป
JSON object โดยจะมี property ที่ชื่อ 'iat' เพิ่มเข้ามาด้วย ย่อมาจาก issued at เป็น timestamp เวลาที่ ออก token
ในโค้ด เรามีการอ้างอิงใช้งานค่า role property ที่เรากำหนดสถานะของผู้ใช้ว่าเป็น 'admin' หรือ 'user' เพื่อตรวจสอบ
สิทธิ์อีกขั้น รูปแบบเหล่านี้ สามารถดัดแปลงและประยุกต์เพิ่มเติมได้
 
    ต่อไปให้เราสร้างไฟล์ชื่อ private.key ในโฟลเดอร์ config ซึ่งได้บอกไปในตอนต้นว่า เราจะใช้ค่าจากการ
Generate ในหน้า RSA Key Generator[ ใช้ key size 512 bit ]
ค่าไม่จำเป็นต้องเหมือนกับตัวอย่างด้านล่าง
 
    ไฟล์ private.key [config/private.key]
 
 
 

 

การใช้งาน Authorization ฟังก์ชั่น

    หลังจากเราสร้าง authorization middleware ฟังก์ชั่นแล้ว ต่อไปก็เรียกใช้งานในไฟล์ users.js [api/users.js]
ซึ่งเป็น USERS API ของเราที่ใช้งานร่วมกับ MongoDB การใช้งานก็ไม่ยาก แทรกไปส่วนของ Method และ Routes
path ที่ต้องการ ในที่นี้เราจะทดสองใส่ไปเฉพาะในส่วนของการ GET ข้อมูลทั้งหมด
 
    ไฟล์ users.js [api/users.js]
const express = require('express')
const router = express.Router()
const { validation, schema } = require('../validator/users')
const db = require('../config/db')
const authorization = require('../config/authorize')

router.route('/users?')
    .get(authorization, (req, res, next) => { 
        db.then((db)=>{  // เมื่อมีการเชื่อมต่อไปยัง MongoDB server เรียบร้อยแล้ว
             db.collection('tbl_users')
             .find().toArray( (error, results) => { // แสดงข้อมูลของ tbl_usrs ทั้งหมด
                if(error) return res.status(500).json({
                    "status": 500,
                    "message": "Internal Server Error"
                })    
				const result = {
					"status": 200,
					"data": results
				}
				return res.json(result)     				 
             })            
        })
    })
    .post(validation(schema),(req, res, next) => {   
        db.then((db)=>{  // เมื่อมีการเชื่อมต่อไปยัง MongoDB server เรียบร้อยแล้ว
			// ก่อนเพิ่มข้อมลใหม่ 
            db.collection('tbl_lastid')  // ทำการดึง id ล่าสุดจาก tbl_lastid ที่ได้บันทึกไว้
            .findOneAndUpdate({id:1},
            { $inc: { user_id: 1 }},(error, results)=>{ // ถ้าเจอก็ให้อัพเดทค่าเก่า เพื่อให้ค่าไม่ซ้ำกัน
                if(error) return res.status(500).json({
                    "status": 500,
                    "message": "Internal Server Error"
                })    
				// ใช้ค่า user_id จาก tbl_lastid มาบวกค่าเพิ่ม เพื่อนำไปใช้งาน
                let ID = results.value.user_id+1
				// เตรียมข้อมูลที่จะทำการเพิ่ม 
                let user = {
                    "id": ID, // จะได้ค่า ID เป็น auto incremnt ที่เราประยุกต์ขึ้นมา ค่าจะไม่ซ้ำกัน
                    "name": req.body.name, 
                    "email": req.body.email 
                }            
                db.collection('tbl_users')
                .insertOne(user, (error, results) => { // ทำการเพิ่มข้อมูลไปยัง tbl_users
                    if(error) return res.status(500).json({
                        "status": 500,
                        "message": "Internal Server Error" 
                    })
                    // เพื่อไม่ต้องไปดึงข้อมูลที่เพิ่งเพิม มาแสดง ให้เราใช้เฉพาะ id ข้อมูลใหม่ที่เพิ่งเพิม
                    // รวมกับชุดข้อมูลที่เพิ่งเพิ่ม เป็น ข้อมูลที่ส่งกลับออกมา
                    user = [{_id:results.insertedId, ...user}]
                    const result = {
                        "status": 200,
                        "data": user
                    }
                    return res.json(result)     
                })                                      
            })
        })        
    })
  
router.route('/user/:id')
    .all((req, res, next) => { 
        db.then((db)=>{  // เมื่อมีการเชื่อมต่อไปยัง MongoDB server เรียบร้อยแล้ว
            db.collection('tbl_users') // แสดงข้อมูล user ตาม id ที่ส่งมา เพื่อให้แน่ใจว่าจะเป็นตัวเลข เราใส่ +นำหน้าตัวแปร
            .find({id:+req.params.id}).toArray( (error, results) => {
				// หาก error หรือไม่พบข้อมูล
                if(!results.length) return res.status(400).json({
                    "status": 400,
                    "message": "Not found user with the given ID"
                })                 
                res.user = results // ส่งต่อข้อมูลไปยัง method ที่มีการใช้งาน
                next()
            })
        })
    })
    .get((req, res, next) => { 
		// ถ้าเป็นแสดงข้อมํลของ id ที่ส่งมา ใช้ค่า results ที่ส่งมาจาก middleware ก่อนหน้า แล้วนำไปแสดง
        const result = {
            "status": 200,
            "data": res.user
        }
        return res.json(result)
    })
    .put(validation(schema),(req, res, next) => {   
        db.then((db)=>{ // เมื่อมีการเชื่อมต่อไปยัง MongoDB server เรียบร้อยแล้ว
		    // เมื่อมีการแก้ไขข้อมูล  เตรียมข้อมูลสำหรับแก้ไข
            let user = {
                "id": +req.params.id,
                "name": req.body.name, 
                "email": req.body.email 
            }              
            db.collection('tbl_users')
            .updateOne({id:+req.params.id}, // ทำการแก้ไขค่าให้ตรงกับ id ที่กำหนด
            { $set: user }, (error, results) => {
                if(error) return res.status(500).json({
                    "status": 500,
                    "message": "Internal Server Error" 
                })
                // ถ้ามีการแก้ไขค่าใหม่ 
                if(results.modifiedCount > 0) {
                    // เอาค่าฟิลด์ทีได้ทำการอัพเดท ไปอัพเดทกับข้อมูลทั้งหมด
                    user = Object.assign(res.user[0], user)
                }else{ // มีการอัพเดท แต่เป็นค่าเดิม
                    user = res.user
                }                
                const result = {
                    "status": 200,
                    "data": user
                }
                return res.json(result)        
            })
        })
    })
    .delete((req, res, next) => { 
        db.then((db)=>{ // เมื่อมีการเชื่อมต่อไปยัง MongoDB server เรียบร้อยแล้ว
            db.collection('tbl_users')
            .deleteOne({id:+req.params.id}, (error, results) => {
                if(error) return res.status(500).json({
                    "status": 500,
                    "message": "Internal Server Error" 
                })
                const result = {
                    "status": 200,
                    "data": res.user
                }
                return res.json(result)        
            })
        })
    })
  
module.exports = router
 
    จะเห็นว่า เราแค่ทำการเรียก authorization module มาใช้งาน จากนั้นกำหนดเข้าไปใน method ที่ต้องการ
ไว้ในส่วนของ middleware ฟังก์ชั่น โดยในกรณีนี้ เราไว้ด้านหน้าสุด ก่อน middleware ฟังก์ชั่นอืน
 .get(authorization, (req, res, next) => { 
    ตัวอย่าง ถ้าเป็นกรณีการ POST การเพิ่มข้อมูล เรามี middleware ฟังก์ชั่นสำหรับตรวจสอบความถูกต้องของรูปแบบ
ข้อมูลที่จะเพิ่ม ชื่อว่า validation เราก็เพิ่มไปก่อน ฟังก์ชั่นนี้ ซึ่งหากไม่ผ่านการตรวจสอบการเข้าใช้งาน ก็จะไม่ทำคำสั่ง
validation จะได้เป็น
.post(authorization, validation(schema),(req, res, next) => {   
    เมื่อเราได้ขั้นตอนการ verification ไปแล้ว ต่อไปเราก็กลับมาที่ขึ้นตอนการขอ Token ที่จะส่งไป verify ด้วยฟังก์ชั่น
ทีเราสร้างขึ้นข้างต้น
 
 
 

การ Signing Token

    เพื่อจำลองขั้นตอนการใช้งาน JWT เราจะมีรูปแบบการจำลองเหตุการณ์ดังนี้คือ
    เราจะสมมติว่า ผู้ใช้ได้มีการทำการล็อกอินไปยัง path: '/login' แต่เราจะไม่ได้ทำรูปแบบการล็อกอินจริงๆ
แค่จำลองว่า เมื่อเรียกมายัง path: '/login' ก็คือได้ทำการล็อกอินแล้ว การล็อกอินก็คือการ Authentication เป็น
การยืนยันตัวตนว่าเราเป็นใคร โดยการล็อกอินผ่านระบบ ปกติ เราก็จะเห็นว่า ผู้ใช้จะทำการกรอก username และ password
แล้ว submit ส่งข้อมูลไปตรวจสอบกับฐานข้อมูล หลังจากนั้นก็ดึงข้อมูลบางส่วนของผู้ใช้ไปใช้งาน 
    ให้เราสร้างไฟล์ login.js ในโฟลเดอร์ routes และกำหนดโค้ดเป็นดังนี้
 
    ไฟล์ login.js [routes/login.js]
 
 
const express = require('express')
const router = express.Router()
const jwt = require('jsonwebtoken')  // ใช้งาน jwt module
const fs = require('fs') // ใช้งาน file system module ของ nodejs

router.get('/', function(req, res, next) {
	// ใช้ค่า privateKey เป็น buffer ค่าที่อ่านได้จากไฟล์ private.key ในโฟลเดอร์ config
    const privateKey = fs.readFileSync(__dirname+'/../config/private.key')
	// สมมติข้อมูลใน payload เช่น id , name , role ค่าเหล่านี้ เราจะเอาจากฐานข้อมูล กรณีทำการล็อกอินจริง
    const payload = {
        id:20134,
        name:'ebiwayo',
        role:'admin'
    }
	// ทำการลงชื่อขอรับ token โดยใช้ค่า payload กับ privateKey
    const token = jwt.sign(payload, privateKey);
	// เมื่อเราได้ค่า token มา ในที่นี้ เราจะแสดงค่าใน textarea เพื่อให้เอาไปทดสอบการทำงานผ่าน postman
	// ในการใช้งานจริง ค่า token เราจะส่งไปกับ heaer ในขั้นตอนการเรียกใช้งาน API  เราอาจจะบันทึก
	// ไว้ใน localStorage ไว้ใช้งานก็ได้
    var html = 'Login Page Token: <br>'
    html += '<textarea rows="5" cols="50">'+token+'</textarea>'
    res.send(html)
})

module.exports = router
 
   ต่อไปเรียกใช้งาน router ในไฟล์ app.js กำหนดเป็นดังนี้
 
    ไฟล์ app.js
const express = require('express')  // ใช้งาน module express
const app = express()  // สร้างตัวแปร app เป็น instance ของ express
const path = require('path') // เรียกใช้งาน path module
const createError = require('http-errors') // เรียกใช้งาน http-errors module
const port = 3000  // port 
 
// ส่วนของการใช้งาน router module ต่างๆ 
const indexRouter = require('./routes/index')
const userRouter = require('./routes/users')
const loginRouter = require('./routes/login')
const userApi = require('./api/users')
 
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.set('view options', {delimiter: '?'});
// app.set('env','production')
 
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(express.static(path.join(__dirname, 'public')))
 
// เรียกใช้งาน indexRouter
app.use('/', indexRouter)
app.use('/user', userRouter)
app.use('/login', loginRouter)
app.use('/api', [userApi]) 

// ทำงานทุก request ที่เข้ามา 
app.use(function(req, res, next) {
    var err = createError(404)
    next(err)
})
 
// ส่วนจัดการ error
app.use(function (err, req, res, next) {
    // กำหนด response local variables 
    res.locals.message = err.message
    res.locals.error = req.app.get('env') === 'development' ? err : {}
 
    // กำหนด status และ render หน้า error page
    res.status(err.status || 500) // ถ้ามี status หรือถ้าไม่มีใช้เป็น 500
    res.render('error') 
})
 
app.listen(port, function() {
    console.log(`Example app listening on port ${port}!`)
})
 
 

ทดสอบการใช้งาน JWT Authorization

    ให้เราเปิดบราวเซอร์ไปที่ path: '/login' ซึ่งเราจำลองการล็อกอินไว้ 
 
 

 
 
    จะเห็นว่าในขึ้นตอนนี้ เสมือนว่าเราได้ทำการล็อกอินแล้ว และได้ค่า token สำหรับส่งไปใช้งาน
ในขั้นตอนการเรียกใช้งาน API เราลองนำค่านี้ไป decode ค่าในเว็บไซต์ https://jwt.io/
 
 

 
 
    เราจะเห็นว่า ค่า payload เป้นค่าเดียวกับที่เรากำหนดก่อนถูกแปลงเป็น JWT ยกเว้นค่า iat ที่เพิ่มเข้ามาทีหลัง
 
    ทีนี้เราลองทดสอบกับ POSTMAN เรียกใช้งาน API โดยที่ยังไม่กำหนด headers 'Authorization' 
 
 

 
 
    เมื่อเรา GET ไปที่ '/api/users' โดยไม่ส่งค่า headers['authorization'] ก็จะขึ้นสถานะ 401
พร้อมข้อความว่า Unauthorized ดังรูป
    ทั้งนี้ก็เพราะ เราไม่ได้รับสิทธิ์การใช้งาน API นี้นั่นเอง 
    ต่อไปเราทดสอบส่งค่า  headers['authorization'] ไปด้วย เป็นดังนี้
 
 

 
 
    คราวนี้เราสามารถเข้าถึง API ได้เนื่องจากผ่านการ Verification Token และ เงื่อนไข role
    
    สุดท้ายเรามาลองเปลี่ยน ค่า role ในไฟล์ login.js เป็น 'user' แล้วทำการเรียก path: '/login'
ใหม่ เพื่อเอาค่า token ค่าใหม่ไปทดสอบ

 

 
 
    ในกรณีนี้ ถึงแม้เราจะผ่านเงื่อนไขการ verification token แต่ก็จะไม่ผ่านเงื่อนไข role ที่เรากำหนด
เพื่มเติมว่า ผู้ใช้ต้องเป็น 'admin' เท่านั้นถึงจะใช้งาน API นี้ได้
 
 

 
 
    ผลลัพธ์ที่ได้จะเป็นสถานะ 403 Forbidden 
 
    เนื้อหาการทำ Authorization ให้กับ RESTful API ข้างต้น เป็นแนวทาง สามารถนำไปประยุกต์หรือปรับ
แต่งเพิ่มเติมตามต้องการ เนื้อหาตอนหน้า เราจะมาดูในเรื่องของระบบสมาชิก การสมัครสมาชิกใหม่
การล็อกอิน ที่เราจะทำแบบใช้งานร่วมกับ Database จริง ไม่ได้สมมติขึ้นเหมือนข้างต้น
    สำหรับการใช้งาน Database ในบทความเกี่ยวกับ Express ในลำดับต่อๆ ไป จะขอใช้เป็น MongoDB 
ประกอบ รอติดตามบทความตอนหน้า


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ









เนื้อหาที่เกี่ยวข้อง









URL สำหรับอ้างอิง











เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ