[WordPress] 使用者管理與角色權限開發教學

本篇文章更新時間:2026/03/03
如有資訊過時或語誤之處,歡迎使用 Contact 功能通知或向一介資男的 LINE 社群反應。
如果本站內容對你有幫助,歡迎贊助支持


本系列文參考自 WordPress.org 官方外掛開發文件 - Users 的繁體中文版本,並加入作者實務開發經驗補充。

WordPress 角色權限系統是控制使用者存取與操作的核心安全機制。WordPress 透過「角色(Roles)」和「權限(Capabilities)」的組合,精確管理每位使用者能做什麼、不能做什麼。在外掛開發中,我們經常需要操作使用者的自訂資料(User Metadata)、檢查使用者權限、甚至建立全新的角色來滿足特殊的業務需求——例如「店長」、「審核者」、「VIP 會員」等。本文將從 User Metadata 的 CRUD 操作開始,逐步介紹 WordPress 內建角色與權限體系,並說明如何安全地自訂角色與權限。

使用者 Metadata

WordPress 的使用者資料主要儲存在兩張資料表中:wp_users 儲存帳號、信箱、密碼等核心欄位,而 wp_usermeta 則以 key-value 形式儲存額外的使用者資料。這個設計與 Post Meta 的概念完全相同——透過 Metadata API,開發者可以為每位使用者附加任意數量的自訂資料,而不需要修改資料庫結構。

常見的 User Metadata 使用場景包括:

  • 儲存使用者的偏好設定(如後台介面語言、通知偏好)
  • 記錄使用者的額外個人資料(如社群帳號連結、自我介紹)
  • 追蹤使用者的操作紀錄(如最後登入時間、瀏覽歷史)
  • 儲存外掛功能需要的使用者層級資料(如 API key、訂閱狀態)

新增 User Metadata - add_user_meta()

使用 add_user_meta() 為使用者新增 metadata。此函式的第四個參數 $unique 決定是否允許重複的 key:

/**
 * 新增使用者 metadata
 *
 * @param int    $user_id    使用者 ID
 * @param string $meta_key   metadata 名稱
 * @param mixed  $meta_value metadata 值
 * @param bool   $unique     是否唯一(預設 false,允許多筆同 key 資料)
 * @return int|false 新增的 meta_id 或失敗時回傳 false
 */
add_user_meta( $user_id, $meta_key, $meta_value, $unique );

// 範例:記錄使用者的社群帳號連結
add_user_meta( $user_id, 'myplugin_twitter_url', 'https://twitter.com/example', true );

// 範例:允許一位使用者儲存多筆技能標籤
add_user_meta( $user_id, 'myplugin_skill', 'PHP' );
add_user_meta( $user_id, 'myplugin_skill', 'JavaScript' );
add_user_meta( $user_id, 'myplugin_skill', 'WordPress' );

注意:如果 $unique 設為 true,當指定的 key 已存在時,add_user_meta() 不會新增也不會更新,而是直接回傳 false。這和 update_user_meta() 的行為不同。

讀取 User Metadata - get_user_meta()

使用 get_user_meta() 讀取使用者的 metadata 值:

/**
 * 讀取使用者 metadata
 *
 * @param int    $user_id  使用者 ID
 * @param string $meta_key metadata 名稱(留空取得全部)
 * @param bool   $single   是否回傳單一值(預設 false)
 * @return mixed 根據參數回傳不同格式
 */
get_user_meta( $user_id, $meta_key, $single );

// 範例一:取得單一值($single = true)
$twitter_url = get_user_meta( $user_id, 'myplugin_twitter_url', true );
// 回傳:'https://twitter.com/example'(字串)

// 範例二:取得該 key 的所有值($single = false)
$skills = get_user_meta( $user_id, 'myplugin_skill', false );
// 回傳:array( 'PHP', 'JavaScript', 'WordPress' )

// 範例三:取得使用者的所有 metadata
$all_meta = get_user_meta( $user_id );
// 回傳:array( 'myplugin_twitter_url' => array('https://...'), 'myplugin_skill' => array('PHP', 'JS', 'WP'), ... )

重要:$single 參數的差異是新手常見的踩坑點。當 $singletrue 時,回傳的是 metadata 的實際值(字串、陣列等);當 $singlefalse(預設)時,回傳的是包含所有該 key 值的陣列。如果你只需要一筆資料,記得傳入 true

更新 User Metadata - update_user_meta()

使用 update_user_meta() 更新使用者的 metadata。如果指定的 key 不存在,此函式會自動建立:

/**
 * 更新使用者 metadata
 *
 * @param int    $user_id    使用者 ID
 * @param string $meta_key   metadata 名稱
 * @param mixed  $meta_value 新的 metadata 值
 * @param mixed  $prev_value 選填,指定要更新的舊值(用於同一 key 有多筆值時)
 * @return int|bool 成功時回傳 meta_id,值未變更時回傳 false
 */
update_user_meta( $user_id, $meta_key, $meta_value, $prev_value );

// 範例一:更新社群帳號
update_user_meta( $user_id, 'myplugin_twitter_url', 'https://twitter.com/new_handle' );

// 範例二:如果 key 不存在,update_user_meta() 會自動新增
update_user_meta( $user_id, 'myplugin_last_login', current_time( 'mysql' ) );

// 範例三:利用 $prev_value 精確更新特定值
update_user_meta( $user_id, 'myplugin_skill', 'TypeScript', 'JavaScript' );

刪除 User Metadata - delete_user_meta()

使用 delete_user_meta() 刪除使用者的 metadata:

/**
 * 刪除使用者 metadata
 *
 * @param int    $user_id    使用者 ID
 * @param string $meta_key   metadata 名稱
 * @param mixed  $meta_value 選填,只刪除符合此值的 metadata
 * @return bool 成功回傳 true,失敗回傳 false
 */
delete_user_meta( $user_id, $meta_key, $meta_value );

// 範例一:刪除使用者的所有 myplugin_twitter_url 資料
delete_user_meta( $user_id, 'myplugin_twitter_url' );

// 範例二:只刪除特定值(例如移除某一項技能)
delete_user_meta( $user_id, 'myplugin_skill', 'PHP' );

User Metadata 的安全性考量

操作 User Metadata 時,請務必注意以下安全原則:

/**
 * 安全地更新使用者 metadata 的完整範例
 */
function myplugin_save_user_profile( $user_id ) {
    // 1. 檢查權限:確認目前使用者有權編輯此使用者資料
    if ( ! current_user_can( 'edit_user', $user_id ) ) {
        return false;
    }

    // 2. 驗證 nonce:防止 CSRF 攻擊
    if ( ! wp_verify_nonce( $_POST['myplugin_user_nonce'], 'myplugin_save_user_profile' ) ) {
        return false;
    }

    // 3. 清理輸入資料
    if ( isset( $_POST['myplugin_twitter_url'] ) ) {
        $twitter_url = esc_url_raw( $_POST['myplugin_twitter_url'] );
        update_user_meta( $user_id, 'myplugin_twitter_url', $twitter_url );
    }

    if ( isset( $_POST['myplugin_bio'] ) ) {
        $bio = sanitize_textarea_field( $_POST['myplugin_bio'] );
        update_user_meta( $user_id, 'myplugin_bio', $bio );
    }
}
add_action( 'personal_options_update', 'myplugin_save_user_profile' );
add_action( 'edit_user_profile_update', 'myplugin_save_user_profile' );

在 User Metadata 的前綴命名方面,建議統一使用外掛專屬的前綴(如 myplugin_),避免與其他外掛或佈景主題發生衝突。關於更完整的資料驗證與清理方法,請參考「WordPress 安全性完整教學」。

角色與權限系統(Roles & Capabilities)

WordPress 的存取控制採用「角色(Roles)」和「權限(Capabilities)」的兩層架構。每個角色是一組權限的集合,而每位使用者可以被指派一個角色。角色和權限資料儲存在 wp_options 資料表的 wp_user_roles 選項中,而使用者所屬的角色則儲存在 wp_usermeta 資料表的 wp_capabilities 欄位。

WordPress 內建角色一覽

WordPress 預設提供以下六種角色,從最高權限到最低權限排列:

角色 識別名稱 說明 主要權限
超級管理員 super_admin 僅存在於多站網路(Multisite),可管理整個網路 所有權限 + 網路管理
管理員 administrator 單站的最高權限角色 所有單站權限
編輯 editor 可管理所有文章與頁面的內容 publish_pages, edit_others_posts, manage_categories 等
作者 author 可發佈與管理自己的文章 publish_posts, edit_published_posts, upload_files 等
投稿者 contributor 可撰寫文章,但不能發佈 edit_posts, read 等(不含 upload_files)
訂閱者 subscriber 只能管理自己的個人資料 read

每個角色擁有一組預定義的權限。WordPress 的權限大致分為以下幾類:

  • 文章相關:edit_postspublish_postsedit_others_postsdelete_posts
  • 頁面相關:edit_pagespublish_pagesedit_others_pagesdelete_pages
  • 使用者相關:list_userscreate_usersedit_usersdelete_userspromote_users
  • 外掛與佈景主題:activate_pluginsinstall_pluginsswitch_themesedit_themes
  • 系統管理:manage_optionsexportimportupdate_core

使用 current_user_can() 檢查權限

current_user_can() 是外掛開發中最常使用的權限檢查函式,它可以判斷目前登入的使用者是否擁有指定的權限:

/**
 * 檢查目前使用者是否擁有特定權限
 *
 * @param string $capability 權限名稱
 * @param mixed  ...$args    額外參數(用於物件層級的權限檢查)
 * @return bool 使用者是否擁有該權限
 */
current_user_can( $capability, ...$args );

// 範例一:基礎權限檢查
if ( current_user_can( 'manage_options' ) ) {
    // 只有管理員可以看到這段內容
    echo '<p>網站管理設定連結</p>';
}

// 範例二:在後台選單中使用權限控制
add_menu_page(
    '外掛設定',
    '我的外掛',
    'manage_options',  // 只有擁有此權限的使用者才能看到選單
    'myplugin-settings',
    'myplugin_settings_page'
);

// 範例三:在 AJAX 處理中檢查權限
function myplugin_ajax_delete_item() {
    // 檢查使用者是否有刪除權限
    if ( ! current_user_can( 'delete_posts' ) ) {
        wp_send_json_error( '權限不足', 403 );
    }

    // 驗證 nonce
    check_ajax_referer( 'myplugin_nonce', 'security' );

    // 執行刪除操作...
    wp_send_json_success( '刪除成功' );
}
add_action( 'wp_ajax_myplugin_delete_item', 'myplugin_ajax_delete_item' );

重要原則:在外掛開發中,應該以「權限(Capability)」而非「角色(Role)」來做存取控制。例如,使用 current_user_can( 'manage_options' ) 而非 current_user_can( 'administrator' )。雖然後者也能運作,但前者更靈活——因為管理員可以將特定權限授予其他角色,而以角色做判斷就無法適應這種彈性配置。

user_can() 與 Meta Capabilities

除了 current_user_can(),WordPress 還提供 user_can() 來檢查指定使用者的權限:

// 檢查特定使用者(而非目前使用者)的權限
$can_edit = user_can( $user_id, 'edit_posts' );

// Meta Capabilities - 物件層級的權限檢查
// 檢查目前使用者是否能編輯「這篇」特定文章
if ( current_user_can( 'edit_post', $post_id ) ) {
    // 注意:'edit_post'(單數)是 meta capability
    // WordPress 會自動將它映射到 'edit_posts' 或 'edit_others_posts'
    // 取決於文章是否為該使用者所撰寫
}

// 檢查目前使用者是否能刪除「這位」特定使用者
if ( current_user_can( 'delete_user', $target_user_id ) ) {
    // 'delete_user' 是 meta capability
    // WordPress 會檢查目標使用者是否為管理員等條件
}

Meta Capabilities(元權限)是 WordPress 權限系統的進階機制。它們不是直接被角色持有的權限,而是 WordPress 在檢查時透過 map_meta_cap 過濾器動態映射到實際的「原始權限(Primitive Capabilities)」。例如 edit_post(編輯某一篇文章)會根據文章擁有者和使用者身份,映射到 edit_postsedit_others_posts

自訂角色與權限

當 WordPress 內建的角色無法滿足專案需求時,外掛可以建立自訂角色或為現有角色新增自訂權限。這在許多商業應用中非常常見——例如 WooCommerce 新增了「商店管理員(Shop Manager)」和「客戶(Customer)」兩個角色。

新增角色 - add_role()

/**
 * 新增自訂角色
 *
 * @param string $role         角色識別名稱(小寫英文與底線)
 * @param string $display_name 角色顯示名稱
 * @param array  $capabilities 該角色擁有的權限陣列
 * @return WP_Role|null 成功回傳 WP_Role 物件,角色已存在則回傳 null
 */
add_role( $role, $display_name, $capabilities );

// 範例:建立「內容審核者」角色
add_role(
    'content_reviewer',
    '內容審核者',
    array(
        'read'                 => true,   // 可閱讀
        'edit_posts'           => true,   // 可編輯文章
        'edit_others_posts'    => true,   // 可編輯他人文章
        'edit_published_posts' => true,   // 可編輯已發佈文章
        'publish_posts'        => true,   // 可發佈文章
        'delete_posts'         => false,  // 不可刪除文章
        'manage_categories'    => false,  // 不可管理分類
    )
);

移除角色 - remove_role()

/**
 * 移除自訂角色
 *
 * @param string $role 角色識別名稱
 */
remove_role( $role );

// 範例:移除先前建立的角色
remove_role( 'content_reviewer' );

注意:移除角色前,請確保沒有使用者正在使用該角色。如果有使用者被指派了即將移除的角色,這些使用者將失去所有權限。建議在移除前先將這些使用者轉移到其他角色。

為現有角色新增或移除權限

除了建立全新角色,你也可以為現有角色新增自訂權限:

// 取得角色物件
$editor_role = get_role( 'editor' );

// 為「編輯」角色新增自訂權限
if ( $editor_role ) {
    $editor_role->add_cap( 'myplugin_manage_settings' );
    $editor_role->add_cap( 'myplugin_view_reports' );
}

// 移除角色的某項權限
if ( $editor_role ) {
    $editor_role->remove_cap( 'myplugin_manage_settings' );
}

關鍵:只在外掛啟用時執行角色與權限操作

這是外掛開發中極為重要的觀念:角色和權限資料儲存在資料庫中wp_options 資料表的 wp_user_roles 選項),而非在 PHP 程式碼中即時產生。因此,add_role()add_cap() 只需要執行一次,就會永久寫入資料庫。如果你把這些操作放在 init Hook 中,每次頁面載入都會觸發資料庫寫入,嚴重影響效能。

正確做法:將角色和權限操作放在外掛啟用鉤子(activation hook)中執行,並在外掛停用時清理:

/**
 * 外掛啟用時建立角色與權限
 */
function myplugin_activate() {
    // 建立自訂角色
    add_role(
        'content_reviewer',
        '內容審核者',
        array(
            'read'                 => true,
            'edit_posts'           => true,
            'edit_others_posts'    => true,
            'edit_published_posts' => true,
            'publish_posts'        => true,
        )
    );

    // 為既有角色新增自訂權限
    $admin_role = get_role( 'administrator' );
    if ( $admin_role ) {
        $admin_role->add_cap( 'myplugin_manage_settings' );
        $admin_role->add_cap( 'myplugin_view_reports' );
        $admin_role->add_cap( 'myplugin_manage_reviews' );
    }

    $editor_role = get_role( 'editor' );
    if ( $editor_role ) {
        $editor_role->add_cap( 'myplugin_view_reports' );
        $editor_role->add_cap( 'myplugin_manage_reviews' );
    }
}
register_activation_hook( __FILE__, 'myplugin_activate' );

/**
 * 外掛停用時清理角色與權限
 */
function myplugin_deactivate() {
    // 移除自訂角色
    remove_role( 'content_reviewer' );

    // 移除為既有角色新增的自訂權限
    $admin_role = get_role( 'administrator' );
    if ( $admin_role ) {
        $admin_role->remove_cap( 'myplugin_manage_settings' );
        $admin_role->remove_cap( 'myplugin_view_reports' );
        $admin_role->remove_cap( 'myplugin_manage_reviews' );
    }

    $editor_role = get_role( 'editor' );
    if ( $editor_role ) {
        $editor_role->remove_cap( 'myplugin_view_reports' );
        $editor_role->remove_cap( 'myplugin_manage_reviews' );
    }
}
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );

自訂權限的實際應用

以下是一個完整的範例,展示如何在外掛中使用自訂權限來控制功能的存取:

/**
 * 外掛設定頁面 - 使用自訂權限控制存取
 */
function myplugin_add_admin_menu() {
    // 主選單 - 需要 myplugin_manage_settings 權限
    add_menu_page(
        '外掛設定',
        '我的外掛',
        'myplugin_manage_settings',
        'myplugin-settings',
        'myplugin_settings_page',
        'dashicons-admin-generic',
        30
    );

    // 報告子選單 - 需要 myplugin_view_reports 權限
    add_submenu_page(
        'myplugin-settings',
        '報告',
        '報告',
        'myplugin_view_reports',
        'myplugin-reports',
        'myplugin_reports_page'
    );

    // 審核子選單 - 需要 myplugin_manage_reviews 權限
    add_submenu_page(
        'myplugin-settings',
        '內容審核',
        '內容審核',
        'myplugin_manage_reviews',
        'myplugin-reviews',
        'myplugin_reviews_page'
    );
}
add_action( 'admin_menu', 'myplugin_add_admin_menu' );

/**
 * 在前台也使用自訂權限控制內容顯示
 */
function myplugin_restricted_content( $content ) {
    if ( is_singular( 'premium_content' ) ) {
        if ( ! current_user_can( 'myplugin_view_premium' ) ) {
            return '<p>此內容僅限授權使用者閱覽。</p>';
        }
    }
    return $content;
}
add_filter( 'the_content', 'myplugin_restricted_content' );

處理外掛更新時的角色變更

當外掛更新需要新增權限或修改角色時,由於啟用鉤子不會在外掛更新時觸發,你需要一個版本檢查機制:

/**
 * 版本檢查機制 - 處理外掛更新時的角色與權限變更
 */
function myplugin_check_version() {
    $current_version = '2.0.0';
    $installed_version = get_option( 'myplugin_version', '0.0.0' );

    if ( version_compare( $installed_version, $current_version, '<' ) ) {
        // 執行升級邏輯
        myplugin_upgrade_roles( $installed_version );
        update_option( 'myplugin_version', $current_version );
    }
}
add_action( 'admin_init', 'myplugin_check_version' );

/**
 * 根據版本差異執行對應的角色更新
 */
function myplugin_upgrade_roles( $from_version ) {
    // 1.0.0 → 2.0.0:新增「報告」權限
    if ( version_compare( $from_version, '2.0.0', '<' ) ) {
        $admin_role = get_role( 'administrator' );
        if ( $admin_role ) {
            $admin_role->add_cap( 'myplugin_view_reports' );
        }
    }
}

這種版本檢查的寫法確保了每次升級操作只執行一次。由於放在 admin_init 中,只有在後台管理頁面載入時才會觸發版本比對,而不會影響前台效能。

實務建議

作者實務經驗分享:

1. 永遠以權限而非角色做判斷:這是 WordPress 官方文件反覆強調的原則。使用 current_user_can( 'manage_options' ) 而非 current_user_can( 'administrator' )。以角色做判斷會讓你的外掛失去彈性——例如在多站網路中,manage_options 權限的分配可能與單站不同。以權限做判斷才能讓網站管理員靈活地透過角色管理外掛配置。

2. 自訂權限命名一律加前綴:和 Post Meta、User Meta 一樣,自訂權限的命名也應該加上外掛前綴,例如 myplugin_manage_settings。這不僅避免命名衝突,也讓其他開發者一眼就能看出這個權限來自哪個外掛。

3. 角色操作務必放在啟用鉤子:我在 code review 中最常看到的錯誤之一,就是開發者把 add_role()add_cap() 放在 init Hook 中。這會造成每次頁面載入都寫入資料庫——角色和權限資料儲存在 wp_options 中,每次呼叫都會觸發 UPDATE 查詢。正確做法是只在外掛啟用(register_activation_hook)時執行一次。

4. 善用 Nonce 搭配權限檢查:權限檢查和 Nonce 驗證是兩道不同的防線。權限檢查確保使用者「有權」執行操作,Nonce 驗證確保請求「來自」正確的頁面。兩者缺一不可,詳細的安全機制說明請參考「WordPress 安全性完整教學」。

5. 測試不同角色的體驗:開發外掛時,別只用管理員帳號測試。建議建立幾個不同角色的測試帳號(編輯、作者、投稿者、訂閱者),實際登入這些帳號確認外掛在不同權限下的表現是否符合預期。特別注意錯誤訊息是否友善、權限不足時是否有適當的提示而非直接顯示空白頁面。

6. 搭配 Hooks 建立完整的權限架構:角色和權限的設定只是第一步。在實務中,你通常還需要搭配 admin_menu 控制選單可見性、用 map_meta_cap 過濾器自訂 meta capability 的映射邏輯、以及透過 user_has_cap 過濾器動態調整使用者權限。如果你對 WordPress Hook 機制還不熟悉,建議先閱讀「WordPress Hooks 完整教學」建立基礎觀念。


本文是「WordPress 外掛開發完整指南」系列的第 10 篇。

上一篇:[WordPress] 自訂內容類型(CPT)與分類法開發教學

下一篇:[WordPress] HTTP API 教學 - 外掛串接外部資源的正確方式


Share:

作者: Chun

WordPress 社群貢獻者、開源社群推廣者。專注於 WordPress 外掛開發、網站效能最佳化、伺服器管理,以及 iDempiere 開源 ERP 導入與客製開發。曾參與 WordCamp Taipei 等社群活動,GitHub Arctic Code Vault Contributor。提供資訊顧問、WordPress 開發教學、主機最佳化與企業 ERP 整合服務。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


文章
Filter
Apply Filters
Mastodon