删除用户信息,一般是采用Ajax发送delete请求来实现,但是这里主要是熟悉thinkPHP6.0的相关功能,因此不考虑API的封装。但是由于省去了Ajax封装,通常表单默认支持的是get请求和post请求两种方式。因此这里介绍一种新的方式,伪造delete(删除)和put(修改)请求。拿delete请求举例,其写入如下:
<input type="hidden" name="_method"value="delete"/>
解释:其实仔细一想,原来还是很好理解的,浏览器你去要知道是哪一种请求,肯定需要知道发送的参数值。而这个参数便是method,这里是使用php能够识别的"_method"变量并传递delete值来实现的。其次,去要注意的是thinkPHP中对于数据的删除和修改资源路由都是通过url/:id实现的,传递的id便是数据在数据库中的额唯一标志id。
回到之前的页面,重写button的删除代码如下:
<td class="text-center">
<form action="{:url('/user/'.$obj.id)}" method="post">
<!--伪造请求表单-->
<input type="hidden" name="_method"value="delete"/>
<button class="btn btn-danger btn-sm btn-delete">删除</button>
<a class="btn btn-warning btn-sm" href="{:url('/user/'.($obj.id).'/edit')}">修改</a>
</form>
</td>
解释:这样,根据表单提交原理,当表单中没有出现type=submit的表单组件时,button会代替并具有submit功能。至于修改部分,本篇文章后半部分再来详细说明。
当我们进行一次数据设置之后,出现如下报错提示。这应该是thinkPHP框架内部做的检测,毕竟对于数据的删除还有连接据库等相关操作:
在controller/User.php文件中写入如下方法:
public function delete($id){
return UserModel::destroy($id) ? view('../view/public/toast.html',[
'infos' => ['恭喜,删除成功!'],
'url_text' => '去首页',
'url_path' => url('/user')
]) : '删除失败';
}
解释:重用toast提示模块,使用继承了think\model后的UserModel中的destroy方法,利用$id这个数据唯一标识实现数据的删除,这样数据删除就完成了。
数据删除还是比较简单的,修改数据相对要复杂一点。首先描述一下模拟删除的需求:用户点击修改数据之后,会跳转到一个edit.html页面展示用户信息,并设置密码可修改,用户名和邮箱无法修改,修改信息同样要有验证。
接下来便是具体的实现了,首先由于需要展示未修改前的用户数据。故而接上面的伪造表单做url路由重定向,跳转到用户数据展示与编辑页面edit.html.在controller/User.php中增加如下方法:
public function edit($id){
return view('edit',[
'obj' => UserModel::find($id)
]);
}
解释:edit方法用户将页面重定向到edit.html,方法中传递的$id实际上是用户数据字段唯一标识。可通过这个标识配上查询方法find找到用户具体数据,obj实际就是将用户原始数据传递到edit.html页面中。
在edit.html页面中添加如下表单,为了方便验证其实就是拿着create.html作略微修改得到的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>新增用户页面</title>
<!--引入css文件-->
<link rel="stylesheet" href="__CSS__/bootstrap.css.map"/>
<link rel="stylesheet" href="__CSS__/bootstrap.css"/>
<link rel="stylesheet" href="__CSS__/style.css"/>
<!--引入js文件-->
<script src="__JS__/jquery-3.5.1.min.js"></script>
<script src="__JS__/bootstrap.js"></script>
<style>
.create-form{
margin-top: 100px;
}
</style>
</head>
<body>
<form class="form-horizontal create-form" role="form" action="{:url('/user/'.$obj.id)}" method="post">
<div class="form-group">
<label for="firstname" class="col-sm-2 control-label">用户昵称:</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="firstname" disabled name="username" placeholder="请输入用户昵称" value={$obj.username}>
</div>
</div>
<div class="form-group">
<label for="email" class="col-sm-2 control-label" >用户邮箱:</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="email" disabled name="email" value={$obj.email} placeholder="请输入用户邮箱">
</div>
</div>
<div class="form-group">
<label for="newpassword" class="col-sm-2 control-label">修改密码:</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="newpassword" name="newpassword" placeholder="请输入用户密码">
</div>
</div>
<div class="form-group">
<label for="newpasswordnot" class="col-sm-2 control-label">重复密码:</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="newpasswordnot" name="newpasswordnot" placeholder="请输入重复密码">
</div>
</div>
<div class="form-group">
<label for="email" class="col-sm-2 control-label">用户状态:</label>
<div class="col-sm-10">
<select name="status" class="form-control">
<option class="disabled" value="">请选择用户状态</option>
<option value="0" {$obj.status =='待审核' ? 'selected' : ''} >待审核</option>
<option value="1" {$obj.status =='正常' ? 'selected' : ''}>正常</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="hidden" name="__token__" value="{:token()}" />
<input type="hidden" name="_method" value="put"/>
<button type="submit" class="btn btn-success">提交修改</button>
</div>
</div>
</form>
</body>
</html>
得到如下页面:
通过控制台查看发现是static文件中的bootstrap资源没有正确引入:
之前的资源引入为:
'tpl_replace_string' => [
'__JS__' => '../../static/js',
'__CSS__' => '../../static/css',
]
这里使用的是相对路径,网站的入口文件目录为public,也就是说localhost/index.php/user写入之后要想将bootstrap资源引入,路径上需要用到两次‘…/’转到localhost(/public)目录下,之后在使用/static/js就能引入了。故而跳转edit.html,由于路径定向使用的相对路径,所以由于传递了依次id,被默认视为一级目录导致路径出错,由于是两种不同的路径,因此只能考虑使用绝对路径,修改为如下代码:
'tpl_replace_string' => [
'__JS__' => request()->domain().'/static/js',
'__CSS__' => request()->domain().'/static/css',
]
解释:利用函数request()->domain()
可以获取网站域名(也就是入口文件目录),这样就不必使用相对路径了。(补充说明一点:由于thinPHP6.0提供缓存,如果没有效果可以删除runtime/temp目录下的文件并刷新重试),设置完成后的页面展示如下:
展示部分就完成了,接下来就是数据修改部分的php实现了。
cookie
,当用户使用浏览器访问一个支持Cookie的网站的时候,用户会提供包括用户名在内的个人信息并且提交至服务器;接着,服务器在向客户端回传相应的超文本的同时也会发回这些个人信息,当然这些信息并不是存放在HTTP响应体(Response Body)中的,而是存放于HTTP响应头(Response Header)。
当客户端浏览器接收到来自服务器的响应之后,浏览器会将这些信息存放在一个统一的位置,对于Windows操作系统而言,我们可以从: [系统盘]:\Documents and Settings[用户名]\Cookies目录中找到存储的Cookie(该文件属于受保护的文件,如果想要查看需要在查看的选项中去掉 ‘隐藏受保护文件’ 提高访问权限,不过不安全修改了权限记得改回来,浏览器其实也是可以查看cookie的);自此,客户端再向服务器发送请求的时候,都会把相应的Cookie再次发回至服务器。而这次,Cookie信息则存放在HTTP请求头(Request Header)了。
cookie特点
:cookie不可跨域名,不同的网站服务器向客户端颁发的cookie不一样。cookie保存中文使用Unicode编码,内存中占用4个字符。其次,cookie名称被创建后不可更改。由于每次用户访问网站会携带网站的cookie信息,因此cookie信息需要做到小而精。
有了Cookie这样的技术实现,服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从而动态生成与该客户端相对应的内容。通常,我们可以从很多网站的登录界面中看到“请记住我”这样的选项,如果你勾选了它之后再登录,那么在下一次访问该网站的时候就不需要进行重复而繁琐的登录动作了,而这个功能就是通过Cookie实现的。
session
是一次浏览器和服务器的交互的会话。
session会话的来历
:浏览器打开网页使用的是http协议,而相邻的两个http协议之间没有联系。但是有的时候,在同一网站下面的两个不同页面需要登录用户才能访问,这个时候两次要求用户两次输入密码显得不太合理。其初解决上述问题是采用cookie来解决的,但是cookie存储数据少、不安全等导致了session的诞生,由于session的存储量更大,可以用于保存页面的退出前状态,提高用户友好性。
session的工作原理
:如上图所示,当浏览器第一次访问服务器时,服务器创建一session对象(该对象有一个唯一的id,一般称之为sessionId),服务器会将sessionId以cookie的方式发送给浏览器。当浏览器再次访问服务器时,会将sessionId发送过来,服务器依据sessionId就可以找到对应的session对象。
HttpSession s = request.getSession(boolean flag);
getSession函数的参数中,flag为true表示的是,先查看请求当中是否有sessionId,如果没有,则创建一个session对象。如果有,则依据sessionId 查找对应的session对象,如果找到了,则返回该对象,如果找不到,创建一个新的session对象。为false的话,只有当请求中存在sessionid才查找并返回session对象否则直接返回null。
session的特点
:存储容量大,由于存在在服务器端,用于传递在客户端与服务器端的信息量被增大,加大了服务器的压力。
说到底,session终究还是cookie的一种优化,当浏览器禁用cookie时,二者都将失效,因此便诞生了当前比较流行、更为灵活的token令牌机制。但是token并不只是验证这一个作用,更胜一筹的是token优化了表单提交处理。
token的工作原理
:和session有点相似,用户第一次登录网站提交用户名和密码,如果通过验证由服务器利用(用户唯一标识+网站设计者的秘钥),结合HMAC-SHA256 算法进行加密,这样能够更加安全,只要秘钥不丢失token伪造就几乎不可能,这样就能满足安全性和唯一性两大要求了。当然,至于token的加密和存储,很多博主也提到了对称加密,token存在解密过程,token直接使用mac地址(电脑唯一标识之一)或者sessionId(sessionId与token并不冲突)等,token存储在cookie或者Local Storage中等。不管是哪一种方法,在整体上表达的意思是:token诞生之后,服务器不在无脑存储session以及sessionId,存储的是一种验证算法,这种算法如果是堆成加密,那么token解密后得到的数据可以进行查询,如果是不可逆加密,两次token可用于鉴权(鉴权:是否登录或者说是使用网站api的权限)。
接下来,我们回到thinkPHP修改用户数据上。一般我们会使用“返回修改”按钮回到edit.html页面,但是我们也可能会使用浏览器自带的返回,当我们使用浏览器自带按钮返回并提交表单时,实际数据是无效的,原因如下:
在表单提交之后会生成一个token,该token存放在服务端的session或者cache中,并且后端会将该数据发一份给前端。在form表单中,页面使用一个隐藏表单域获取后端传过来的 token值,下一次便将该token再次传回到服务端,如果第二次生成的token与隐藏表单传值一致说明重复了,证明这是一次重复提交,将会像用户返回提示并终止后面的验证。
实现起来也比较简单,在Validate/User.php中增加rule规则如下:
'__token__' => 'require|token'
添加相应的规则并写入隐藏表单之后出现如下报错:
解决方法,在app/middleware.php文件中写入如下代码,开启session:
'think\middleware\SessionInit'
根据thinkPHP函数报错提示在controller/User.php中写入如下函数并打印表单提交参数:
public function read(){
}
public function update(Request $request,$id){
// return '修改:'.$id;
dd($request->param());
}
得到如下效果:
接下来就是密码验证了,下面给出场景验证的概念,这里肯定是想要重用Validate模块,但是Validate中本来有username,并设置了require,我们需要设置部分验证(相对于batch全部验证而言),在app/Validate/User.php写入如下代码:
protected $scene=[
'insert' =>['username','email','password','passwordnot','agree','__token__'],
'edit' =>['__token__','newpasswordnot']
];
解释:挺好理解的,就是将上面写好的规则组成一个小的数组集合,形成一套验证方案,之后在Validate中直接调用选择好的验证方案名字就行。
修改后的update方法如下(注意场景之后还需要写batch(true),表示验证场景内部所有规则):
$data=$request->param();
try{
validate(UserValidate::class)->scene('edit')->batch(true)->check($data);
}catch(ValidateException $exception){
return view('../view/public/toast.html',[
'infos' => $exception->getError(),
'url_text' => '返回列表',
'url_path' => url('/user')
]);
}
$id=UserModel::update($data)->getData('id');
return $id ? view('../view/public/toast.html',[
'infos' => ['恭喜你,修改成功!'],
'url_text' => '返回首页',
'url_path' => url('/user')
]): '注册失败';
1.理解cookie与session机制
2.session的到底是做什么的?
3.什么是Session?
4.Cookie和token的区别
5.token防止表单重复提交,token 原理
6.thinkphp6.0没有开启session