Problems with PHP, MySQL based log-in system

2019-05-31 00:01发布

问题:

IMPORTANT

After following the advice in the answers below, the client was able to log-in without any problems but did not attempt to actually navigate the secured pages. When he attempted to do so later, he was returned to log-in as before with the "Please log in" error. After much head scratching, something incredibly simple came to mind - the client was accessing the site with http://www.example.com/admin and everything in the login script was redirecting to http://example.com, so the session cookie that it was looking for was set for another domain. This also explains why he had problems logging in the first time but not subsequent times - the script redirected him to the log-in form without the www.

A quick fix was to write a .htaccess file to remove the www, problem solved. Of course this could also be handled within the login script, which I will improve for future use.

ORIGINAL POST

I develop PHP and MySQL based sites with a home brew CMS and log-in system. My CMS is unique to every client and it has been quite the crowd pleaser - unfortunately the same is not true of my log-in system. The following is a long post but I need to cover the details to try and find a solution. Bear with me..

The system is fairly straight forward, if not a bit hefty. Each user has a salted hash stored in a MySQL table, alongside the SALT. When the user logs in, their SALT is retrieved and the submitted password becomes a salted hash.

If the submitted salted hash matches the one stored in the table, the user is authenticated. Details such as their name, last IP address, and account level (3 levels on most sites) are stored in an array assigned to a session variable. They are then redirected to the landing page of the restricted site to which they logged in (Members Only or Admin/CMS).

Secured pages include a smaller auth.php file that checks to see if the session variable containing their details is present. If not, they are redirected to that site's log-in form with an error message that reads "Please log-in." If it is present, they are allowed to continue and the details stored in the array are assigned to variables.

The problem that many users has reported is that they often need to log-in several times to keep from being bounced back to the log-in form with the "Please log-in" error message, or they navigate to another page in the secure site and randomly get bounced back to login with the same error. So, the session variable seems to either not be getting set or it is being cleared for some reason during normal use of the site.

The first problem has NEVER happened to me - over a multitude of devices and networks - and I have witnessed it at a client's office using their laptop. I had them connect to my mobile hotspot and there was no change. However, they were able to log in without any problems using my laptop and my hotspot connection. Unfortunately, I was not able to connect to their network using my laptop, so that variable could not be ruled out.

*NOTE - * I forgot to mention initially that the system works normally for problem clients after they have logged in two or three times with the correct credentials. Subsequent log-in attempts while their browser remains open tend to execute without problems thereafter. Also, the log-in page destroys the session.

Here is the code for each stage, starting with the log-in script:

login.php

<?php
putenv("TZ=US/Eastern");

if (array_key_exists('site', $_POST)) {
    $authenticate = new loginUser($_POST['username'], $_POST['password'], $_POST['site'], $_SERVER['REMOTE_ADDR']);
}
//Authenticate and log-in
class loginUser {
    private $memDB, $username, $password, $site, $ip_address;

     //Clean input variables
    private function clean($str) {
        $str = @trim($str);
        if(get_magic_quotes_gpc()) {
            $str = stripslashes($str);
        }
        return $str;
    }
    //Construct variables
    function __construct($username, $password, $site, $ip_address) {
    session_start();
        $this->memDB = new PDO('mysql:host=localhost;dbname=exampleDB', 'exampleUser', 'examplePassword');
        $this->username = $this->clean($username);
        $this->password = $this->clean($password);
        $this->site = $site;
        $this->ip_address = $ip_address;
    $this->authUser();
    }
    //Validate username
    private function validateUsername($username) {

        $checkUsername = $this->memDB->prepare("SELECT COUNT(*) FROM accounts WHERE username = ?");
        $checkUsername->execute(array($username));
        return $checkUsername->fetchColumn();
    }
    //Obtain and set account details
    private function accountDetails() {

        $fetchAccountDetails = $this->memDB->prepare("SELECT id, name_f, name_l, ipAddr, lastLogin, accountLevel, isActive 
        FROM accounts WHERE username = ?");
        $fetchAccountDetails->execute(array($this->username));
        $accountDetails = $fetchAccountDetails->fetch();
        $this->updateLogin();
        return $accountDetails;
    }
    //Update last login details
    private function updateLogin() {

        $updateLogin = $this->memDB->prepare("UPDATE accounts SET ipAddr = ?, lastLogin = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE username = ?");
        $updateLogin->execute(array($this->ip_address, $this->username));
    }
    public function authUser() {

        $loginErr = array(); //Array for holding login error message
        $loginErrFlag = false; //Boolean for error
        //Validate submitted $_POST elements
        if (!$this->username) {
            $loginErr[] = "Username missing";
            $loginErrFlag = true;
        }
        if (!$this->password) {
            $loginErr[] = "Password missing";
            $loginErrFlag = true;
        }
        if ($this->username && $this->validateUsername($this->username) == 0) {
            $loginErr[] = "Username invalid";
            $loginErrFlag = true;
        }
        if (!$loginErrFlag) {
            //Fetch the password and SALT to compare to entered password
            $validatePW = $this->memDB->prepare("SELECT password, salt FROM accounts WHERE username = ? LIMIT 1");
            $validatePW->execute(array($this->username));
            $passwordResult = $validatePW->fetch();
            $dbPW = $passwordResult['password'];
            $dbSalt = $passwordResult['salt'];
            //Compare entered password to SALT + hash
            $hashPW = hash('sha512', $dbSalt . $this->password);
            if ($hashPW === $dbPW) {
                //Logged in
                $_SESSION['CVFD-USER-DETAILS'] = $this->accountDetails();
                //Redirect to secure landing page for log-in origin (Members or Admin)
                //Adding SID is a recent attempt to handle log-in problems
                header("Location: http://example.com/$this->site/$this->site-main.php?" . SID);
                //session_write_close() was here but was removed
                exit();
            } else {
                //Password invalid
                $loginErr[] = "Please check your password and try again";
                $_SESSION['CVFD_LOGIN_ERR'] = $loginErr;
                //Redirect to the log-in for the origin
                header("Location: http://example.com/$this->site");
        session_write_close();
                exit();
            }
        } else {
            $_SESSION['CVFD_LOGIN_ERR'] = $loginErr;
            header("Location: http://example.com/$this->site");
            session_write_close();
            exit();
        }

    }
}
?>

auth.php

<?php
session_start();
if (!isset($_SESSION['CVFD-USER-DETAILS']) || $_SESSION['CVFD-USER-DETAILS'] == '') {
    //Not logged in
    $_SESSION['CVFD_LOGIN_ERR'] = array('Please login');
    header('Location: http://example.com/members');
    session_write_close();
    exit();
} else {
    $userDetails = $_SESSION['CVFD-USER-DETAILS']; //Assign user details array to variable
    //Check to see if account is active
    $accountStatus = $userDetails['isActive'];
    $accountLevel = $userDetails['accountLevel'];
    if ($accountStatus == 0) {
        //Account is not yet active (pending Admin activation)
        $_SESSION['CVFD_LOGIN_ERR'] = array('Your account is suspended or pending activation');
        header('Location: http://example.com/members');
        session_write_close();
        exit();
    } else {
        $CVFDFirstName = $userDetails['name_f'];
        $CVFDLastName = $userDetails['name_l'];
        $CVFDLastLogin = date("m/d/Y H:i:s", strtotime($userDetails['lastLogin']));
        $CVFDAccountLevel = $userDetails['accountLevel'];
        $CVFDIPAddr = $userDetails['ipAddr'];
    }
}
?>

Here is how the auth.php is included in secure files-

<?php
if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) ob_start("ob_gzhandler"); else ob_start();
require($_SERVER['DOCUMENT_ROOT'] . '/members/includes/handlers/handler.auth.php');

Any help would be appreciated. Quite a mystery..

Thanks!

回答1:

The one thing that jumps out at me is the following:

header('Location: http://example.com/members');
session_write_close();
exit();

I would place the session_write_close() call before the header('location ...')

Are any 'headers already sent' errors showing up in your logs?

Other thing that comes to mind is some AJAX race condition. Any async calls going on with login pages?



回答2:

The way I do login system is to just use the session id, rather than storing anything in the session itself. When a user logs in their hashed user agent data, their session ID, their user id (corresponding to a users table) and an expiry time is put into a table, often called "active_users", I then had a logged in head file included in every admin restricted page that starts the session, retrieves the users session ID and checks to see whether that session ID is in the active users table and whether the user being checked against has the same user agent data, the expiry time is not surpassed. If nothing is returned from that query they're not logged in and are bounced out.

That's how most login systems I make work and I haven't had any problems.



回答3:

Success! Still need to narrow down exactly what change resulted in the problem going away, but the client reports that he no longer has problems logging in.

The biggest change that immediately comes to mind was removing session_write_close() just about everywhere. It may have been placed AFTER the header redirect in parts of the code, or just having it present may have been the cause. I will experiment with placing it before the redirect.

Thanks to all for your suggestions