首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >Codeigniter 3注册登录系统

Codeigniter 3注册登录系统
EN

Code Review用户
提问于 2019-08-25 20:45:06
回答 2查看 2.8K关注 0票数 2

我正在用Codeigniter 3.1.8Bootstrap 4编写一个基本的博客应用程序。

应用程序允许注册和登录。我对我所设立的注册制度的保安水平表示关注。

登记册主计长:

代码语言:javascript
运行
复制
class Register extends CI_Controller {
    public function __construct()
    {
        parent::__construct();
    }

    public function index() {
        $data = $this->Static_model->get_static_data();
        $data['pages'] = $this->Pages_model->get_pages();
        $data['tagline'] = 'Want to write for ' . $data['site_title'] . '? Create an account.';
        $data['categories'] = $this->Categories_model->get_categories();

        $this->form_validation->set_rules('first_name', 'First name', 'required');
        $this->form_validation->set_rules('last_name', 'Last name', 'required');
        $this->form_validation->set_rules('email', 'Email', 'required|trim|valid_email');
        $this->form_validation->set_rules('password', 'Password', 'required|min_length[6]');
        $this->form_validation->set_rules('cpassword', 'Confirm password', 'required|matches[password]');
        $this->form_validation->set_rules('terms', 'Terms and Conditions', 'required', array('required' => 'You have to accept the Terms and Conditions'));
        $this->form_validation->set_error_delimiters('', '');

        // If validation fails
        if ($this->form_validation->run() === FALSE) {
            $this->load->view('partials/header', $data);
            $this->load->view('auth/register');
            $this->load->view('partials/footer');
        } else {
            // If the provided email does not already
            // exist in the authors table, register user
            if (!$this->Usermodel->email_exists()) {
                // Encrypt the password
                $enc_password = md5($this->input->post('password'));

                // Give the first author admin privileges
                if ($this->Usermodel->get_num_rows() < 1) {
                    $active = 1;
                    $is_admin = 1;
                } else {
                    $active = 0;
                    $is_admin = 0;
                }

                // Register user
                $this->Usermodel->register_user($enc_password, $active, $is_admin);

                if ($this->Usermodel->get_num_rows() == 1) {
                    $this->session->set_flashdata('user_registered', "You are now registered as an admin. You can sign in");
                } else {
                    $this->session->set_flashdata('user_registered', "You are now registered. Your account needs the admin's aproval before you can sign in.");
                }
                redirect('login');
            } else {
                // The user is already registered
                $this->session->set_flashdata('already_registered', "The email you provided already exists in our database. Please login.");
                redirect('login');
            }
        }
    }
}

用户模型模型:

代码语言:javascript
运行
复制
class Usermodel extends CI_Model {

    public function email_exists() {    
        $query = $this->db->get_where('authors', ['email' => $this->input->post('email')]);
        return $query->num_rows() > 0;
    }

    public function get_num_rows() {
        $query = $this->db->get('authors');
        return $query->num_rows(); 
    }

    public function getAuthors(){
        $query = $this->db->get('authors');
        return $query->result();
    }

    public function deleteAuthor($id) {
        return $this->db->delete('authors', array('id' => $id));
    }

    public function activateAuthor($id) {
        $author = null;
        $updateQuery = $this->db->where(['id' => $id, 'is_admin' => 0])->update('authors', array('active' => 1));
        if ($updateQuery !== false) {
        $authorQuery = $this->db->get_where('authors', array('id' => $id));
        $author = $authorQuery->row();
        }
        return $author;
    }

    public function deactivateAuthor($id) {
        $author = null;
        $updateQuery = $this->db->where(['id' => $id, 'is_admin' => 0])->update('authors', array('active' => 0));
        if ($updateQuery !== false) {
            $authorQuery = $this->db->get_where('authors', array('id' => $id));
            $author = $authorQuery->row();
        }
        return $author;
    }

    public function register_user($enc_password, $active, $is_admin) {
        // User data
        $data = [
            'first_name' => $this->input->post('first_name'),
            'last_name' => $this->input->post('last_name'),
            'email' => $this->input->post('email'),
            'password' => $enc_password,
            'register_date' => date('Y-m-d H:i:s'),
            'active' => $active,
            'is_admin' => $is_admin
        ];
        return $this->db->insert('authors', $data);
    }

    public function user_login($email, $password)
    {
        $query = $this->db->get_where('authors', ['email' => $email, 'password' => md5($password)]);
        return $query->row();
    }
}

<#>更新:

我决定从Login控制器中发布login()方法,因为更改Register类将需要相应地更改登录:

代码语言:javascript
运行
复制
public function login() {  
    $this->form_validation->set_rules('email', 'Email', 'required|trim|valid_email');
    $this->form_validation->set_rules('password', 'Password', 'required|trim');
    $this->form_validation->set_error_delimiters('', '');
    if ($this->form_validation->run()) {
      $email = $this->input->post('email');
      $password = $this->input->post('password');
      $this->load->model('Usermodel');
      $current_user = $this->Usermodel->user_login($email, $password);
        // If we find a user
      if ($current_user) {
        // If the user found is active
        if ($current_user->active == 1) {
          $this->session->set_userdata(
           array(
            'user_id' => $current_user->id,
            'user_email' => $current_user->email,
            'user_first_name' => $current_user->first_name,
            'user_is_admin' => $current_user->is_admin,
            'user_active' => $current_user->active,
            'is_logged_in' => TRUE
            )
           );
          // After login, display flash message
          $this->session->set_flashdata('user_signin', 'You have signed in');
          //and redirect to the posts page
          redirect('/dashboard');  
        } else {
          // If the user found is NOT active
          $this->session->set_flashdata("login_failure_activation", "Your account has not been activated yet.");
          redirect('login'); 
        }
      } else {
        // If we do NOT find a user
        $this->session->set_flashdata("login_failure_incorrect", "Incorrect email or password.");
        redirect('login'); 
      }
    }
    else {
      $this->index();
    }
  }

寻求反馈和改进的想法。

EN

回答 2

Code Review用户

回答已采纳

发布于 2019-09-01 04:27:41

有很多事情我会以不同的方式去做,第一件是必须

首先,使用md5()对密码进行编码被普遍认为是一种糟糕的做法。至少从2004年起,如果不是更早的话,它就被认为太弱了。相反,使用密码_哈希函数。

我会在模型中加密密码。事实上,我会将几乎所有的注册逻辑移到模型上。这是我的Usermodel::register_user()版本

代码语言:javascript
运行
复制
public function register_user()
{
    // Get the entire $_POST array with one call then extract what we need
    $posted = $this->input->post();

    $field_list = ['first_name', 'last_name', 'email', 'password'];
    foreach($field_list as $field){
        $data[$field] = $posted[$field];
    }

    $enc_password = password_hash($data['password']);

    // if password_hash fails it's time to bail
    if( ! $enc_password)
    {
        return false;
    }
    // update $data with encrypted password
    $data['password'] = $enc_password;

    // Put additional items in the $data array   
    $data['register_date'] = date('Y-m-d H:i:s');
    // CodeIgniter has a method to count records - use it. Remove get_num_rows() from model
    $data['is_admin'] = $this->db->count_all('authors') === 0 ? 1 : 0;
    $data['active'] = $data['is_admin'];

    if($inserted = $this->db->insert('authors', $data) === TRUE)
    {
        if($data['is_admin'] === 1)
        {
            $msg = "You are now registered as an admin. You can sign in";
        }
        else
        {
            $msg = "You are now registered. Your account needs the admin's approval before you can sign in.";
        }
        $this->session->set_flashdata('user_registered', $msg);
    }
    return $inserted;
}

通过这个新方法和控制器中的一些重新排列,我们可以使代码更简洁一些。

代码语言:javascript
运行
复制
public function index()
{
    $this->form_validation
        ->set_rules('first_name', 'First name', 'required')
        ->set_rules('last_name', 'Last name', 'required')
        ->set_rules('email', 'Email', 'required|trim|valid_email')
        ->set_rules('password', 'Password', 'required|min_length[6]')
        ->set_rules('cpassword', 'Confirm password', 'required|matches[password]')
        ->set_rules('terms', 'Terms and Conditions', 'required',
            array('required' => 'You have to accept the Terms and Conditions'))
        ->set_error_delimiters('', '');

    if($this->form_validation->run())
    {
        // If the provided email isn't on record then register user
        if( ! $this->Usermodel->email_exists())
        {
            // Register user
            if($this->Usermodel->register_user())
            {
                // worked - go to login
                redirect('login');
            }
            // register_user() returned false - set an error message for use in 'auth/register' view
            $data['registration_error_msg'] = "Registration Failed! Please contact Administrator.";
        }
        else
        {
            // The user is already registered
            $this->session->set_flashdata('user_registered',
                "The email you provided already exists in our database. Please login.");
            redirect('login');
        }
    }

    // Validation or registration failed or it's the first load of this page
    $data = $this->Static_model->get_static_data();
    $data['pages'] = $this->Pages_model->get_pages();
    $data['tagline'] = 'Want to write for '.$data['site_title'].'? Create an account.';
    $data['categories'] = $this->Categories_model->get_categories();
    $this->load->view('partials/header', $data);
    $this->load->view('auth/register');
    $this->load->view('partials/footer');
}

使用password_hash()也意味着需要重构登录逻辑,并且必须使用password_verify()函数。password_verify -确认密码与该密码的散列匹配。考虑到这一点,`Usermodel::user_login可能看起来是这样的。

代码语言:javascript
运行
复制
public function user_login()
{
    $email = filter_var($this->input->post('email'), FILTER_SANITIZE_EMAIL);

    if(filter_var($email, FILTER_VALIDATE_EMAIL))
    {
        $user = $this->db
            ->select('password, user_id, active')
            ->get_where('authors', ['email' => $email])
            ->row();

        if(password_verify($this->input->post('password'), $user->password))
        {
            if($user->active)
            {
                return $user->user_id;
            }
            $this->session->set_flashdata("login_failure_activation", "Your account has not been activated yet.");
        }
        else
        {
            $this->session->set_flashdata("login_failure_incorrect", "Incorrect email or password.");
        }
    }
    return false;
}

上面的内容将意味着控制器的login()方法也需要重新工作。

一个不可打破的安全规则是“永不信任用户输入!”

因此,发送到服务器的任何数据都必须经过消毒和验证。即使是某些标识符(如user_id)也是如此,而这些标识符是您在屏幕上放置的URL的一部分。考虑到这一点,您应该始终(最低限度)确保任何具有$id参数的模型函数都得到正确的数据类型。

让我们假设您的user_id类型是整数。例如,激活作者的控制器方法可能在将其发送到模型之前进行这种检查。

代码语言:javascript
运行
复制
public function activate_author($id)
{
    if(filter_var($id, FILTER_VALIDATE_INT)) 
    {
        $this->Usermodel->activateAuthor($id)
        // other code 
    } 
    else 
    {
       //respond in a way that doesn't give a bad-actor too much info
    }

}

虽然CodeIgniter的验证库很好,但它还不够深入。尤其是在消毒的时候。建议认真研究过滤器上的文档。还有很多关于使用filter_var的在线教程。

关于PHP安全性的进一步阅读:

应用程序安全简介

2018年年构建安全软件指南

清单驱动的安全性被认为是有害的

在具有长期持久性的应用程序中实现安全的用户身份验证

票数 4
EN

Code Review用户

发布于 2019-09-02 18:55:47

DFriend的回答很好,但听起来需要更清楚地说明如何使用内置的password_hash函数,还有一些可以改进的体系结构。

电子邮件地址是authors表的主键吗?这不是一个很好的选择,但我们会接受的。

要正确使用密码_哈希,还必须使用密码_验证和密码_需求_重散。这意味着您不能仅仅重新散列密码并在数据库中查找哈希。您必须识别数据库中的用户行,将存储的散列加载到PHP层中,并根据所提供的密码检查它。我还没有测试过下面的内容,但这或多或少是我写它的方式。

代码语言:javascript
运行
复制
class Password_helper {
  const ALGO =  PASSWORD_DEFAULT;

  public static set_password(CI_Controller $CI, string $email, string $p):bool{
    // We could do any password-strength rules or setting of optional arguments here,
    // but I'll assume you're not doing that.
    return $CI->db->update(
      'authors',
      ['password' => password_hash($p, self::ALGO)],
      ['email' => $email]);
  }

  public static check_and_upkeep(CI_Controller $CI, string $email, string $p):bool{
    $_user = $CI->db->get_where(
      'authors',
      ['email' => $email], 
      1)
      ->result();
    if($_user){
      $user = $_user[0];
      if(password_verify($p, $user->password)){
        if(password_needs_rehash($user->password, self::ALGO)){
          self::set_password($CI, $email, $p);
        }
        return true;
      }
      else{
        return false;
      }
    }
    else{
      return false;
    }
  }
}

以上可能与您正在使用的“模型”的概念并不完美;CodeIgniter的“模型”概念坦率地说是有问题的。另外,要使用上面的内容,您需要先将用户插入数据库,然后设置他们的密码。这很好,您可以暂时将他们的密码设置为"“。

另一个安全细节:

使第一作者成为(an?)管理员不是很好。假设有可能有人攻击你,不管怎么说,它会永远把代码库弄乱。根据您所说的"admin“的含义,您甚至不应该将"admins”与“用户”放在同一个表中。无论如何,将整个业务从注册堆栈中删除;您可以使用其他工具手动设置数据库中的第一个管理员。

其他内容:

  • Controller“终结点”函数(例如index())的一个关键和特定角色是解析、清理和验证输入,所有这些都是如此。您和DFriend都在模型层中向下读取POST值;这些值应该作为参数传递下来。如果参数的数量开始感到超凡脱俗,您可以为不同的数据结构(如作者)创建类(“模型”)。
  • 我想您在您的站点中多次重复这三次$this->load->view()调用以及相关的$data构建。所有这些都放在一个地方,或者作为Controller基类的方法,或者作为Loader扩展类的方法。还请注意,您可以从视图内部加载视图,因此一个主模板可能比单独的页眉和页脚“部分”视图更好。
  • 与我前面提到的类似,不要使用$this->Usermodel->get_num_rows() == 1来检查用户是管理员还是活动的;此时您应该有用户的数据,并且能够进行字面上的检查。
  • 我不喜欢闪存数据,我宁愿使用url散列向目标页面上运行的javascript传递范围较窄的消息。但这只是个人偏好。
  • 如果需要,可以合并(de)activateAuthor函数。当调用它们时,您应该已经验证了目标的存在,因此从DB获取一个假值将是抛出错误的理由。
  • 尽管我讨厌CI的“模型”,但如果将它们全部抛到自动加载配置文件中,它们会稍微少一些痛苦;然后您就不需要显式地加载它们了。
  • 如果验证失败,login()端点方法将调用index()端点方法。这是个奇怪的选择。
  • 使用类型化函数签名将使您的代码更容易阅读,并且更有可能以明显的方式中断。好处是它不太可能以阴险的方式突破。I强烈建议这样做。
  • 在DFriend的代码:中
    • 在if语句的条件中为$inserted分配一个值。别干那事。
    • 不要依赖redirectload->view()来停止执行。即使它起作用了,也会使事情更难读懂。使用显式返回语句,或者将备用路径放置在显式else中,或者两者都使用。

编辑:嗨,DFriend!

你提出了几点意见,这听起来像是你希望得到回应。

  • 我喜欢你的观点,简洁是清晰的一部分。另一个清晰的地方是说出你的意思,把它说成代码比留下评论更简洁和更有力。
  • 正如我所理解的,清理、验证和解析输入不是业务逻辑。主计长的一个关键作用是处理输入。如果我们认为框架是理所当然的(就像我们想要的那样),那么我们从概念上将程序输入到Controller端点方法中。该方法需要了解输入信息(POST值、查询字符串、url片段、cookie),因为所有进一步的操作都是从那里进行的。为了保持关注点的分离,我们可以做的事情是确保没有其他人需要读取这些输入源。
  • redirect()停止执行,但您必须知道并记住这一点,才能理解依赖它的代码。CI->load->view()没有停止执行,但几乎总是流中的最后一个调用。在所有这些事件之后抛出一个return;有点冗长,但这是显而易见的。
  • 这是生活中的一个事实,会有很多条件,我真的不认为有一个单一的模式将涵盖所有的情况。也就是说,如果仅在先前的if失败时才运行代码,那么不管您是否这样说,您都在其他部分。
票数 0
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/226795

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档