今天抽離之前使用的 laravel 版本的 fastadmin 後臺,權限系統,當時沒有寫 '管理員日誌' 這個模塊,今天實現了下,過程中,也發現幾個問題,分享給大家。
可以先看下 fastadmin 源碼,它使用了 tp 的 behavior 功能,在應用結束後,調用了 admin log 鉤子
好久沒看 tp 了,不過還稍微瞭解點 laravel,看代碼機制,應該就是 hook 鉤子之類的,還專門搜索了下 tp 的 behavioir 和 laravel 的 event 區別,可惜沒找到...,不過兩者應該差不多的
tp 有 behavioir 這種機制,而且在 tp 內,內置了一些系統級別的 hook,但是 laravel 好像並沒有啊,這個我也簡單搜了下,好像沒有想要的,記得模型的一些操作好像有內置的 event,created、updated 等,laravel 系統好像沒。
那我們在哪個位置需要記錄日誌呢,fastadmin 是在 app_end - 應用結束,記錄的日誌,laravel 哪裏能判定應用結束,而且得是公共的地方
從 public/index.php,看過 laravel 源碼的,應該知道 laravel 的最簡單的執行機制是:
/*
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
*/
實例化內核(kernel),將請求(request)傳遞給內核,供內核處理,得到響應(response),然後發送響應
看 public/index.php 的幾行代碼,就是這個意思,而且,在得到相應後,調用了 terminate(),這個是所有的 middleware 的 terminate 方法調用的時機。
我們記錄日誌,可以在入口文件的 $reponse 後,表示已經處理了請求,得到了相應,表示其實已經結束了,然後記錄日誌。
但是,覺得不應該修改 laravel 的核心代碼,不利於升級,而且 laravel 提供了更好的解決方法,就是上面說的 terminate():
https://learnku.com/docs/laravel/6.x/middleware/5136#terminable-middleware
Terminable 中間件
它的執行時機就是:
在準備好 HTTP 響應之後,中間件可能需要做一些工作。例如,Laravel 內置的「session」 中間件會在完全準備好響應後將會話數據寫入存儲。如果你在中間件上定義了一個 terminate 方法,並且你使用的是 FastCGI ,那麼它將會在響應準備發送到瀏覽器之後自動調用。
所以,我們就確定了日誌的位置。
所以,定義了一個 AdminLogMiddleware,記錄下過程中的幾個問題:
1>只定義 terminate() 方法,不定義 handle() 方法,報錯:
Function name must be a string
2>terminate() 方法,調試過程中,輸出不了任何內容,這個其實就跟在 public/index.php 中,在
$response->send();
執行後面,也輸出不了任何內容,而 $kernel->terminate($request, $response); 也是在這後面,所以輸出不了內容
3>輸出不了內容,如何調試,在 public/index.php 中調試(這個也是我在寫這邊筆記時,重新看了下 public/index.php 想到的。。。)
我的調試,就是在 AdminLogMiddleware 中間件的 handle() 方法中調試的。
/*
這裏再強調個知識點,文檔中的:
前置 & 後置中間件
前置中間件:
public function handle($request, Closure $next)
{
// 執行內容
return $next($request);
}
後置中間件:
public function handle($request, Closure $next)
{
$response = $next($request);
// 執行內容
return $response;
}
*/
發現後置中間件,也是能獲取到響應的,但這裏的響應,是不是經歷了當前中間件處理後,然後得到的響應?還是所有請求結束後的響應內容?(應該是所有請求結束後的響應內容,因爲中間件,並非執行請求,只是處理請求前的,一道道過濾機制,或其他邏輯處理吧)
還是文檔、源碼、整個機制不清楚。。。有時間我也得再好好啃,先把自己理解的記錄下來
4>後置中間件 和 terminate() 方法,都可以收到 $response,但2者的區別到底是什麼,我還不是很清楚,但是目前看有一點是清楚的:
後置中間件,我們得到 $response,可以輸出,而 terminate() 不行
5>我們可以更優化下日誌處理,將其作爲一個 event 事件,解耦,邏輯還清晰。因爲可能以後還可能定義其他操作
如果那樣的話,我們也不能定義爲 AdminLogMiddleware,但我們目前只想讓部分路由使用,中間件好像更好點。
不過,我們也可以定義一個全局的日誌中間件,在中間件裏,通過路由匹配,來指定哪些路由想要記錄日誌
關於這點發現好像自己都被繞進去了,是不是這麼架構合理...(架構知識很欠缺...)
臨時寫的筆記,有點亂,另外自身實力有限,勿怪,最後,記錄下代碼:
數據庫遷移:
Schema::create('admin_logs', function (Blueprint $table) {
$table->increments('id');
$table->integer('admin_id')->unsigned()->comment('管理員ID');
$table->string('url', 255)->comment('請求地址');
$table->text('params')->comment('請求參數');
$table->string('ip', 255)->comment('IP地址');
$table->string('user_agent', 255)->comment('用戶代理');
$table->string('content', 255)->comment('日誌描述');
$table->timestamps();
$table->index('admin_id');
$table->index('ip');
});
DB::statement("ALTER TABLE `app_versions` comment '後臺管理日誌表'");
中間件 terminate() 方法:
public function terminate($request, $response)
{
$admin = Auth::guard('admin')->user();
// 數據處理
$admin_id = $admin ? $admin->id : 0;
$method = $request->method();
$uri = $request->path();
// 1.過濾 uris(不記錄日誌的 uri)
$guarded_uris = [
'admin/ajax/lang',
];
if(in_array($uri, $guarded_uris)){
return true;
}
// 2.過濾 uris + method(某些 uri 的 get 和 post 一致,也需要過濾)
$guards_get_method_uris = [
'admin/account/login',
];
if(in_array($uri, $guards_get_method_uris)){
return true;
}
// 2.過濾 params(日誌中不記錄密碼等私密信息)
$guarded_params = [
'password',
];
$params = $request->all();
$params = array_filter($params, function ($key) use ($guarded_params) {
return !in_array($key, $guarded_params);
}, ARRAY_FILTER_USE_KEY);
$params = json_encode($params, JSON_UNESCAPED_UNICODE);
$ip = $request->getClientIp();
$user_agent = Agent::getUserAgent();
/*
這裏結合的是我係統裏的,記錄內容會根據權限菜單名來匹配
*/
// 4.得到日誌內容(我們通過 uri 匹配相應的 '權限菜單 identifier',來獲取)
// 1>後臺權限路由
$admin_permission = AdminPermission::where('identifier', substr($uri, 6))->first();
if($admin_permission){
$id_chain = $admin_permission->id_chain;
$names = AdminPermission::whereIn('id', explode('-', $id_chain))->pluck('name');
$content = $names->join('-');
// 2>後臺其他路由
}else{
switch($uri){
case 'admin/account/login':
$content = '登錄';
break;
case 'admin/account/logout':
$content = '退出登錄';
break;
default:
$content = '其他操作';
break;
}
}
$admin_log_data = [
'admin_id' => $admin_id,
'method' => $method,
'uri' => $uri,
'params' => $params,
'ip' => $ip,
'user_agent' => $user_agent,
'content' => $content,
];
AdminLog::create($admin_log_data);
}
最終的記錄結果:
(`id`, `admin_id`, `uri`, `params`, `ip`, `user_agent`, `content`, `created_at`, `updated_at`)
(26,1,'admin/auth/permission/index','{\"sort\":\"sortid\",\"order\":\"desc\",\"offset\":\"0\",\"limit\":\"10\",\"_\":\"1569680908796\"}','127.0.0.1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36','權限管理-菜單管理-列表','2019-09-28 14:40:43','2019-09-28 14:40:43');