مدیریت مجوزهای کاربر در Vue، با استفاده از CASL

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 21 مرداد 1397
دسته بندی ها : جاوا اسکریپت

یک مسئله وجود دارد که همگی می‌توانیم تایید کنیم: مهم نیست که چه زبان یا پلتفرمی را برای ساخت برنامه‌ها ترجیح بدهیم؛ باید نوعی کنترل و سطح دسترسی در برنامه خود، برای اطمینان از اجرای نرم آن داشته باشیم. وقتی که اولین برنامه خود را بسازید، مفهوم مجورهای کاربر برای شما عادی خواهد شد.

در زبان‌های سمت سرور، مجوز‌های کاربر می‌توانند بدون هیچ زحتمی تنظیم شوند. می‌توانید از sessionها برای نگه داشتن اطلاعات کاربر استفاده کنید و صدها کتابخانه وجود دارند که به شما در مدیریت این که کاربر چه چیزی را و در چه موقعی می‌بیند، کمک می‌کنند. و البته، می‌توانید منطق مجوزهای پیچیده را با کمک دیتابیس‌های قدرتمند، مدیریت کنید.

در JavaScript، این مسئله کمی سخت می‌شود. در این آموزش، نحوه مدیریت مجوزهای کاربر با استفاده از CASL را بررسی خواهیم کرد.

جدول محتویات:

  1. CASL‌ چیست؟
  2. شروع کار
  3. BlogManager
  4. وبلاگ
  5. بروزرسانی اسکریپت‌های سرور
  6. راه‌اندازی CASL
  7. از «قابلیت‌ها» در کامپوننت Blog استفاده کنید:
  8. بروزرسانی لینک‌های Vue Router
  9. اجرای برنامه
  10. نتیجه گیری

CASL‌ چیست؟

CASL یک کتابخانه احراز هویت JavaScript است که ما را قادر می‌سازد تا تعریف کنیم که یک کاربر داده شده، به چه نوع منابعی دسترسی دارد. CASL ما را مجبور می‌کند که مجوزها را به عنوان قابلیت در نظر بگیریم؛ یعنی این که یک کاربر در قبال نقش خود، چه کارهایی را می‌تواند و چه کارهایی را نمی‌تواند انجام دهد. نقش کاربر، می‌تواند در هنگام تعریف قابلیت‌های او تشکیل شود.

شروع کار

برای تسریع فرایند، از برنامه‌ای که در یکی از مقاله‌های قبلی ساختیم، استفاده خواهیم کرد. برای درک بهتر این مقاله نیز بهتر است آن را مطالعه کنید:

  • احراز هویت Vue و مدیریت Route با استفاده از Vue-router

در این مقاله، برنامه‌ای با انواع مختلف کاربر ساخته بودیم. برای این آموزش، آن برنامه را گسترش خواهیم داد و صفحه‌ای شامل پست‌های وبلاگ مانند اضافه خواهیم کرد، که فقط توسط سازندگان می‌توانند ویرایش شوند.

مخزن مربوط به پروژه را کپی (clone) کنید

$ git clone https://github.com/christiannwamba/vue-auth-handling

Dependencyهای مربوطه را نصب کنید:

$ npm install

CASL را نصب کنید:

$ npm install @casl/vue @casl/ability

حال که تمام پایه‌های مورد نیاز را راه‌اندازی کرده‌ایم، بیایید به سراغ ساخت کامپوننت‌ها برای برنامه خود برویم. ما بر روی یک پروژه که از قبل داشتیم کار می‌کنیم؛ پس از هدر رفتن مقدار زیادی زمان جلوگیری می‌شود. در اینجا باید ۲ کامپوننت به پروژه مورد نظر اضافه کنیم تا بتوانیم پست‌هایی را بر روی برنامه خود قرار داده، و مشاهده کنیم.

BlogManager

در ابتد، فایلی به نام BlogManager در شاخه ./src/components بسازید و این کد را در آن قرار دهید:

<template>
    <div class="hello">
        <h1>Create New Blog</h1>
        <form @submit="create">
            <input class="form-input" type="text" placeholder="Blog Title..." v-model="blog_title">
            <textarea class="form-input" v-model="blog_body" placeholder="Type content here"></textarea>
            <button>Create</button>
            <br/>
        </form>
    </div>
</template>

این کد یک صفحه HTML ساده به همراه یک فرم برای برنامه ما می‌سازد. این فرم، فرم مربوط به ساخت یک پست جدید برای وبلاگ است.

