Quản lý user permission trong VueJS

Trong việc authen trên frontend, chúng ta thường xuyên muốn thay đổi những cái mà được hiển thị cho người dùng tùy theo role. VD như chúng ta chỉ cho phép người dùng thấy bài đăng, còn tác giả hoặc quản trị thì được thấy thêm nút chỉnh sửa chẳng hạn.

100++ vị trí lập trình Front-end đang chờ bạn khám phá

Quản lý permission trong front end là một việc cực kỳ phức tạp. Trước đây bạn chắc đã từng viết:

if (user.type === ADMIN || user.auth && post.owner === user.id ) {
  ...
}

Thay vào đó bạn có thể sử dụng 1 thư viện nhỏ gọn tên là CASL giúp quản lý quyền người dùng rất đơn giản. Một khi bạn đã define quyền của mình với CASL, và set nó cho người dùng đang hoạt động, bạn có thể thực hiện bất kỳ hành động nào cũng được:

if (abilities.can('update', 'Post')) {
  ...
}

Trong bài viết này, mình sẽ hướng dẫn các bạn quản lý quyền người dùng với Vue.js và CASL.

CASL

CASL cho phép bạn define 1 set các rule mà nó có thể quy định những resource nào mà user được phép access.

Ví dụ, các rule của CASL có thể cho phép các hành động CRUD (Create, Read, Update và Delete) mà 1 user có thể thực hiện trên 1 resource hoặc data nào đó (post, comment…).

Giả sử chúng ta có 1 trang tin tức với 1 bộ các quy tắc sau:

  • Khách có thể xem bất kỳ post nào
  • Admin có thể xem tất cả các post, có thể update hoặc xóa post.

Trong CASL, chúng ta sử dụng AbilityBuilder để define các rule.

const { AbilityBuilder } = require('casl');

