PHP CodeIgniter项目个人开发小功能分享--学习心得①

皮皮
2 评论
/ /
1144 阅读
/
10315 字
27 2023-02

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

    dadada

    皮皮

    surprised