今天抽离之前使用的 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');