export function(type) {
  AbilityBuilder.define(can => {
    switch(type) {
      case 'guest':
        can('read', 'Post');
        break;
      case 'admin':
        can('read', 'Post');
        can(['update', 'delete'], 'Post');
        break;
      // Add more roles here
    }
  }
};

Bây giờ bạn có thể control dựa trên các rule mà mình đã xác định:

import defineAbilitiesFor from './abilities';

let currentUser = {
  id: 999,
  name: "Hai"
  type: "registered",
};

let abilities = defineAbilitiesFor(currentUser.type);

Vue.component({
  template: `<div v-if="showPost"><div>
             <div v-else>Please log in</div>
            `,
  props: [ 'post' ],
  computed: {
    showPost() {
      return abilities.can('read', 'Post');
    }
  }
});

Xem thêm tại đây nhé. CASL

Demo

Bây giờ mình sẽ làm 1 demo đơn giản nhé. Rule của ứng dụng là user có thể đọc tất cả các post hoặc tạo post mới, nhưng chỉ được update và xóa post do chính mình tạo ra. Mình sẽ dùng Vue.js với CASL để dễ dàng tạo rule và dễ dàng thêm action trong tương lai khi cần.

Xác định những quyền của người dùng

Chúng ta xác định quyền của user trong file resources/ability.js. Chúng ta sẽ định nghĩa các rule là một module CommonJS để đảm bảo tương thích với Node (Webpack có thể chuyển đổi thành module sử dụng trên client).

const casl = require('casl');

module.exports = function defineAbilitiesFor(user) {
  return casl.AbilityBuilder.define(
    { subjectName: item => item.type }, 
    can => {
      can(['read', 'create'], 'Post');
      can(['update', 'delete'], 'Post', { user: user });
    }
  );
};

Truy cập các quyền trong Vue

Thực hiện các bước sau:

  1. Import Vue và abilities plugin. Plugin này sẽ thêm CASL với Vue prototype, cho phép chúng ta gọi nó bên trong các component.
  2. Import tập rules vào Vue app (resources/abilities.js).
  3. Gán cho người dùng hiện tại, trên thực tế thì chúng ta sẽ lấy data user từ server, trong ví dụ này mình mặc định data trong code luôn.
  4. Hãy nhớ rằng, module abilities xuất ra 1 hàm mà chúng ta sẽ gọi đến là defineAbilitiesFor. Chúng ta sẽ truyền object user đến hàm này. Bất cứ khi nào chúng ta kiểm tra một đối tượng, chúng ta có thể thấy những permission nào được phép cho user.
  5. Thêm abilities plugin, cho phép chúng ta kiểm tra bên trong component giống như thế này this.$can(...)

Bây giờ thêm đoạn code bên dưới trong src/main.js

import Vue from 'vue';
import abilitiesPlugin from './ability-plugin';

const defineAbilitiesFor = require('../resources/ability');
let user = { id: 1, name: 'Hai' };
let ability = defineAbilitiesFor(user.id);
Vue.use(abilitiesPlugin, ability);

Quy định vài thứ cho Post

Cần có 2 thuộc tính mà 1 post cần có:

  1. Thuộc tính type. CASL sẽ sử dụng 1 subjectName call back đã define trong abilities.js để check thể loại post đang được kiểm tra.
  2. Thuộc tính của người dùng. Nếu là author thì có quyền update hoặc xóa. Trong main.js chúng ta đã cho CASL biết người dùng hiện tại là ai với defineAbilitiesFor(user.id) . Những việc CASL cần làm là check id của user có phù hợp hay không rồi gán quyền cho họ.
let posts = [
  {
    type: 'Post',
    user: 1,
    content: 'User Hai là ID = 1, chỉ có thể có quyền update hoặc xóa post này'
  },
  {
    type: 'Post',
    user: 2,
    content: 'User Hai chỉ đọc được và không có quyền gì trong post này'
  }
];

Kiểm tra quyền user trên một object

Chúng ta thêm đoạn code bên dưới vào file src/components/Post.vue:

<template>
  <div class="post">
    <div class="content">
       
      <br/><small>posted by </small>
    </div>
    <button @click="del">Delete</button>
  </div>
</template>
<script>
  import axios from 'axios';

  export default {
    props: ['post', 'username'],
    methods: {
      del() {
        if (this.$can('delete', this.post)) {
          ...
        } else {
          this.$emit('err', 'Only the owner of a post can delete it!');
        }
      }
    }
  }
</script>
<style lang="scss">...</style>

Khi user click nút xóa, hành độc click sẽ dc method del xử lý, sau đó chúng ta sử dụng CASL để check nếu người dùng hiện tại có quyền xóa hay không thông qua this.$can('delete', post) . Nếu họ có quyền thì xử lý vài action. Nếu không có thì show thông báo lỗi “Only the owner of a post can delete it!”.

Thử nghiệm trên Server-side

Những bướv bên trên thì hoàn toàn làm ở client side để cho tiện và nhanh, bây giờ chúng ta thử trên server xem sao. Nếu user xóa 1 post ở front end, chúng ta sẽ sử dụng AJAX để gửi request lên server thông qua API. Ta thêm 1 ít code trong file src/components/Post.vue

if (this.$can('delete', post)) {
  axios.get(`/delete/${post.id}`, ).then(res => {
    ...  
  });
}

Sau đó chúng ta để CASL test logic trên server, ta thêm đoạn code sau vào file server.js

app.get("/delete/:id", (req, res) => {
  let postId = parseInt(req.params.id);
  let post = posts.find(post => post.id === postId);
  if (ability.can('delete', post)) {
    posts = posts.filter(cur => cur !== post);
    res.json({ success: true });
  } else {
    res.json({ success: false });
  }
});

Vì CASL là isomorphic (chạy được cả server và phía browser chung một mã nguồn hay còn gọi là universal), đối tượng ability trên server có thể được import từ abilities.js, đỡ bị đúp code.

Tổng kết

Nói chung là dùng thư nhỏ nhẹ như CASL sẽ giúp ta tiết kiệm nhiều thời gian để làm sản phẩm, code dễ đọc hơn. Cám ơn bạn đã đọc!

TopDev lược dịch từ vuejsdevelopers.com

  API Authentication trong Laravel-Vue SPA sử dụng Jwt-auth