2019年12月26日 星期四

[Vue.js] 跨元件的資料傳遞 Eventbus 簡易使用範例


Eventbus可以達成跨元件的資料傳遞,這個範例只是作為Eventbus效果的示範,依據實際的應用時,不一定使用eventbus是最合適的,可以考量使用props/emit父子元件的傳遞方式,或是vuex來需要共用的資源。

先參考一下整體的檔案結構:
├── App.vue
├── components
│   ├── Button.vue
│   └── DisplayBoard.vue
├── libs
│   └── eventbus.js
└── main.js

eventbus.js
import Vue from 'vue';
export const EventBus = new Vue();

MyButton.js
傳遞eventbus的元件,當按下按鈕時,會將目前的值傳出去,以$emit發出一個 "add-count" 的事件。
<template>
  <button @click="onClickCountAdd">Click</button>
</template>

<script>
// Import the EventBus we just created.
import { EventBus } from '../libs/eventbus.js';

export default {
  data() {
    return {
      clickCount: 0,
    }
  },
  methods: {
    onClickCountAdd() {
      this.clickCount++;
      // Send the event on a channel (add-count) with a payload (the click count.)
      EventBus.$emit('add-count', this.clickCount);
    }
  }
}
</script>

DisplayBoard.js
註冊eventbus的元件,$on是註冊;$off是取消,兩者必須成雙出現,可以避免產生重複綁定。display-board註冊 "add-count" 的事件,後面的參數是時間接收到事件時,負責處理的method。
<template>
  <div v-text="displayCount" />
</template>

<script>
// Import the EventBus we just created.
import { EventBus } from '../libs/eventbus.js';

export default {
  data() {
    return {
      displayCount: 0,
    }
  },
  created() {
    this.handleClick = this.handleClick.bind(this);
    // Listen to the event.
    EventBus.$on('add-count', this.handleClick);
  },
  destroyed() {
    // Stop listening.
    EventBus.$off('add-count', this.handleClick);
  },
  methods: {
    handleClick(clickCount) {
      this.displayCount = clickCount;
    }
  }
}
</script>

App.js
<template>
  <div>
    <my-button />
    <display-board />
  </div>
</template>

<script>
import MyButton from './components/MyButton'
import DisplayBoard from './components/DisplayBoard'

export default {
  components: {
    MyButton,
    DisplayBoard,
  }
}
</script>

最終成果,隨著my-button內按鍵點擊,display-board需呈現目前的點擊數。



2019年12月25日 星期三

Symfony4.3 基本應用【第四篇】權限與安全 Authentication & Security



你更傾向影片教學?查看教學影片

Symfony的安全系統非常強大,但是設定方法常常讓人困擾,別擔心!在這篇文章,將一步一步的教你如何設定應用程式的安全系統:
  1. 安裝套件
  2. 建立User Class
  3. 認證與防火牆
  4. 拒絕存取(權限)
  5. 獲取目前的User物件

還有幾個重要的主題也會在後面討論。


1) 安裝

在應用程式裡使用安全系統的功能前,先透過
Symfony Flex執行下面的指令安裝:
composer require symfony/security-bundle


2a) 建立你的User Class

無論你將如何認證(authenticate)(e.g. 登入表單或是API tokens)或者你的使用者資料存放在哪裡(資料庫、單一登入入口),你都必須先建立一個"User" class,最簡單的方式是使用MakerBundle

我們先假設你會把使用者資料用Doctrine存在資料庫裡:
php bin/console make:user

The name of the security user class (e.g. User) [User]:
> User

Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes

Enter a property name that will be the unique "display" name for the user (e.g.
email, username, uuid [email]
> email

Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes

created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml
就是這樣!指令會詢問幾個問題來了解你的需求並產生User class,最重要的是檔案 User.phpUser class唯一的規定就是必須實作(implemnt) UserInterface,如果有需要你也可以任意加上任何其他欄位或是程式邏輯,若你的 User class是一個實例(entity)(像是上述的例子),你可以使用make:entity command來加上更多欄位,並且要記得對新的實例執行移植(migration)。
php bin/console make:migration
php bin/console doctrine:migrations:migrate


2b) The "User Provider"

除了 User class之外,你還需要一個"User provider":一個class,能夠幫助你從session重新載入使用者資訊,以及一些選擇性的功能像是remember meimpersonation

幸運地是,指令 make:user 已經幫你在 security.yaml 以關鍵字(key) providers 設定好了。

如果你的 User class是一個實體,你不需要額外做任何事,但如果你的class不是實體,指令 make:user 會需要產生一個需要由你完成的class UserProvider ,了解更多關於user providers的資訊:User Providers


2c) 密碼編碼(Encoding Passwords)