ما باید صفات داده‌ای برای اتصال فیلدهای فرم بسازیم:

[...]
<script>
  export default {
      data () {
          return {
              blog_title: null,
              blog_body: null
          }
      },
  }
</script>

حال بیایید متدی بسازیم که تایید فرم را مدیریت می‌کند:

<script>
  export default {
      [...]
      methods : {
          create(e){
              e.preventDefault()
              let user = JSON.parse(localStorage.getItem('user'))
              this.$http.defaults.headers.common['x-access-token'] = localStorage.jwt
          }
      }
  }
</script>

ما متد را ساختیم و رشته کاربر را که در localStorage ذخیره کرده بودیم، parse کردیم. این رشته کاربر در هنگام ارسال داده‌های فرم به سرور کارآمد خواهد بود. همچنین headerهای پیشفرض را برای ابزار مدیریت درخواست http خود، یعنی axios راه‌اندازی کردیم. برخی endpointهای ما نیازمند یک نشانه دسترسی برای کار کردن هستند، که باید تنظیم کنیم.

حال بیایید پست مورد نظر را به سرور خود ارسال کنیم:

<script>
  export default {
      [...]
      methods : {
          create(e){
              [...]
              this.$http.post('http://localhost:3000/blog', {
                  blog_title: this.blog_title,
                  blog_body: this.blog_body,
                  created_by : user.id
              })
              .then(response => {
                  alert(response.data.message)
                  this.blog_title = null
                  this.blog_body  = null
              })
              .catch(function (error) {
                  console.error(error.response);
              });
          }
      }
  }
</script>

پس از این که یک پاسخ موفقیت آمیز دریافت کردیم، مقدار فیلدهای فرم را مساوی با null قرار می‌دهیم تا کاربر بتواند سریعا یک پست جدید بسازد.

در اینجا کمی استایل به صفحه اضافه می‌کنیم تا ظاهر بهتری داشته باشد.

<style scoped>
h1, h2 {
    font-weight: normal;
}
button {
    border-radius: 2px;
    font-size: 14px;
    padding: 5px 20px;
    border: none;
    background: #43bbe6;
    color : #ffffff;
    font-weight: 600;
    cursor: pointer;
    transition: 0.2s all;
}
button:hover {
    background: #239be6;
    transition: 0.2s all;
}
.form-input {
    min-width: 50%;
    border: 1px #eee solid;
    padding: 10px 10px;
    margin-bottom: 10px;
}
textarea {
    resize: none;
    height: 6em;
}
</style>

وبلاگ

ما نیاز به یک کامپوننت تکی برای نمایش پست‌هایی که ساختیم، هستیم. فایل جدیدی به نام Blog.vue در شاخه ./src/components بسازید و این کد را در آن قرار دهید:

<template>
    <div class="hello">
        <h1>Welcome to blog page</h1>
        <div class="blog" v-for="blog,index in blogs" @key="index">
            <h2>{{blog.title}}</h2>
            <p>{{blog.body}}</p>
        </div>
    </div>
</template>

در بلوک کد بالا، blogs را در ساختار حلقه v-for نمایش می‌دهیم.

بیایید اسکریپت مربوط به دریافت داده‌ها را اضافه کنیم:

[...]
<script>
    export default {
        data () {
            return {
                blogs: []
            }
        },
        mounted(){
            this.$http.get('http://localhost:3000/blog')
            .then(response => {
                this.blogs = response.data.blogs
            })
            .catch(function (error) {
                console.error(error.response)
            });
        }
    }
</script>

تعریف صفت blogs به عنوان یک آرایه خالی، بسیار مهم است. هدف از این کار، جلوگیری از بروز خطاها در هنگام بارگذاری صفحات است.

همچنین، از mounted() و beforeMouin() استفاده کردیم تا کاربرانمان بتونند صفحه وبلاگ را حتی قبل از این که محتویات بارگذاری شوند، ببینند. اگر به هر دلیلی یک خطای شبکه تاخیری در بارگذاری محتویات ایجاد کند، کاربران ما مجبور به نگاه به یک صفحه خالی نخواهند شد.

حال، بیایید کمی استایل به آن اضافه کنیم تا ظاهر بهتری داشته باشد:

<style scoped>
h1, h2 {
    font-weight: normal;
}
.blog {
    width: 60%;
    border: 1px #eee solid;
    padding: 20px;
    padding-top: 0px;
    display: table;
    margin: 0 auto;
    margin-bottom: 20px;
    text-align: left;
}
.blog h2 {
    text-decoration: underline;
}
.delete {
    border-radius: 2px;
    background: #aaa;
    height: 24px;
    min-width: 50px;
    padding: 4px 7px;
    color: #ffffff;
    font-size: 14px;
    font-weight: 900;
    border: none;
    cursor: pointer;
    transition: 0.2s all;
}
.delete:hover {
    background: #ff0000;
    transition: 0.2s all;
}
</style>

بروزرسانی اسکریپت‌های سرور

حال که تغییرات قابل توجهی به Frontend برنامه خود اعمال کرده‌ایم، باید تغییرات متناظری به سرور خود نیز اعمال کنیم، تا سرور ما آن را پشتیبانی کند.

Db Manager

از شاخه ./server، فایل db.js را باز کنید و این کد را به آن اضافه کنید:

[...]
class Db {
    constructor(file) {
        [...]
        this.createBlogTable()
    }
    [...]
}

در اینجا متوجه متد this.createTable() در constructor کلاس که جدول کاربران را می‌سازد، خواهید شد. همچنین می‌خواهیم هر زمان و در هر جایی که کلاس Db ما فراخوانی می‌شود، جدول بلاگ را بسازیم.

حال بیایید متد createBlogTable را اضافه کنیم:

[...]
class Db {
    [...]
    createBlogTable() {
        const sql = `
            CREATE TABLE IF NOT EXISTS blog (
            id integer PRIMARY KEY, 
            title text NOT NULL, 
            body text NOT NULL,
            created_by integer NOT NULL)
        `
        return this.db.run(sql);
    }
}

در اینجا متد مربوط به انتخاب تمام بلاگ‌ها را اضافه می‌کنیم:

[...]
class Db {
    [...]
    selectAllBlog(callback) {
        return this.db.all(`SELECT * FROM blog`, function(err,rows){
            callback(err,rows)
        })
    }
}

همچنین متد مربوط به ساخت یک بلاگ جدید را نیز اضافه می‌کنیم:

[...]
class Db {
    [...]
    insertBlog(blog, callback) {
        return this.db.run(
            'INSERT INTO blog (title,body,created_by) VALUES (?,?,?)',
            blog, (err) => {
                callback(err)
            }
        )
    }
}

و سپس نیز متد مربوط به بروزرسانی بلاگ:

[...]
class Db {
    [...]
    updateBlog(blog_id, data, callback) {
        return this.db.run(
            `UPDATE blog SET title = ?, body = ?) where id = ${blog_id}`,
            data, (err) => {
                    callback(err)
            }
        )
    }
}

در آخر، متد حذف بلاگ:

[...]
class Db {
    [...]
    deleteBlog(blog_id, callback) {
        return this.db.run(
            'DELETE FROM blog where id = ?', blog_id, (err) => {
                callback(err)
            }
        )
    }
}

حال که اسکریپت مدیریت دیتابیس ما بروز است، بیایید اسکریپت سرور را بروزرسانی کنیم.

اسکریپت سرور برنامه

تغییراتی که در اینجا اعمال می‌نماییم، endpointهایی را در معرض سرور ما قرار می‌دهند، تا برنامه ما بتواند از آن‌ها استفاده کند. در ابتدا، به endpoint مربوط به بررسی تمام بلاگ‌ها رسیدگی می‌کنیم.

فایل app.js را باز کرده، و این کد را به آن اضافه کنید:

[...]
router.get('/blog', function(req, res) {
    db.selectAllBlog((err, blogs) => {
        if (err) return res.status(500).send("There was a problem getting blogs")

        res.status(200).send({ blogs: blogs });
    }); 
});
[...]

همانطور که می‌توانید ببینید، ما از متد selectAllBlog که پیش‌تر در اسکریپت مدیریت دیتابیس ساختیم استفاده کرده، و یک متد callback را منتقل می‌کنیم که باید پس از دریافت داده‌ها، کارهای مورد نیاز را انجام دهد.

حال، بیایید متد مربوط به ساخت یک بلاگ جدید را اضافه کنیم:

[...]
router.post('/blog', function(req, res) {
    let token = req.headers['x-access-token'];
    if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' });

    jwt.verify(token, config.secret, function(err) {
        if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });

        db.insertBlog(
            [
                req.body.blog_title,
                req.body.blog_body,
                req.body.created_by,
            ],
            function (err) {
                if (err) return res.status(500).send("Blog could not be created.")

                res.status(201).send({ message : "Blog created successfully" });
            }
        ); 
    });
});
[...]

