پیاده سازی لاگین بصورت MVC و ActiveRecord و جلوگیری از Session Fixation

پرسیده شده
فعالیت 1454 روز پیش
دیده شده 940 بار
4

سلام.

ساختار این پروژه همونطور که داخل عنوان هست، بصورت MVC و Active Record پیاده کردم.

از session-fixation هم استفاده شده، برای regenerate کردن session id.

این پروژه فقط برای پیاده سازی ساختار و ارتباط کدها با یکدیگر نوشته شده و فقط یک لاگین و لاگ اوت هست و نمایش پروفایل و لیست یوزر ها، البته بصورت آبجکت. view رو پیاده نکردم، البته اضافه کردنش راحته، کافیه داخل controller و action مرتبط بگیم کدوم view رندر بشه. با همین ساختار میشه هر کنترلر و مدلی رو اضافه کرد و پروژه های متفاوتی رو ایجاد کرد.

 

هنوز کار داره تا ساختار بهتر بشه ولی تازه دو روزه Nodejs رو شروع کردم، فرصت نشد خیلی، بقول معروف deep بشم روی موضوعات.

باید جلسه ی بلاگ بیاد و ببینمش تا ایده بگیرم.

توضیحات این پروژه و نکاتشو میگم.

یک کلاس DAL ایجاد کردم و داخل دایرکتوری core قرار گرفته و برای connection هام، این متد استاتیک رو نوشتم:

class DAL
{
    static connect()
    {
        if(connection == null)
        {
            connection = mysql.createConnection({
                host: process.env.MYSQL_HOST,
                port: process.env.MYSQL_PORT,
                user: process.env.MYSQL_USER,
                password: process.env.MYSQL_PASSWORD,
                database: process.env.MYSQL_DBNAME
            }).promise();
        }
        
        return connection;
    }
}

البته یک متد myQuery هم نوشتم که ازش استفاده ای نکردم.

 

route هارو داخل فولدر routes اوردم. برای مثال اگر آدرس سایت مثلا http://localhost:port روی مرورگر اجرا بشه، مسیردهی ها درون site.js فراخوانی میشه.

زمانی که مسیری فراخوانی شد، اینجا باید با استفاده از controller مربوطه اون action مورد نظرو صدا بزنیم.

مثلا برای http://localhost:port به این شکل نوشته شده:

const siteController = require('../controllers/SiteController');

router.get('/', siteController.actionIndex);

actionIndex مورد خاطی نداره و html رو بصورت مستقیم تولید کردم و میتونیم همونجا از render استفاده کنیم.

 

اما وقتی /auth/login درخواست میشه:

router.get('/login', siteController.isLogin, siteController.actionLogin);
router.post('/login', siteController.isLogin, siteController.actionLogin);

post زمانیه که دکمه ی لاگین submit میشه و مقادیر post میشن.

همونطور که میبنیید این دستورو نوشتم:

siteController.isLogin

متد isLogin قبل از actionLogin اجرا میشه، تا اگر کاربر لاگین بود، صفحه ریدایرکت بشه به صفحه ی home وگرنه با next بره به عملیات بعدی و actionLogin اجرا بشه:

isLogin(req, res, next)
{
    if(req.session.userID) {
        res.redirect('/')
    } else {
        next();
    }
}

 

اطلاعات لاگین هم بصورت زیر چک میشه و سشن ست میشه:

const {username, password} = req.body;
user.find(username, password).then(
    usr => {
        if(usr == undefined) {
            res.redirect('/auth/login');
        } else {
            req.session.userID = usr.id;
            res.redirect('/user/view/' + usr.id);
        }
    }
)

متد find در کلاس مدل User قرار داده شده که در ادامه در مورد این Model توضیح میدم.

 

دوتا route برای users ایجاد کردم:

router.get('/index', siteController.isNotLogin, userControler.actionIndex);
router.get('/view/:userID', siteController.isNotLogin, userControler.actionView);

اینجا قبل از action ها متد isNotLogin چک میشه و اگر کاربر لاگین نبود، ریدایرکت میشه به صفحه ی لاگین.

actionIndex لیست کارابرانو نشون میده و actionView آیدی رو میگیره و مثلا پروفایل کاربرو نشون میده. البته گفتم view ای در نظر نگرفتم واسشون و فقط ساختارو پیاده کردم.

 

وقتی actionView از UserController فراخوانی میشه، این کدهارو داریم:

actionView(req, res)
{
    user.findOne(req.params.userID)
    .then(usr => {
        if(usr == undefined) {
            res.send('user not found!');
        } else {
            res.send(usr);
        }
    });
}

دستور user.findOne در کد بالا، user حاوی کلاس Model اش هست. آبجکتی داخل user ایجاد نشده چون متدهای findAll و findOne بصورت static هستند. هرجا که نیاز باشه آبجکت ایجاد شده که در ادامه توضیح میدم.

const user = require('../models/User');

کد بالا در ابتدای فایل UserController اومده.

 

حالا پیاده سازی Active Record روی این پروژه رو توضیح بدم.

وقتی داخل actionView متد findOne صدا زده میشه:

static findOne(id)
{
    return dal.connect().query('select * from users where id = ?', [id])
    .then(result => {
        let objUser = null;
        let obj = result[0][0];
        if(obj != undefined) {
            objUser = new User;
            objUser.id = obj.id;
            objUser.username = obj.username;
            objUser.password = obj.password;
            objUser.confirmed = obj.confirmed;
        } else {
            throw new Error('not found');
        }

        return objUser;
    })
    .catch(err => {console.log(err)});
}

بعد از fetch شدن مقادیر داخل این متد در لاین ۸، new User انجام شده و چیزی که return میشه آبجکتی از خود کلاس مدل هست. یعنی شما میتونید در actionView از کلاس UserController با دستورات زیر به مقادیر دسترسی داشته باشید:

let id = usr.id;
let username = usr.username;
// ...

البته شما آبجکتی هم از User ایجاد نمیکردید میتونستید به همین شکل دسترسی بگیرید چون خروجی بازهم از نوع آبجکت بود ولی تفاوتش اینه در این ساختار، آبجکتی که برگشته از نوع User هست و از طریق این آبجکت به هر متدی که داخل کلاس مدل User نوشتیم، دسترسی داریم. (برای پیاده سازی active record)

برای اینکه واضح تر بگم، مثلا داخل مدل User متد update رو نوشتم:

update()
{
    return dal.connect().query('update users set username = ?, password = ?, confirmed = ? where id = ?', [this.username, this.password, this.confirmed, this.id])
    .then(result => {
        return result[0].affectedRows;
    }).catch(err => {console.log(err)});
}

حالا میتونیم مقادیر آبجکت رو تغییر بدیم و با استفاده از آبجکتی که در دست داریم، متد آپدیت رو صدا بزنیم.

متد insert هم به همین شکل.

یک متد save نوشتم، که هر وقت صدا زده بشه، چک میکنه، با توجه به پراپرتی آیدی، از قبل وجود داشته یا تازه ایجاد شده. برای اینکه اگر آیدی وجود داشته باشه یعنی این رکورد داخل دیتابیس وجود داره عمل update انجام بشه وگرنه insert:

save()
{
    if(this.id == null) {
        return this.insert();
    }
    return this.update();
}

حالا از این متد چطور میتونیم استفاده کنیمو توضیح میدم.

همونطور که گفتم، وقتی findOne صدا زده میشه، آبجکتی از نوع مدل User برگشت داده میشه. برای آپدیت این رکورد میتونیم پراپرتی های آبجکت User رو تغییر بدیم و save رو صدا بزنیم. مثلا آبجکت برگشتی از findOne رو داریم:

objUser.username = 'ali';
objUser.save();

متد save چک میکنه id وجود داره یا نه و چون این رکورد قبلا وجود داشته در دیتابیس، متد update فراخوانی میشه و username آپدیت میشه.

اما وقتی میخواهیم یک رکورد جدید اینسرت کنیم، داخل constructor مدل User من مقدار this.id رو برابره null گذاشتم، برای اینکه تشخیص بدم رکوردی fetch شده(update) یا خودم آبجکتی ایجاد کردم(insert):

constructor()
{
    this.id = null;
}

برای اینسرت به این شکل عمل میکنم:

let newUser = new User;
newUser.username = 'bidak';
newUser.password = '123';

newUser.save();

اینجا متد save عمل insert رو انجام میده.

توضیحات داده شده روی متد findAll هم قابل اجراست.

 

اما در مورد سشن، خب یخورده ریزه کاری داخلش وجود داره اما بعنوان یک نکته ی امنیتی، برای مثال، شما در مرورگر chrome لاگین میکنید، اگر شما مقدار session id داخل کوکی رو از روی مرورگر chrome کپی کنید و این مقدارو داخل مرورگر firefox ست کنید، میبینید که به محتوای سشن از طریق مقدار session id داخل کوکی، دسترسی گرفتید و بدون اینکه در firefox لاگین شوید، به محتوای لاگین شده دسترسی دارید.

برای جلوگیری از حمله ی Session Fixation یا Session Hijacking میتونید از regenerate کردن session id استفاده کنید. به این طریق که هربار که صفحه رفرش یا فراخونی میشه، سشن آیدی قبلی محتوارو از داخل سشن روی سرور میگیره و مقدار session id رو reset میکنه. میتونید پیاده کنید و با رفرش کردن صفحه مقدار جدید کوکی session رو ببینید.

const fixation = require('express-session-fixation');
app.use(fixation({
    everyRequest: true
}));

داخل فایل middlewares در فولدر core اومده.

میتونید everyRequest رو روی false بگذارید و فقط برای صفحات خاصی این عملیات رو درنظر بگیرید.

httpOnly رو true قرار بدید، البته فک کنم بصورت پیشفرض true باشه. داک رو ببینید.

اگر برای cookie مقدار maxAge ای در نظر نگیرید، بصورت پیشفرض با بستن مرورگر مقدار کوکیه سشن expire میشه، میتونید زمانی برحسب میلی ثانیه در نظر بگیرید.

 

این کلیت ماجرای پروژه بود. همونطور که گفتم فقط یک ساختار پیاده شده و روی جزئیات کار نکردم.

برای مثال روی ذخیره ی پسورد مانوری ندادم. این جور ریزه کاریا، پیچیدگی خاصی ندارن.

این مقاله رو هم میتونید مطالعه کنید که در مورد نحوه ی ایندکس گذاری و تاثیر ایندکس ها روی پرفورمنس Mysql نوشتم.

 

نکته ی آخرمم اینکه، اگر پروژه ای رو بصورت ماژولار خواستید پیاده کنید میتونید همین ساختار mvc رو برای هر ماژول ایجاد کنید. مثلا یک فولدر user رو بعنوان ماژول user بسازید و پوشه بندی و کلاس های model و controller و view رو داخل همون فولدر user ایجاد کنید.

فایل پیوست
node-mvc.zip
12.28 KB

محسن موحد
محسن موحد

18 اردیبهشت 99