並不是所有應用程式的"user"都需要密碼,如果用戶需要設定密碼,你可以在 security.yaml 決定如何將密碼進行編碼,make:user 指令會重新幫你設定:
# config/packages/security.yaml
security:
    # ...

    encoders:
        # use your user class name here
        App\Entity\User:
            # Use native password encoder
            # This value auto-selects the best possible hashing algorithm.
            algorithm: auto
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <encoder class="App\Entity\User"
            algorithm="bcrypt"
            cost="12"/>

        <!-- ... -->
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    // ...

    'encoders' => [
        'App\Entity\User' => [
            'algorithm' => 'bcrypt',
            'cost' => 12,
        ]
    ],

    // ...
]);
現在Symfony知道你想要如何對密碼編碼,你可以在儲存使用者到資料庫之前,使用 UserPasswordEncoderInterface 這個服務。

例如,透過DoctrineFixturesBundle,你可以在資料庫建立一些用戶虛擬資料:
php bin/console make:fixtures

The class name of the fixtures to create (e.g. AppFixtures):
> UserFixtures
使用這個服務對密碼編碼:
// src/DataFixtures/UserFixtures.php

+ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
// ...

class UserFixtures extends Fixture
{
    private $passwordEncoder;

    public function __construct(UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->passwordEncoder = $passwordEncoder;
    }

    public function load(ObjectManager $manager)
    {
        $user = new User();
        // ...

        $user->setPassword($this->passwordEncoder->encodePassword(
            $user,
            'the_new_password'
        ));

        // ...
    }
}
你也可以執行下面指令手動編碼密碼:
php bin/console security:encode-password


3a) 認證與防火牆(Authentication & Firewalls)

安全系統的設定都在 config/packages/security.yaml,最重要的部分是 firewalls
# config/packages/security.yaml
security:
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <firewall name="dev"
            pattern="^/(_(profiler|wdt)|css|images|js)/"
            security="false"/>

        <firewall name="main">
            <anonymous/>
        </firewall>
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    'firewalls' => [
        'dev' => [
            'pattern'   => '^/(_(profiler|wdt)|css|images|js)/',
            'security'  => false,
        ),
        'main' => [
            'anonymous' => null,
        ],
    ],
]);
認證系統的"firewall":底下的設定定義用戶如何進行認證(e.g. 登入表單、API token等等)。

有一個防火牆對每一個請求都啟用效果:Symfony使用關鍵字(key)
pattern 尋找第一個符合(你也可以match by host or other things)。dev 防火牆是虛設的:它只會確保你不會不小心阻擋Symfony開發工具 - 前綴為 /_profiler/_wdt的URLs 。

所有真正的URLs會由main防火牆處理(如果沒有 pattern key代表適用所有的URLs),但是這並不表示所有的URL都需要認證。多虧了 anonymous key,這個防火牆可以被匿名訪問。

事實上,如果你馬上回到首頁,你"將會"發現你被"認證(authenticated)"為 anon. ,別被下面Authenticated的"Yes"騙了,防火牆驗證不出你的身分,所以你是匿名訪客。



你將會學到如何拒絕某些URLs或控制器的存取。

如果你看不到底下工具列,請安裝profiler
composer require --dev symfony/profiler-pack

現在我們已經了解防火牆,下一步是建立一個用戶認證的方法!


3b) 對用戶認證

Symfony認證有一點神奇,因為你不需要建立路由或是控制器來處理登入,你只要啟動認證提供者(authentication provider):某些程式在控制器之前會自動被呼叫。

Symfony有幾個內建的認證提供者(built-in authentication providers),如果使用情境與其中一個正好相符,那就太棒了!但是在大部分的情況,包含一個登入表單,我們建議建立一個Guard Authenticator: 一個可以讓你控制每一個部分認證步驟的class(請看下個章節)。

如果應用程式透過第三方服務像是Google、Facebook或是Twitter(社群登入)讓使用者登入,請查看HWIOAuthBundle


Guard Authenticators
Guard authenticator是提供認證步驟完整的控制權的class,有很多不同方式建立認證,所以這邊有一些常見的情境:
  • 如何建立登入表單
  • 客製化認證系統(API Token)

如果要瞭解更詳細的認證和其運作方式,請看Custom Authentication System with Guard (API Token Example)


4) 拒絕存取、角色以及其他認證

使用者現在可以透過登入表單登入你的應用程式,太棒了!現在你需要學習如何拒絕訪問,以及操作User物件,這被稱為authorization,它的工作是決定用戶是否可以使用某些資源(URL、model、呼叫method等等...)。

認證分為兩個部分:
  1. 使用者在登入後獲得特定的角色列表(e.g. ROLE_ADMIN )。
  2. 你加入特定的代碼,使得資源(e.g. URL、controller)需要特定的"屬性(attribute)"(大多像是一個 ROLE_ADMIN 的角色)才能夠存取。

角色(Roles)
當用戶登入,Symfony呼叫 User 內的方法 getRoles() 來決定此用戶的角色,在先前產生的 User ,角色為一個存放在資料庫的陣列,且所有的使用者至少會有一個角色:ROLE_USER
// src/Entity/User.php
// ...

/**
 * @ORM\Column(type="json")
 */
private $roles = [];

