1. 论坛系统升级为Xenforo,欢迎大家测试!
    排除公告

来个曲奇饼吧

本帖由 相当2006-04-13 发布。版面名称:谈天说地

  1. 相当

    相当 New Member

    注册:
    2006-03-28
    帖子:
    9,431
    赞:
    42
    User identification using cookies in PHP/MySQL

    by devarticles


    What is a cookie and how do we use them in PHP? If you can not answer this question, you should definitely have a read through this article.
    This article will explain the basics and possibilities of user identification using cookies. First of all the mechanics of cookies must be explained in order to understand what can be passed to cookies and how well protected that information is.

    Cookies are ASCII files that browsers store in temporary internet directories. These files are set by internet webpage, either through HTTP header properties, or by using javascript code. Therefore, the browser is the one responsible of storing the cookie files, by request of the viewed page. All that gets transmitted from remote page is a set of variables the cookie should contain. A cookie contains several variables that are important to us:

    * name string
    * information string, called VALUE
    * expiration time
    * relevant host address
    * relevant host directory

    Name is cookie�s name. By this name it will be identified in our script.

    The value is a string that can carry certain information. In our case, it will hold the data user is identified by. Expiration time is number of seconds passed since January 1., 1970., midnight GMT. Therefore, it represents accurate date/time combination when the cookie is no longer valid. The host address identifies the host this cookie will be transmitted to, and the directory represents host�s subdirectory which should receive this cookie.

    In other words, by setting the address and directory you somewhat ensure that only files contained within that directory and on that host will receive the cookie. For example, you have a script called identify.php with the following url:

    www.somedomain.com/somedirectory/identify.php

    To ensure only identify.php will receive the desired cookie, the cookie�s host address must be �.somedomain.com� and the directory must be �/somedirectory/�. Browsers will recognize the addres/directory combination and will pass the cookie to the identify.php script. Note that domain address has a dot as the first character. This ensures that any prefix validates the domain, prefixes being �www� or �ftp� or any other.

    So the mechanism is this: browsers register domains and directories for all cookies stored in a local directory. When the browser is directed to a particular URL, only the cookies that are registered for that URL (domain/directory combination) will be passed to a HTML or PHP file in that URL, along with all those cookies that do not have domain address nor host directory set (which are in that case passed to all URL�s browsers are directed to). Also, only those cookies that have not yet expired will be passed. Expired cookies will be deleted from local computers by the browser.

    This article assumes that the reader is familiar with PHP programming and usage of MySQL databases.

    Setup of Cookie Data

    Up to now we know what a cookie can and should contain. In order to strenghten the security, domain address and relevant subdirectories must always be set, along with expiration time.

    Before we actually deal with the data our cookie should carry, we must define user data first. Let’s assume that our users have following fields in the database:

    * ID
    * Username
    * Password
    * Logcode

    along with any other data your users list must contain.

    Let us set up the MySQL table with a following command:

    >CREATE TABLE my_users (
    >id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    >username VARCHAR(20),
    >password CHAR(32),
    >logcode VARCHAR(32),
    >PRIMARY KEY (id),
    >INDEX (username),
    >INDEX (password));

    Certain fields require explanation:

    password will be a md5 hash of user’s password, so it is 32 chars fixed-length

    logcode is a md5 hash that identifies whether the user is logged in or not.

    Now, let us explain what data will be transmited to cookies, and in what manner.

    1. When the user logs in, he is identifed by his username/password combination (thus the indices).
    2. Upon actual login he will receive a randomly created, md5 hashed code called logcode
    3. The logcode will be written in my_users table, for that particular user
    4. User’s ID and LOGCODE will be set in a cookie, on user’s computer

    When the user is accessing any protected area, a small function should read the cookie and compare if ID exists in the table, and if does, compare to see if LOGCODEs are equal. If they are not, the user is not allowed to access the page, if they are, a new LOGCODE is generated, stored in the table, and stored on user’s computer in the cookie.

    Therefore, upon each successful access, the user receives new LOGCODE, which actually updates his logged status, and strenghtens the security. We will see later why.

    Now, let us see what should actually happen in our scripts.

    login.php and Logging of Users

    A small form should input user’s username and password, both passed to login.php via POST protocol. The login.php does the following:

    PHP:
    <?php
    // Check if the script received required values
    if (isset($_POST['username']) && isset($_POST['password'])) {
    // trim and read username and password from the form
    $usernameltrim(rtrim(addslashes($_POST['username'])));
    $passwordltrim(rtrim(addslashes($_POST['password'])));
     
    // generate a MD5 hash of the password
    $mdpassmd5($password);
    // now, detect user (get his ID) by username/password_md5 combination
    $resmysql_query("SELECT id FROM my_users WHERE username='$username' AND password='$mdpass'") or die(“Could not select user ID.);
    // just to ensure there is only one user by that username/password combination
    // in the database, we check if only one row was returned
    if (mysql_num_rows($res)==1) {
    $user_objmysql_fetch_object($res);
    $user_id$user_obj->id;
      
    // now generate a random 8 char long string, and hash it with MD5
    $logcodemd5(func_generate_string());
      
    // now update user’s information in the database
    $resmysql_query("UPDATE my_users SET logcode='$logcode' WHERE id=$user_id") or die(“Could not update database.);
    // now, let us setup the identification information that will be passed to user’s computer via a cookie
      // we will store user’s ID and LOGCODE in ID:LOGCODE form so that we can later extract it using explode() function
    $newval"$user_id:$logcode";
      
    // store the cookie
                 
    setcookie("cookiename"$newvaltime() + 300"/subdirectory/"".somedomain.com");
      
    // redirect to some user welcome area
    header("Location: http://www.somedomain.com/subdirectory/welcome.php");
    exit;
    } else {
     
    // report invalid user or invalid username/password combination
    }
    } else {
     
    // report invalid login
    }
    ?>
    Now, it must be mentioned that cookies can only be set in HTTP headers, thatis before any output has been generated for browsing. Our die() functions will abort before the cookie is set, automatically switching to HTML output mode, setcookie will never be reached in that case. Lastly, we redirect to a welcome area. This is important, because set cookies are not visible until next reload of some page, under that domain and subdirectory. Also, subdirectories are not needed. You can have your script in domain’s root, in which case you omit the directory value. See PHP documentation for setcookie() function explanation.

    Note that this script requires an open connection to MySQL database, so you must open it before abovementioned code, and close the connection after.

    The Random String Generating Function

    As seen in previous caption, we have generated a 8 char long random string that we additionally hashed with MD5 algorithm. First, here is the code of func_generate_string()

    PHP:
    <?php
    function func_generate_string() {
    $auto_stringchr(mt_rand(ord('A'), ord('Z')));
    for (
    $i0$i<8$i++) {
    $ltrmt_rand(13);
    if (
    $ltr==1$auto_password .= chr(mt_rand(ord('A'), ord('Z')));
    if (
    $ltr==2$auto_password .= chr(mt_rand(ord('a'), ord('z')));
    if (
    $ltr==3$auto_password .= chr(mt_rand(ord('0'), ord('9')));
    }
    return 
    $auto_string;
    }
    ?>
    What this function actually does is that it sets the first character to random uppercase letter (A-Z), and then it sets the following characters to either uppercase (A-Z; if $ltr=1), lowercase (a-z; if $ltr=2) or a number (0-9; if $ltr=3). Using mt_rand() function we strenghten the random distribution. The for(;;) loop can be modified to output any number of characters.

    This is a neat function that can be used in automatic password generation too, or any other situation that requires a random string.

    detectuser.php and User Identification

    The next script actually identifies the user, via values in cookies. It is important to note that this script must be required with require() function in all your pages that require logged users. Also, this script will setup a global $global_user_id variable that stores user’s id in my_users table. You can use its value to manipulate user’s data or monitor what the user is doing. Also, require detectuser.php at the beginning of your scripts to ensure eventual header redirections before anything is output. An example for this would be if the user is not identified, but has requested a protected page, the page will redirect to a login page, instead of showing its contents.

    someprotectedpage.php

    PHP:
    <?php
     $legal_require_php
    1234;
    require (
    ‘detectuser.php’);
     ...
    ?>
    detectuser.php

    PHP:
    <?php
     
    //see if detectuser.php has been required, not URL’d.
    if ($legal_require_php!=1234) exit;
     
    // setup global variable $global_user_id, set it to 0, which means no user as auto_increment IDs in MySQL begin with 1
     
    $global_user_id0;
     
    // now, check if user’s computer has the cookie set
    if (isset($_COOKIE['cookiename'])) {
    $cookieval$_COOKIE['cookiename'];
    //now parse the ID:LOGCODE value in cooke via explode() function
    $cookieparsedexplode (":"$cookieval);
    // $cookie_uid will hold user’s id
    // $cookie_code will hold user’s last reported logcode
    $cookie_uid$cookieparsed[0];
    $cookie_code$cookieparsed[1];
      
    // ensure that ID from cookie is a numeric value
    if (is_numeric($cookie_uid)) {
    //now, find the user via his ID
    $resmysql_query("SELECT logcode FROM my_users WHERE id=$cookie_uid");
    // no die() this time, we will redirect if error occurs
    if ($res) {
     
    // now see if user’s id exists in database
       
    if (mysql_num_rows($res,0) {
      
    $logcode_in_basemysql_result($res0);
      
    // now compare LOGCODES in cookie against the one in database
      
    if ($logcode_in_base == $cookie_code) {
       
    // if valid, generate new logcode and update database
       
    $newcodemd5(func_generate_string());
       
    $resmysql_query(“UPDATE my_users SET logcode=’$newcode’ WHERE id=$cookie_uid”);
       
    // setup new cookie (replace the old one)
       
    $newval“$cookie_uid:$newcode”;
       
    setcookie("cookiename"$newvaltime() + 300"/subdirectory/"".somedomain.com");
       
    // finally, setup global var to reflect user’s id
       
    $global_user_id$cookie_uid;
      } else {
       
    // redirect if logcodes are not equal
      
    }
     } else {
      
    // redirect if user ID does not exist in database
     
    }
     } else {
      
    // redirect in case of database error
     
    }
    } else {
     
    // redirect if user ID in cookie not numeric
    }
     }
    ?>
    First, let us explain the $legal_require_php variable. In order to ensure that our script is actually required(), not called via URL, we must setup this variable to some arbitrary value in all CALLER scripts. In all REQUIRED scripts we check if that variable is set and contains chosen value.

    Next, a note on redirection. We redirect from ALL secure pages (all pages that require() detectuser.php) in following cases:

    * cookie is not set – meaning, user is not logged in, or cookie expired
    * logcode from cookie is not the same as logcode in database – meaning breach attempt
    * value ID in cookie is not numeric – someone tampered with the cookie, or the cookie is corrupt
    * user does not exist in database – cookie corrupted or has been tampered with
    * database error

    You can setup redirection or error message notification in mentioned cases. That is up to you.

    Also note that on each access to detectuser.php (meaning on each reload or click within secure pages) the user receives a new logcode. This renews user identification and somewhat disables someone else to steal the cookie (or its value) and sets it on his own computer, pretending to be a logged user. More frequent the clicks (reloads of detectuser.php), more secure he is from being hacked.

    Also, we have set up the cookie expiration time to be 5 minutes (300 seconds) from current access time. This means the user will be considered logged in for up to 5 minutes of him being idle. You can modify this number, or request your users to choose this time upon registration. By storing this time (in seconds) in the my_users table, you can, upon each access, read it and set it to cookie expiration time like this:

    $expire= time() + $user_chosen_time_in_seconds;
    setcookie("cookiename", $newval, $expire, "/subdirectory/", ".somedomain.com");

    The logout.php and Cookie Cleanup

    When the user wants to logout, all that has to be done (in a separate script, if wished) is to store an empty cookie with SAME NAME and host/directory parameters, but with expiration time set in past.

    In order for the script to know who is logged out, pass the $global_user_id value to it, preferably via POST protocol. This means you should have a LOGOUT button in a small form, somewhere on your secure pages. Pass the user id via userid variable, as hidden value in your form.

    PHP:
    <?php
     
    // ensure the userid is passed
    if (isset($_POST['userid'])) {
    $userid$_POST['userid'];           
      
    // ensure the value is numeric
    if (is_numeric($userid)) {
       
    // update database
    $resmysql_query("UPDATE my_users SET logcode='none' WHERE id=$userid");
    setcookie("cookiename""empty"time() - 3600"/directory/"".somedomain.com");
    }
    }
     
    // redirect to a logoff (thanks for using our service) page
    header("Location: http://somedomain.com/logoffpage.php");
    exit;
    ?>
    As you can see, we have set the cookie with the same name and parameters, but with empty value string, and expiration time somewhere in the past (one hour before now).

    If someone misuses this script and somehow passes some value to this script, all he can do is to logout the user with ID he passed.

    Again, setting cookies with same name and host/directory parameters REPLACES old cookies with same parameters on local machines.
    Naturally, using cookies is not the safest method to protect your pages, since there are pitfalls. Here are some:

    * Cookies are not visible to your pages until first reload after you set it
    * Cookies must be deleted with same parameters as they were set with
    * Cookies can be stolen, their values read, since they are ASCII files
    * Cookies can be stored on proxy servers and thus exposed externally to user’s computer
    * Not a safest method if user does access your pages through a public computer
    * In case user does not logout, cookie remains on his computer until it expires

    But, since we are not passing usernames/passwords to cookies, no one can read them and try to login. Using logcodes we ensure somewhat unique identification of your users, and since logcodes are renewed upon each access to your protected pages, it makes it difficult to be stolen, since the thief must steal the cookie, read the value, setup a cookie on his own machine and access your pages, all within the timeframe between two clicks of your real user, or within the timeframe of his idle time.

    In case of increased security is required, consider making a javascript that will reload your pages each n seconds and set cookie expiration time to that number of seconds. This will ensure that even if the user is idle, each n seconds his logcode is renewed. The smaller amount of time between two reloads, the more secure the access is. Also, you can combine this method with session-based user id and pass session ID within the cookie too, ensuring cookie cannot be stolen.

    Actually, we consider cookie-based user identification to be very secure, except if accessed from a public computer, but a proper logout fixes the problem. Force your users to return and logout if their connections are broken.

    Also, you can setup a javascript event handler that calls logout.php when the browser is closed, or removes the cookie manually. But, removing the cookie with javascript exposes your cookie name and parameters in your page’s sourcecode, so keep that in mind. By setting cookie names to some difficult-to-guess names can be an additional boost up to your security.
     
  2. shangjay

    shangjay New Member

    注册:
    2006-01-13
    帖子:
    3,530
    赞:
    15
    给神看?
     
  3. 相当

    相当 New Member

    注册:
    2006-03-28
    帖子:
    9,431
    赞:
    42
    给老外看
     
  4. 相当

    相当 New Member

    注册:
    2006-03-28
    帖子:
    9,431
    赞:
    42
    :ghost: