PHP CodeIgniter
项目个人开发小功能分享--学习心得①
前言
本人碰到一个维护项目,在技术选型上已经不具备可能性,只能在原有的基础上继续开发,和用新技术来重构。重构由于风险太大,因此在这种情况下,继续在原有项目基础上迭代开发成为了不可避免的选择,为什么要选择CI,不选择TP,laravel,也是没得选择啦!
虽然在框架选择上并没有太多的选择余地,但PHP CodeIgniter
框架作为一种服务端渲染框架,具有一些优点,如减少客户端负担、提高网站的安全性和可维护性,以及更易于搜索引擎索引和识别网站内容等。当然,服务端渲染也有一些局限性,例如无法实现实时更新和动态交互等功能,需要使用客户端渲染来实现。
首先我分享下关于完整用户登录功能的完善的两个要素:
① 通信保密性--会话初始化功能
② 身份鉴别-登录失败处理功能
针对用户安全性和使用体验的两点完善措施:
③ 对单个帐户的多重并发会话进行限制
④ 设置超过一定时间未连接自动结束会话
在本文中,我将分享我在实现 PHP CodeIgniter
框架会话初始化功能时的经验和总结,希望能够对其他开发人员有所帮助。同时,也希望能够借此机会向其他开发者学习和交流,共同进步,请大佬们轻拍!
通信保密性--会话初始化功能
在通信双方建立连接之前,应用系统利用密码技术进行会话初始化验证。用户第一次访问网页时,由于没有session的存在,所以服务器会随机分配一个新的sessionid给他,会话初始化验证通常用于确保用户的身份和凭据有效。在会话初始化期间,可以对用户身份进行多重验证,如登录口令、验证码、第三方身份验证等等。这有助于确保用户的身份得到有效的保护,防止账户被盗用、欺诈等恶意行为。相比于仅验证账户密码的方式,会话初始化验证可以提高用户账户的安全性。
该功能是在login.html中输入完用户名密码验证码后,form表单提交三者元素给后端。 在后端对应的controller
类中的Login撰写一个verify
函数,这个函数就是我们所说的接口,该函数的实现主要包括两个部分:
会话初始化和验证密钥。
① 在第一次访问时,会话初始化会生成一个随机的会话密钥,并将其发送到客户端保存。同时,服务端设置会话标志以表示会话已经初始化过了。如果后续的请求没有带着该密钥,就无法通过后续的验证。
② 对于每个请求,服务端会从POST
请求中获取客户端传来的密钥,并与服务端保存的密钥进行比较。如果两者一致,则认为验证通过,否则会话会被终止。
接下来让我们一步一步实现这个重要的verify函数: ①我们因为需要发两次请求到verify函数,因此需要一个标识来判断是否是初次进入,我们通过session机制存储一个标识is_session_initiated来进行判断
$this->session->set_userdata('is_session_initiated', true);
②那么就需要用一个if判断来分割首次和第二次的请求,将首次请求包裹在if (!$this->session->userdata('is_session_initiated'))中,紧接着就得返回我们随机生成的session_key(随机生成的具有安全性的十六进制数)
if (!$this->session->userdata('is_session_initiated'))
{
$session_key = bin2hex(random_bytes(16));
$response = array('session_key' => $session_key,'msg'=>true);
echo json_encode($response);
return;
}
③因为这是首次请求,在此之前都没有存储is_session_initiated标识,所有得在后面加点代码,不然每次请求都是进入这个判断里,顺便将生成session_key也保存到session里。
// 会话初始化
//检测是否是第一次请求,产生一个 session_key给前端,以后访问,此用户都带着这个key
if (!$this->session->userdata('is_session_initiated')){
$session_key = bin2hex(random_bytes(16));
$response = array('session_key' => $session_key,'msg'=>true);
header('Content-Type: application/json');
echo json_encode($response);
$this->session->set_userdata('session_key', $session_key);
$this->session->set_userdata('is_session_initiated', true);
return;
}
④后面就是第二次请求verify的时候,要验证session_key和用户名,密码了。我们先将这从前端传来的三个参数获取到先.
$username = $this->input->post('username'); // Get form data
$password = $this->input->post('password');
$received_key = $this->input->post('received_key');
⑤随后从session的userdata中取出先前这个用户专属的session_key
$session_key = $this->session->userdata('session_key');
⑥随后很容易了,就是做比对,received_key是前端传来的,两者一比对,正确的就进行下一步,错误了就抛出错误
if ($received_key != $session_key)
{
// 密钥比对不一致,传错误到前端
error('Session key validation failed.');
return;
}
那么简易的verify函数就实现了,以下是完整的代码:
public function verify(){
if($this->input->post('init'))
{
// 会话初始化
if (!$this->session->userdata('is_session_initiated')) {//检测是否是第一次请求,产生一个session_key给前端,以后访问,此用户都带着这个key
// Generate session key
$session_key = bin2hex(random_bytes(16));
// Send session key to client (e.g. via HTTPS)
$response = array('session_key' => $session_key,'msg'=>true);
header('Content-Type: application/json');
echo json_encode($response);
// Set flag in session
$this->session->set_userdata('session_key', $session_key);
$this->session->set_userdata('is_session_initiated', true);
return;
}
// 验证session key
$username = $this->input->post('username'); // Get form data
$password = $this->input->post('password');
$received_key = $this->input->post('received_key');
$session_key = $this->session->userdata('session_key');
if ($received_key != $session_key) {
// 密钥比对不一致,传错误到前端
error('Session key validation failed.');
return;
}
}
}
如果不写 verify 函数,那么在后端就没有进行会话初始化和密钥验证的功能。这将导致以下后果: 会话安全性降低:没有进行会话初始化,就不能保证会话的安全性。未经初始化的会话可能容易受到各种攻击,如会话劫持、会话固化等。 数据泄露:没有进行密钥验证,攻击者可以使用任意密钥进行请求,这可能导致敏感数据泄露,如用户信息、密码等。无法追踪用户:未初始化的会话无法被跟踪,无法知道哪些请求是由哪个用户发出的。 网站不可用性:如果网站没有进行会话初始化,那么可能会出现一些问题,如用户无法登录、无法进行某些操作等,这可能会导致网站不可用。 综上所述,如果不进行会话初始化和密钥验证,那么会话的安全性无法得到保障,同时可能会导致用户数据泄露和网站不可用等问题。因此,编写 verify 函数是确保会话安全性的重要一步
接下来,我们需要在前端的Login.html中添加JavaScript
代码,使用JQuery
库和Ajax
方法来向服务端发送请求。
<form action="<?php echo site_url('login/verify')?>" onsubmit="onSub(event)" method="post">
<button class="btn btn-large btn-primary" id="form" type="submit">登录</button>
</form>
function onSub(event) {
var password = window.btoa(document.getElementById('password').value)
document.getElementById('password').value = password
}
var submit = document.getElementById('form')
submit.onclick = function firstReq() {
// Get username and password
var username = document.getElementById('username').value
var password = document.getElementById('password').value
// var received_key = document.getElementById('received_key').value;
// Send request to server to initiate session and get session key
$.ajax({
url: '<?php echo site_url(\'Login/verify\'); ?>',
type: 'POST',
dataType: 'json',
data: { received_key: null, init: true },
success: function (response) {
// Use session key to encrypt password
if (response.msg = 'true') {
var session_key = response.session_key
var encrypted_password = CryptoJS.AES.encrypt(password, session_key).toString()
console.log(session_key)
// Send request to server to verify login
SecondReq(session_key)
}
},
error: function () {
// Error occurred, display error message
var error_msg = document.getElementById('error-msg')
error_msg.innerHTML = 'An error occurred while initiating session.'
error_msg.style.display = 'block'
},
// 其中第一个 AJAX 请求发送到 /login/verify 以初始化会话并获取会话密钥,
// 第二个 AJAX 请求也发送到 /login/verify,以使用会话密钥对加密密码进行验证。
// 如果登录验证成功,将重定向到主页,否则将显示错误消息
})
}
function SecondReq(session_key) {
// var received_key = document.getElementById('received_key').value;
console.log(session_key)
received_key = session_key
console.log(session_key, '已传')
$.ajax({
url: '<?php echo site_url(\'Login/verify\'); ?>',
type: 'POST',
dataType: 'json',
data: { received_key: session_key, init: true },
success: function (response) {
console.log(response, '接收成功')
$('form').unbind('submit').submit()
},
error: function () {
console.log('第二个error')
// Error occurred, display error message
var error_msg = document.getElementById('error-msg')
error_msg.innerHTML = 'An error occurred while verifying login.'
error_msg.style.display = 'block'
}
})
}
login表单绑定的是onSubmit事件,事件提交后会首先通过post请求发送FormData给后端写好的verify函数中进行判断。 那么起初我的思路是发送两个嵌套的ajax请求,第一次是获取初始会话密钥,赋值received_key = session_key之后,作为附加参数跟账号密码,验证码一起发送第二次请求来进行验证。
第一次遇到问题是ajax中的data参数传到后端是空的,我百思不得其解,打印之后发现是和Form表单默认提交行为冲突了。第一次解决尝试是在提交一开始给一个关闭默认行为,event.preventDefault();发送完两次请求再在success中开启提交,发现这是不行的,在控制台中发送在循环提交请求,差点死机。
那么我第二次解决方案是在表单中写入一个新的按钮,表单默认提交是通过type为submit的按钮来的,我可以用新的button来控制这个默认提交行为。
<form action="<?php echo site_url('login/verify')?>" onsubmit="onSub(event)" method="post">
<button class="btn btn-large btn-primary" id="submit">登录</button>
<button class="btn btn-large btn-primary" id="form" type="submit">登录</button>
</form>
那么在js中也改一下
var submit = document.getElementById('submit');
submit.onclick=...
这样子就可以实现先让ajax两次请求先发送,密钥验证成功后,再开启一次form表单提交,$('form').unbind('submit').submit()
在此这是我的经验和解决方案!这是一个很好的教训,前端和后端之间的交互需要精心协调,以避免冲突和错误。我使用的新按钮来控制默认提交行为的方法算是一个技巧
最后通过了密钥的验证后,就会对用户名,密码,验证码开始验证,我使用的是密码加密技术,在前端中传来的密码是经过base64加密的,得在后端解密。
$password = base64_decode($password);
想要保证安全性,我存入密码到数据库也是经过了md5加盐
// 将密码进行md5加盐加密
function do_hash($psw) {
$salt = 'aFXBxYmk@lsw46y7b8C5qN5!2s'; // 定义一个salt值,最好够长,或者随机
return md5(md5($psw).$salt); // 返回加salt后的散列
}
$password = do_hash($password);
那么在这里分享了这么一个小功能,有需要的朋友很荣幸能帮到你们,如果有什么更好的解决方案欢迎来我这里一起讨论!~ 我也是第一次发表文章,感谢杰佬对我这个小菜鸟的重视给我舞台撰写的机会。PHP虽然已经大势已去,相关资料也非常稀少,但是其轻量便利开发还是值得使用的~
那么很快我也会更新一下第二个觉得挺有意思的功能
身份鉴别-登录失败处理功能
采取结束会话、限制非法登录次数和自动退出等措施
jack
rechard