public function getRoles(): array
{
    $roles = $this->roles;
    // guarantee every user at least has ROLE_USER
    $roles[] = 'ROLE_USER';

    return array_unique($roles);
}
這是一個很常見的狀況,但你可以根據使用者需要什麼角色來決定怎麼做,這邊有一些準則可以參考:
  • 每一個角色必須ROLE_ 作為開頭(否則,無法如預期運作)
  • 除了上述規則,角色只是一段字串,你可以發明所需的內容(e.g. ROLE_PRODUCT_ADMIN)

你將使用這些角色來授予特定區域的存取權限,你還可以使用角色層次性結構(role hierarchy),使得擁有某些角色會自動獲得其他角色。

拒絕訪問的代碼設定(Add Code to Deny Access)
有兩種方法來拒絕訪問:
  1. access_control in security.yaml讓你可以確保URL格式(e.g. /admin/*),簡單但是彈性較少。
  2. in your controller (or other code)

安全的URL格式 (access_control)
保護應用程式最簡單的方法之一就是在 security.yaml 設定完整的URL格式,例如,所有 ROLE_ADMIN 的URL路徑都以 /admin 開頭:
# config/packages/security.yaml
security:
    # ...

    firewalls:
        # ...
        main:
            # ...

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: '^/admin', roles: ROLE_ADMIN }

        # the 'path' value can be any valid regular expression
        # (this one will match URLs like /api/post/7298 and /api/comment/528491)
        - { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER }
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <firewall name="main">
            <!-- ... -->
        </firewall>

        <!-- require ROLE_ADMIN for /admin* -->
        <rule path="^/admin" role="ROLE_ADMIN"/>

        <!-- the 'path' value can be any valid regular expression
             (this one will match URLs like /api/post/7298 and /api/comment/528491) -->
        <rule path="^/api/(post|comment)/\d+$" role="ROLE_USER"/>
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    // ...

    'firewalls' => [
        // ...
        'main' => [
            // ...
        ],
    ],
    'access_control' => [
        // require ROLE_ADMIN for /admin*
        ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],

        // the 'path' value can be any valid regular expression
        // (this one will match URLs like /api/post/7298 and /api/comment/528491)
        ['path' => '^/api/(post|comment)/\d+$', 'roles' => 'ROLE_USER'],
    ],
]);
只要你需要,可以設定多個URL格式,每一個URL格式都是正規表達式(regular expression),但是每一個請求只會和其中一個配對:Symfony會從設定表的開頭依序比對,直到找到第一個符合的就停止:
# config/packages/security.yaml
security:
    # ...

    access_control:
        # matches /admin/users/*
        - { path: '^/admin/users', roles: ROLE_SUPER_ADMIN }

        # matches /admin/* except for anything matching the above rule
        - { path: '^/admin', roles: ROLE_ADMIN }
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <rule path="^/admin/users" role="ROLE_SUPER_ADMIN"/>
        <rule path="^/admin" role="ROLE_ADMIN"/>
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    // ...

    'access_control' => [
        ['path' => '^/admin/users', 'roles' => 'ROLE_SUPER_ADMIN'],
        ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
    ],
]);
在路徑之前加上
^ 表示只有URLs以此模式開頭才算是符合,例如:有個路徑
/admin (沒有加上 ^ )將會與 /admin/foo,甚至是 /foo/admin

每一個 access_control 也可以配對IP位址、主機名稱和HTTP methods,也可以用來將用戶重新導向到 https 版本的URL,更多請看How Does the Security access_control Work?

安全的控制器和其他程式碼 (Securing Controllers and other Code)
你可以在控制器裡拒絕存取:
// src/Controller/AdminController.php
// ...

public function adminDashboard()
{
    $this->denyAccessUnlessGranted('ROLE_ADMIN');

    // or add an optional message - seen by developers
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN');
}
就是這樣,如果不允許被存取,會拋出一個特殊的例外 AccessDeniedException ,且不會執行控制器內其他程式,接著有兩件事會發生:
  1. 如果使用者尚未登入,他們將會被要求登入。(例如導回登入頁面)
  2. 如果使用者已經登入,但是沒有 ROLE_ADMIN ,他們將會看到顯示403拒絕存取的頁面(此頁面也可以客製)
幸虧SensioFrameworkExtraBundle,你也可以使用annotations加強控制器的安全:
// src/Controller/AdminController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

/**
 * Require ROLE_ADMIN for *every* controller method in this class.
 *
 * @IsGranted("ROLE_ADMIN")
 */
class AdminController extends AbstractController
{
    /**
     * Require ROLE_ADMIN for only this controller method.
     *
     * @IsGranted("ROLE_ADMIN")
     */
    public function adminDashboard()
    {
        // ...
    }
}
若要知道更多資訊,請看FrameworkExtraBundle documentation

模板的存取控制
如果你想要確認現在的用戶是否有某個角色權限,你可以用一個既有的函式 is_granted(),可用於任何的Twig模板:
{% if is_granted('ROLE_ADMIN') %}
    <a href="...">Delete</a>
{% endif %}

Securing other Services
詳見How to Secure any Service or Method in your Application



資料翻譯自:
https://symfony.com/doc/4.3/security.html

系列文章: