Часто на различных форумах встречается информация с вопросами о том как можно организовать Аутентификацию\Авторизацию пользователей с внешних сервисов на WordPress.
Под внешними сервисами подразумевается некий фронтенд (например VueJS, Angular и т.д) или мобильные приложения. Но так же часто можно встретить ответы лишённые конструктивна и склоняющие к использованию в качестве бэкенда Nodejs, Laravel, Symfony и т.д, но как быть если всё таки в качестве бэкенда хочется использовать именно WordPress, расскажем ниже по тексту.Немного теории:
JSON Web Token (JWT) — это открытый стандарт для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.
Чем удобен JWT сходу, на наш взгляд:
- Не нужно гонять логин и пароль по сети при каждом запросе
- В токен можно добавить полезную нагрузку, например информацию о пользователе
- Фронт и бэк могут находится на разных серверах и отлично взаимодействовать
- Огромное количество библиотек и реализаций под каждый язык программирования
Реализация:
Реализация, в нашем случае, построена на взаимодействии VueJS фронтенда и REST API (wp-json) WordPress в качестве бэкенда. Здесь мы затронем только то что относится к работе WordPress + немного composer библиотек.
Нам понадобится:
- WordPress
- JWT библиотека написанная на PHP — источник https://github.com/lcobucci/jwt и иснтрукция для composer + примеры
Создадим Роут WordPress, который будет отвечать за авторизацию
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
/** * Маршрут авторизации */ register_rest_route($root,'login',[ 'methods'=>'POST', 'callback'=>[$this,'adminLogin'], 'args'=>[ 'login' => [ 'default' => null, 'required' => true, 'validate_callback' => function($param){ if(empty($param)) { return false; } return true; }, 'sanitize_callback' => function($param){ return trim($param); } ], 'password'=>[ 'default' => null, 'required' => true, 'validate_callback' => function($param){ if(empty($param) || strlen($param)<5) { return false; } return true; }, 'sanitize_callback' => function($param){ return trim($param); } ] ] ]); |
Обычный роут который принимает логин и пароль, после обработки вызывается колбэк — adminLogin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
/** * Авторизация пользователя * Создаёт запись в таблице после проверки пользователя * @param \WP_REST_Request $request * @return array */ public function adminLogin(\WP_REST_Request $request):object { $return_params=[]; $response='ok'; $request_param=$request->get_params(); $user=wp_authenticate($request_param['login']??null,$request_param['password']??null); if(is_wp_error($user)){ $response='Не верный логин или пароль'; $user=null; $response = rest_ensure_response( ['success' => true, 'response' => $response, 'params' => $return_params] ); $response->set_status( 401 ); return $response; } //Класс-обёртка над библиотекой jwt $jwt=new AuthJwt(); $return_params['token']=$jwt->create($user->data->ID);//Создание токена и в качестве полезной нагрузке - ИД пользователя $return_params['token_refresh']=$jwt->createRefresh($user->data->ID);//За одно создадим рефреш-токен $return_params['redirect']='/admin';//на будущее //Сохраним в заранее подготовленную таблицу MySQL наши данные $jwt->addTable([ 'user_id'=>intval($user->data->ID), 'auth_token'=>$return_params['token'], 'refresh_token'=> $return_params['token_refresh'], ] ); if($this->debug) {//Расширенный вывод информации $return_params=array_merge($return_params,$request_param,[$user]); } //Подготовка объекта response WordPress $response = rest_ensure_response( ['success' => true, 'response' => $response, 'params' => $return_params] ); $response->set_status( 200 ); //Возвращаем заголовки с токеном и рефреш-токеном клиенту $response->set_headers( [ $this->header_token_key => $return_params['token'], $this->header_token_refresh_key => $return_params['token_refresh'], ] ); return $response; } |
Суть колбэка «adminLogin» — в том что принять логин и пароль, проверить их в базе пользователей WordPress и далее приступить к созданию токена, которым мы будем обмениваться с клиентом (фронтом).
Исходно (по утверждению автора и в версии 3.3) библиотека «cobucci/jwt» не создана для того что бы делать рефреш-токен, по этому процедуру пересоздания токена JWT — мы можем сделать самостоятельно.
В нашем случаем логика с рефрешем следующая:
- При авторизации (ввод логина и пароля) на фронте, сервер в заголовках ответа — возвращает токен и рефреш-токен. Эти две строки мы сохраняем на фронте для будущего использования
- При запросах с фронта мы отправляем токен и рефреш для того что бы бэк убедился что мы это мы. (рефреш можно и пожалуй нужно слать лишь тогда когда время токена истекло, но это здесь не рассматривается)
- Если WordPress обнаружил что токен истёк, то в качестве подтверждения берётся рефреш-токен. Если с рефреш-токеном всё хорошо и ему соответствует запись в таблице, создаётся новый токен и снова эти данные отправляются клиенту как в пункте 1
- Если токен и рефреш-токен не сработали — мы должны ответить клиенту — отказом в валидации и выполнить каки-то действия
Ниже код обёртка над библиотекой jwt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
<?php namespace Coderun\ContentCabinet; use Lcobucci\JWT\Builder; use Lcobucci\JWT\ValidationData; use Lcobucci\JWT\Parser; use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Signer\Hmac\Sha512; /** * Class AuthJwt * Документация https://github.com/lcobucci/jwt/blob/3.3/README.md * @package Coderun\ContentCabinet */ class AuthJwt{ protected $secret_key='megaKey1238'; public function __construct() { } //Создаём токен public function create($id) { $time = time(); $signer = new Sha512(); $token = (new Builder()) ->issuedBy('/') ->permittedFor('/') ->identifiedBy(md5("user_id_{$id}"), true) ->issuedAt($time) ->canOnlyBeUsedAfter($time + 60) ->expiresAt($time + 120) ->withClaim('userId', $id) ->getToken($signer, new Key($this->secret_key)); $token->getHeaders(); $token->getClaims(); return (string)$token; } //Создаем рефреш public function createRefresh($id) { $time = time(); $signer = new Sha512(); $token = (new Builder()) ->issuedBy('/') ->permittedFor('/') ->identifiedBy(md5("user_id_{$id}"), true) ->issuedAt($time) ->canOnlyBeUsedAfter($time + 60) ->expiresAt($time + 3600) ->withClaim('userId', $id) ->getToken($signer, new Key($this->secret_key)); $token->getHeaders(); $token->getClaims(); return (string)$token; } //Проверка токена public function validate(string $token_check):array { $result=[]; $signer = new Sha512(); try { $token = (new Parser())->parse($token_check); $id=$token->getClaim('userId'); $data = new ValidationData(); $data->setIssuer('/'); $data->setAudience('/'); $data->setId(md5("user_id_{$id}")); $data->setCurrentTime(time()+61); $result['userId']=$id; $result['isValid']=$token->verify($signer,$this->secret_key); if($result['isValid']) { $result['isValid']=!$token->isExpired(); } return $result; }catch (\Exception $e) { return $result; } } //Обновление данных в таблице public function addTable(array $field) { global $wpdb; $table_name='coderun_content_cabint_jwt'; $wpdb->delete($table_name,['user_id'=>$field['user_id']],['%d']); $default_field=[ 'user_id'=>0, 'auth_token'=>'', 'refresh_token'=>'', ]; $field=array_merge($default_field,$field); $wpdb->insert($table_name,$field,['%d','%s','%s']); } public function getUserIdToRefreshToken($refresh_token):int { global $wpdb; $table_name='coderun_content_cabint_jwt'; $data=$wpdb->get_row("select user_id from {$table_name} where refresh_token='{$refresh_token}'"); if(empty($data)) { return 0; } return (int)$data->user_id; } public function getUserIdToToken($token):int { global $wpdb; $table_name='coderun_content_cabint_jwt'; $data=$wpdb->get_row("select user_id from {$table_name} where auth_token='{$token}'"); if(empty($data)) { return 0; } return (int)$data->user_id; } } |
В этом классе можно:
- Создать токен
- Создать рефреш-токен
- Проверить токен на валидность
- Добавить запись в таблицу
- Получить ИД пользователя через токен
- Получить ИД пользователя через рефреш-токен
Для аутентификации пользователя на стороне WordPress используем специальный метод (он же используется в роутах). Суть метода проверки в WordPress, в том что бы он вернул булево значение — этакий ключ к разрашению дальнейшего выполнения колбэка этого роута.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public function checkAuth(\WP_REST_Request $request) { $token=$request->get_header($this->header_token_key);//токен из заголовка $token_refresh=$request->get_header($this->header_token_refresh_key);//рефреш-токен из заголовка $jwt=new AuthJwt();//Наша обёртка $is_valid_token=$jwt->validate($token);//Проверка валидности токена if($is_valid_token['isValid']??false) { return true; } if(empty($token_refresh)) { return false; } $jwt=new AuthJwt(); if($jwt->getUserIdToRefreshToken($token_refresh)===0) { return false; } $this->is_refresh=true;//флаг что унжно делать обновление токена return true; } |
Если функция возвращает true — будет выполнен колбэк роута WordPress
Метод пересоздания токена
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/** * Обновляет токен * Заполняет заголовки для клиента * @param \WP_REST_Request $request * @param \WP_REST_Response $response */ protected function responseRefreshToken(\WP_REST_Request $request,\WP_REST_Response &$response):void { $token_refresh=$request->get_header($this->header_token_refresh_key); if(empty($token_refresh)) { return; } $jwt=new AuthJwt(); $user_id=$jwt->getUserIdToRefreshToken($token_refresh);//ИД пользователя по токена-рефреша if(empty($user_id)) { return; } $user=new \WP_User($user_id); if(empty($user)) { return; } $return_params['token']=$jwt->create($user->ID); $return_params['token_refresh']=$jwt->createRefresh($user->ID); $jwt->addTable([ 'user_id'=>intval($user->ID), 'auth_token'=>$return_params['token'], 'refresh_token'=> $return_params['token_refresh'], ] ); $response->set_headers( [ $this->header_token_key => $return_params['token'], $this->header_token_refresh_key => $return_params['token_refresh'], ] ); } |
Метод добавляет в заголовки ответа для клиента новую пару значений токена и рефреш-токена.
Тестовый роут WordPress для проверки работы JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * Маршрут - тестирования */ register_rest_route($root,'test',[ 'methods'=>'POST', 'permission_callback'=>[$this,'checkAuth'],//проверка токенов 'callback'=>[$this,'adminTest'], ]); //Метод класса (обратите внимание!) public function adminTest(\WP_REST_Request $request):object { $response = rest_ensure_response( ['success' => true, 'response' => 'ok', 'params' => []]); if($this->is_refresh) {// пересоздание токена - если истёк и есть валидный рефреш $this->responseRefreshToken($request,$response); } $response->set_status( 200 ); return $response; } |
Пример передачи заголовков для аутентификации. Как видно из скриншота, WordPress ответил нам отказом, так как токен устарел, а рефреш-токен оказался не валиден(либо тоже устарел, либо не верный)
Ниже пример самой авторизации в WordPress через Rest API с последующим действием получения токена в заголовках ответа