در ابتدا بررسی می‌کنیم که نشانه دسترسی، موجود است یا خیر. اگر نیست، قبل از هر چیز دیگری، سریعا درخواست را رد می‌کنیم. گرچه اگر نشانه خالی نباشد، تلاش می‌کنیم که اعتبار آن را تایید کنیم. اگر منقضی شده باشد، درخواست را رد می‌کنیم. اگر وجود نداشته باشد، باز هم آن را رد می‌کنیم. اگر هم معتبر باشد، ادامه داده و بلاگ را به دیتابیس اضافه می‌کنیم،‌ و پس از آن نیز یک پیام موفقیت‌آمیز را نمایش می‌دهیم.

در اینجا می‌توانیم کارهای بیشتری برای اعتبارسنجی داده‌ها انجام دهیم، اما این کار کد ما را بیش از حد طولانی و پیچیده می‌کند و هدف این آموزش را از بین می‌برد.

در آخر، بیایید متد مربوط به حذف یک بلاگ را اضافه کنیم:

[...]
router.delete('/blog/:id', function(req, res) {
    let token = req.headers['x-access-token'];
    if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' });

    jwt.verify(token, config.secret, function(err) {
        if (err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });

        db.deleteBlog(
            req.params.id,
            function (err) {
                if (err) return res.status(500).send("There was a problem.")

                res.status(200).send({ message : "Blog deleted successfully" });
            }
        ); 
    });
});
[...]

و به این صورت، کار ما برای بروزرسانی سرور به پایان می‌رسد.

راه‌اندازی CASL:

ما همه موارد مورد نیاز برای استفاده از CASL در برنامه خود را داریم. بیایید با تعریف قابلیت‌هایی که کاربران می‌توانند داشته باشند، شروع کنیم. در شاخه ./src، شاخه دیگری به نام config بسازید، تا پیکربندی قابلیت‌های ما را در خود نگه دارد. در داخل این شاخه، فایلی به نام ability.js بسازید و این کد را در آن قرار دهید:

import { AbilityBuilder } from '@casl/ability'

var user = JSON.parse(localStorage.getItem('user'))
function subjectName(item) {
    if (!item || typeof item === 'string' || !user) {
            return item
    }
    else if(item.created_by === user.id || user.is_admin === 1){
            return 'Blog'
    }
}

در اینجا، تابع subjectName توسط AbilityBuilder در CASL برای تعیین این که چه کسی حق انجام کارهایی خاص را خواهد داشت، استفاده خواهد شد.

اولین چیزی که بررسی می‌کنیم، صحیح بودن دو چیز در زمانی که می‌خواهیم قابلیت‌ یک کاربر در انجام یک کار را بررسی کنیم، است.

  1. چیزی که قرار است بر رویش کاری انجام شود، خالی نیست.
  2. کاربر مورد نظر وجود دارد.

در مورد بلاگ خود، تایید می‌کنیم که دو مورد بالا صحیح هستند، سپس بررسی می‌کنیم تا ببینیم که آیا بلاگ مورد نظر توسط کاربر مورد نظر ساخته شده است، یا این که آیا این کاربر یک مدیر است؟ اگر این مورد صحیح باشد، مقدار Blog را بر می‌گردانیم، که برابر با subjectName مورد استفاده در اختصاص‌دهی قابلیت به کاربران است.

حال بیایید قابلیت‌ها را به کاربران اختصاص دهیم:

[...]
export default AbilityBuilder.define({ subjectName }, can => {
  can(['read'], 'all')
  if(user) can(['create'], 'all')
  can(['delete'], 'Blog')
})

متد can([‘read’], ‘all’) اساسا این معنی را دارد: کاربری که موارد مورد نیاز را دارد، می‌تواند هر چیزی که در برنامه موجود است را بخواند. چه بلاگ‌ها، نظرها و...

با داشتن این تعریف در ذهن، می‌توانید درک کنید که can([‘read’], ‘all’) یعنی این کاربر می‌تواند این بلاگ‌ها حذف کند.

اگر همچنان آن را درک نمی‌کنید، صبر کنید تا از آن استفاده کنیم.

پلاگین CASL را به تنظیمات Vue اضافه کنید

برای اعمال پیکربندی‌های قابلیت به صورت global، باید آن را به تنظیمات Vue خود اضافه کنیم. فایل ./src/main.js را باز کرده و آن را به این صورت ویرایش کنید:

[...]
import ability from './config/ability'
import { abilitiesPlugin } from '@casl/vue'

Vue.prototype.$http = Axios;

Vue.config.productionTip = false
Vue.use(abilitiesPlugin, ability)
[...]

ما پلاگین abilitiesPlugin در CASL، و پیکربندی‌های قابلیت موجود در بالا را وارد کرده‌ایم. پس از این که به Vue بگوییم که از آن‌ها استفاده کند، رسما با استفاده از متد $can پیکربندی‌های قابلیت را به صورت global قابل دسترسی کرده‌ایم.

از «قابلیت‌ها» در کامپوننت Blog استفاده کنید

حال که قابلیت‌ها را تعریف کرده‌ایم، بیایید آن‌ها را با کامپوننت بلاگ خود آزمایش کنیم. فایل Blog.vue را باز کرده، و این کد را به آن اضافه کنید:

<template>
    <div class="hello">
        [...]
        <div class="blog" v-for="blog,index in blogs" @key="index">
            [...]
            <button class="delete" v-if="$can('delete',blog)" @click="destroy(blog)">Delete</button>
        </div>
    </div>
</template>
[...]

ما یک دکمه حذف بر روی هر بلاگ اضاف کرده‌ایم، و آن را فقط وقتی که یک کاربر اجازه حذف آن را دارد، نمایش می‌دهیم. به یاد داشته باشید که ما فقط به یک سازنده بلاگ یا یک مدیر اجازه میدهیم که یک پست را حذف کند.

حال، بیایید متد مربوط به حذف، وقتی که یک کاربر بر روی دکمه آن کلیک می‌کند را اضافه کنیم:

[...]
<script>
  export default {
      [...]
      methods : {
          destroy(blog){
              this.$http.defaults.headers.common['x-access-token'] = localStorage.jwt
              this.$http.delete(`http://localhost:3000/blog/${blog.id}`)
              .then(response => {
                  console.log(response)
              })
              .catch(function (error) {
                  console.error(error.response);
              });
          }
      }
  }
</script>
[...]

حال وقتی که کاربرانمان صفحات بلاگ ما را بررسی می‌کنند، view آن‌ها چنین چیزی خواهد بود:

بروزرسانی لینک‌های Vue Router

آخرین مرحله برای آماده‌سازی برنامه ما، این خواهد بود. فایل ./src/router/index.js را باز کرده، و این کد را به آن اضافه کنید:

let router = new Router({
  mode: 'history',
  routes: [
    [...]
    {
        path: '/blog',
        name: 'blog',
        component: Blog
    },
    {
        path: '/blog/create',
        name: 'blogmanager',
        component: BlogManager,
        meta: { 
            requiresAuth: true
        }
    },
  ]
})
[...]

و در اینجا کار به پایان می‌رسد.

اجرای برنامه

برای اجرای برنامه، باید شاخه‌ای که بر روی آن قرار دارد را باز کنید و سپس این دستور را در ترمینال خود اجرا کنید:

$ npm run dev

همچنین، یک شاخه دیگر یا نمونه دیگری از ترمینال خود را باز کرده، و سرور را نیز راه‌اندازی کنید:

$ npm run server

نتیجه گیری

در این آموزش، دیدیم که چگونه می‌توانیم مجوزهای کاربر را در برنامه Vue با استفاده از CASL مدیریت کنیم.

منبع

مقالات پیشنهادی

مدیریت ارتباط با مشتری یا CRM چیست ؟

در فضای  کسب وکار هر شرکت و کسب و کاری که وارد بشوید مشاهده می کنید که در آنجا نرم افزار هایی متناسب با فعالیت آن کسب و کار در حال استفاده است. معمولا...

5 سوال ساده برای وقتی که به دنبال بازخورد کاربر هستید

شما نمی توانید اولین برداشت را از نو بسازید درسته؟ البته که درسته و این یکی از دلایلی است که باید وقت و تلاش زیادی صرف طراحی یک وب سایت کنید . شما می...

8 دليلي كه نبايد از يك مديريت محتوا استفاده كنيد

من تمايل دارم افرادي رو پيدا كنم كه در قدم اول ميخوان به مشتريان چنين ايده اي رو، ترويج بدن كه اون ها ميتونن سايت خودشون رو "به راحتي به كمك يه واژه پ...

آیکون های زیبا و کاربردی طراحی وب | سری سوم

آیکون ها امروزه نقش کلیدی در طراحی وب دارند بطوری که حتی برای نشون دادن اطلاعات ازشون استفاده میشه . این آیکون ها چیزی از بوجود اومدنشون نمیگذره و بوس...