简体   繁体   English

使用会话进行高级用户验证

[英]advanced user authentication using sessions

I am setting up a website that has 3 types of users: 我正在建立一个有3种类型用户的网站:

-those who are registered and are subscribed for current month
-those that are registered, but are not subscribed for current month
-users that are not registered (you cant be subscribed if you are not regitered)

I've created code that identifies these 3 kinds of users and acts appropriately. 我已经创建了识别这3种用户的代码并且行为恰当。 My question is, is this the way to go? 我的问题是,这是要走的路吗? I've never done similar thing before. 我以前从未做过类似的事情。 Or should I reprogram my approach? 或者我应该重新编程我的方法?

//login.php //login.php

//connect to database and see if a user and password combination exists. Store $exists=0 if not, and $exists=1 if it exists.
session_start();

$conn = new mysqli($hn,$un,$pw,$db);
if ($conn->connect_error){
    die($conn->connect_error);
}
$query = "SELECT COUNT(1) as 'exists',expiration_date FROM table WHERE email = ? AND password = ?;";
$stmt = $conn->prepare($query);
$stmt->bind_param("ss", $email, $password);

$email = $_POST["email"];
$password = hash("hashingalgorithm", "salt".$_POST["password"]."salthere");

$stmt->execute();
   /* Get the result */
$result = $stmt->get_result();
$num_of_rows = $result->num_rows;
$row = $result->fetch_assoc();
$exists = $row["exists"];
$expiration_date = $row["expiration_date"];

/* free results */
$stmt->free_result();

/* close statement */
$stmt->close();
$conn->close();

date_default_timezone_set('Europe/Berlin');


if ($exists==0){
    echo "Wrong email or password";
    $_SESSION['loginerror'] = 2;
    header('Location: https://www.homepage.com/login'); 
}else if ($exists){
    if (strtotime($expiration_date) < (strtotime("now"))){//logged in, but not subscribed
        session_destroy();
        session_start();
        $_SESSION["authenticated"] = true;
        header('Location: https://www.homepage.com');
    }else{//logged in and ready to go
        $_SESSION["authenticated"] = true;
        $_SESSION["email"] = $email;
        header('Location: https://www.homepage.com');
    }
}else{
    echo "An error with has occured.";
} 

Then on every single page on my website I use this code, to see what kind of user has visited me 然后在我网站的每一页上我都使用这段代码,看看有哪些用户访问过我

session_start();
if(isset($_SESSION["authenticated"]) && isset($_SESSION["email"])){ 
    $email = $_SESSION["email"];

    //connect to database and fetch expiration_date for a user with $email. Store it in $expiration_date

$conn = new mysqli($hn,$un,$pw,$db);
    if ($conn->connect_error){
        die($conn->connect_error);
    }
    $query = "SELECT expiration_date FROM table WHERE email = ?;";
    $stmt = $conn->prepare($query);
    $stmt->bind_param("s", $email);

    $email = $_SESSION["email"];
    $stmt->execute();
    /* Get the result */
    $result = $stmt->get_result();
    $num_of_rows = $result->num_rows;
    $row = $result->fetch_assoc();
    $expiration_date = $row["expiration_date"];
    /* free results */
    $stmt->free_result();
    /* close statement */
    $stmt->close();
    $conn->close();
    date_default_timezone_set('Europe/Berlin');

    if (strtotime($expiration_date) < (strtotime("now"))){//logged in, but not subscribed
        session_destroy();
        session_start();
        $_SESSION["authenticated"] = true;
        header('Location: https://www.homepage.com');
    }else{  //html for subsribed and registered user
    echo <<<_END
    //html here
    _END;
    }
}else if(isset($_SESSION["authenticated"]) && !isset($_SESSION["email"])){
        // user is logged in, but not subscribed;
        echo <<<_END
        //htmlhere
        _END;
}else{// user is not registered nor is subscribed
        echo <<<_END
        //htmlhere
        _END;
}

The code works, but I am worried about accessing database on every single page once the user registers and is subscribed. 代码有效,但我担心一旦用户注册并订阅,就会在每个页面上访问数据库。 I am in effect penalizing users for registering and subscribing. 我实际上是在惩罚用户注册和订阅。 Is there a better, performance wise, way to handle this kind of problem? 是否有更好的,性能明智的方式来处理这类问题?

The solution here is to check for subscribed users (only on the first page where they login to your website) ie inside login.php you could use, 这里的解决方案是检查订阅用户(仅在他们登录您网站的第一页),即您可以使用的login.php内,

// ...your previous code
session_start();
// initialize session variables
$_SESSION['subscribe_date'] = $_SESSION['authenticated'] = false;
if ($exists == 0){
    echo "Wrong email or password";
    $_SESSION['loginerror'] = 2;
    header('Location: https://www.homepage.com/login'); 
} else if ($exists){
    $_SESSION["authenticated"] = true;
    if (strtotime($expiration_date) > (strtotime("now"))){ //logged in,& subscribed
        $_SESSION["email"] = $email;
        $_SESSION['subscribe_date'] = $expiration_date;
    } else {  //logged in and not subscribed, do nothin!
    }
    header('Location: https://www.homepage.com');
} else {
    echo "An error has occured.";
} 

Then, on every other page, all you need to do is only check for $_SESSION['subscribe_date'] instead of firing a query each time 然后,在每个其他页面上,您需要做的只是检查$_SESSION['subscribe_date']而不是每次都触发查询

if(!empty($_SESSION["subscribe_date"]) && strtotime($_SESSION["subscribe_date"]) > (strtotime("now"))) {
 // html for subscribed and registered user
} else if (!empty($_SESSION["authenticated"])) {
 // html for registered user
} else {
 // html for unregistered user
}

Also, note that I have removed session_destroy(); 另请注意,我已删除session_destroy(); A very bad idea to call it on every page. 在每个页面上调用它是一个非常糟糕的主意。 You can unset session variables instead if needed. 如果需要,您可以取消设置会话变量。 ie unset($_SESSION["email"]); 即未unset($_SESSION["email"]); You should call session_destroy(); 你应该调用session_destroy(); only at the end when the user logs out. 仅在用户注销时才结束。 Check out the warning-section for session_destroy() 查看session_destroy()的警告部分

If it was me, I would separate and refactor these codes into external classes/functions. 如果是我,我会将这些代码分离并重构为外部类/函数。 But if we are talking about minor tweeks to your code, here is what would I do: 但是如果我们在谈论你的代码的小问题,那么我将做什么:

login.php 的login.php

//connect to database and see if a user and password combination exists. Store $exists=0 if not, and $exists=1 if it exists.
session_start();

$conn = new mysqli($hn,$un,$pw,$db);
if ($conn->connect_error){
    die($conn->connect_error);
}
$query = "SELECT COUNT(1) as 'exists',expiration_date FROM table WHERE email = ? AND password = ?;";
$stmt = $conn->prepare($query);
$stmt->bind_param("ss", $email, $password);

$email = $_POST["email"];
$password = hash("hashingalgorithm", "salt".$_POST["password"]."salthere");

$stmt->execute();
/* Get the result */
$result = $stmt->get_result();
$num_of_rows = $result->num_rows;
$row = $result->fetch_assoc();
$exists = $row["exists"];
$expiration_date = $row["expiration_date"];

/* free results */
$stmt->free_result();

/* close statement */
$stmt->close();
$conn->close();

date_default_timezone_set('Europe/Berlin');


if ($exists==0){
    //removed echo, you shouldn't output anything before headers
    $_SESSION['loginerror'] = 2;
    header('Location: https://www.homepage.com/login');
    //use exit after each header redirect
    exit;
}else if ($exists){
    //moved to the top to minimize code redundancy
    $_SESSION["authenticated"] = true;
    //it's good to have user identifier (e.g. email) accessible for all authenticated users
    $_SESSION["email"] = $email;
    if (strtotime($expiration_date) <= (strtotime("now"))){//logged in, but not subscribed
        //unset only subscribed sessions, do not destroy all sessions
        unset($_SESSION["subscribed"]);
        header('Location: https://www.homepage.com');
        exit;
    }else{
        //logged in with active subscribtion
        $_SESSION["subscribed"] = true;
        header('Location: https://www.homepage.com');
        exit;
    }
}else{
    echo "An error with has occured.";
} 

included code on every page 每页都包含代码

session_start();
if(isset($_SESSION["authenticated"])){
    //merged if-statement for both subscribed and unsubscribed user
    $email = $_SESSION["email"];

    //connect to database and fetch expiration_date for a user with $email. Store it in $expiration_date

    $conn = new mysqli($hn,$un,$pw,$db);
    if ($conn->connect_error){
        die($conn->connect_error);
    }
    //check if subscribed user is still subscribed and unsubscribed user is still unsubscribed
    $query = "SELECT expiration_date FROM table WHERE email = ?;";
    $stmt = $conn->prepare($query);
    $stmt->bind_param("s", $email);

    $email = $_SESSION["email"];
    $stmt->execute();
    /* Get the result */
    $result = $stmt->get_result();
    $num_of_rows = $result->num_rows;
    $row = $result->fetch_assoc();
    $expiration_date = $row["expiration_date"];
    /* free results */
    $stmt->free_result();
    /* close statement */
    $stmt->close();
    $conn->close();
    date_default_timezone_set('Europe/Berlin');

    //I have separated expiration verification and template echoing
    //first - expiration verification
    if (strtotime($expiration_date) <= (strtotime("now")) && isset($_SESSION["subscribed"])){
        //had been subscribed a second ago, but now he's not
        unset($_SESSION["subscribed"]);
        header('Location: https://www.homepage.com');
        exit;
    }else if (strtotime($expiration_date) > (strtotime("now")) && !isset($_SESSION["subscribed"])){
        //had been unsubscribed, but renewed subscription in the meantime (maybe in another browser with this session still active)
        $_SESSION["subscribed"] = true;
        header('Location: https://www.homepage.com');
        exit;
    }

    //user successfully passed expiraton verification, maybe was few times redirected
    //second - template echoing
    if (isset($_SESSION["subscribed"])) {
        // user is logged in and subscribed;
        echo '';
    }else{
        // user is logged in, but not subscribed;
        echo '';
    }
}else{
    // user is not registered
    echo '';
}

There are comments before each change in your code. 在代码中的每次更改之前都有注释。 Here is short list of changes I've made: 以下是我所做的更改的简短列表:

  • use exit; 使用退出; after each header redirect: php - Should I call exit() after calling Location: header? 在每个头重定向之后: php - 我应该在调用Location:header之后调用exit()吗?
  • do not echo output before headers 不要在标题之前回显输出
  • use 3 session verification variables for every authenticated user - authenticated, subscribed and email 为每个经过身份验证的用户使用3个会话验证变量 - 经过身份验证,订阅和电子邮件
  • do not destroy whole session if user is not subscribed, use unset instead 如果用户未订阅,请不要销毁整个会话,请改用unset
  • I have merged both authenticated users (subscribed and unsubscribed) into one if statement 我已将两个经过身份验证的用户(已订阅和未订阅)合并到一个if语句中
  • I have separated expiration verification and output echoing in two subsequent steps 我在两个后续步骤中分离了到期验证和输出回显
  • on every page I am checking if subscribed user is still subscribed (subscription can expire during his session) and if unsubscribed did not become subscribed in meantime (he can use two browsers with active sessions) 在每个页面上,我正在检查订阅的用户是否仍然订阅(订阅可以在他的会话期间到期),如果取消订阅没有同时订阅(他可以使用两个具有活动会话的浏览器)

There are some other minor tweaks I would do (move date_default_timezone_set on top of every file etc.), but they are not topic of your question. 我会做一些其他的小调整(在每个文件的顶部移动date_default_timezone_set等),但它们不是你问题的主题。

In my understanding, You already have a working code. 根据我的理解,你已经有了一个有效的代码。 And what you are asking is opinion. 你要问的是意见。 You want to remove duplication in each page of checking into database for authentication and subscription. 您希望在检查数据库的每个页面中删除重复以进行身份​​验证和订阅。

In my opinion, you need to change how you use sessions , 在我看来,你需要改变你使用会话的方式,

 $_session['email']        // email address of user 
 $_session['auth_type']    // holds authentication type
 $_session['auth_till']    // subscription expire date

Then lets create function to check subscription. 然后让我们创建函数来检查订阅。 This function can be put into a separate file for example: init.php. 此函数可以放在单独的文件中,例如:init.php。 Here we can put session start mechanism so that in any case sessions will be available. 在这里,我们可以设置会话启动机制,以便在任何情况下都可以使用会话。

if(!isset($_SESSION)) 
    session_start();       // start session if not already started

// lets define global vars to hold authentication type of visitor
define("SUBSCRIBED",1); 
define("UN_SUBSCRIBED",2);
define("REGISTERED",3);
function checkSubscription():bool{
    $return = false;
    if(($_session['auth_type']==SUBSCRIBED)&&(strtotime($_session['auth_till']) < strtotime("now")))
        $return= true;
    return $return
}

And on login.php use same technique, while setting up sessions authentication type. 并且在login.php上使用相同的技术,同时设置会话身份验证类型。

Now any other page can simply use function to check subscription 现在任何其他页面都可以简单地使用函数来检查订阅

for example: 例如:

<?php
// file: product.php
include_once("init.php");
if(!checkSubscription()){
    // subscription finished. do what you need to do. otherwise continue.
}

There are many improvements that can be done to your codes. 您的代码可以进行许多改进。 But i think this would meet your needs. 但我认为这将满足您的需求。 Please let me know if you need any further assistant. 如果您需要任何其他助手,请告诉我。 Please visit Scape and let me know if any useful coding available there. 请访问Scape ,如果有任何有用的编码,请告诉我。

Use a PHP session variable to store information about the current user before making SQL queries on every page load. 在对每个页面加载进行SQL查询之前,使用PHP会话变量来存储有关当前用户的信息。

For example, when the user logs in, save the user id to that users session in a session variable: 例如,当用户登录时,将用户ID保存到会话变量中的该用户会话:

$_SESSION['user_id'] = <userid>;

On the next page load, check for that session variable before running the database query. 在下一页加载时,在运行数据库查询之前检查该会话变量。

if ($_SESSION['user_id']) {
    // normal operations
} else {
    // Make your database call
}

Generally speaking, when a user logs in for the first time, set two state flags in $_SESSION : 一般来说,当用户第一次登录时,在$_SESSION设置两个状态标志:

 $_SESSION['loggedIn']   = true
 $_SESSION['subscribed'] = bool //true or false

I assume that only registered users can login. 我假设只有注册用户才能登录。 If a user does something to change his or her state, update $_SESSION accordingly. 如果用户做了某些事情来改变他或她的状态,请相应地更新$_SESSION

Take note, be sure to check that a session is active before checking values. 请注意,在检查值之前,请务必检查会话是否处于活动状态。 Also, use session_regenerate_id to deter session fixation. 此外,使用session_regenerate_id来阻止会话固定。

Truly sophisticated types might try serializing a User object and storing it in $_SESSION . 真正复杂的类型可能会尝试序列化 User对象并将其存储在$_SESSION Then, on each page load, the User object's properties can be marshaled (unserialized) and made to come alive once more in a new instance of a User object. 然后,在每个页面加载时,可以对User对象的属性进行封送 (反序列化),并使其在User对象的新实例中再次生效。 In that world, you would just check the properties of the User object to see if he or she is (a) logged in and (b) subscribed. 在那个世界中,您只需检查User对象的属性,看看他或她是否(a)登录并(b)订阅。 The state of the User object would be your concern, not just isolated values in the $_SESSION superglobal. User对象的状态将是您关心的问题,而不仅仅是$_SESSION超全局中的隔离值。

PHP Manual: Object Serialization PHP手册: 对象序列化

Book: The Object Oriented Thought Process (4th Edition) : See Chapter 12 书: 面向对象思想过程(第4版) :见第12章

on login.php you already getting expiration_date from the DB, you could store it in session variable 在login.php上你已经从DB获得expiration_date ,你可以将它存储在会话变量中

$_SESSION['expiration_date'] = $row['expiration_date'];

and on every page you could use the above value to check instead of querying the DB 在每个页面上,您可以使用上面的值来检查而不是查询数据库

if (isset($_SESSION['expiration_date']) && (strtotime($_SESSION['expiration_date']) < (strtotime("now") ))

PHP session model allows to start the session and fetch later all data, concerning to the session that was serialized and stored in $_SESSION array. PHP会话模型允许启动会话并稍后获取所有数据,这些数据与序列化并存储在$_SESSION数组中的会话有关。

It is right to worry about database calls that cause each page access. 担心导致每个页面访问的数据库调用是正确的。 The session data can be set with cookie or given back inside POST or GET method. 会话数据可以使用cookie设置,也可以在POSTGET方法中返回。 So, you need to define suitable method of PHP session id retreival and session lifetime. 因此,您需要定义适合的PHP会话ID retreival和会话生命周期的方法。

$_SESSION['name'] = 'value'; // Stores Session values to avoid database requests
...
session_id($sid); // sets the id of existing session
session_start(); // retreive stored session data;

You can set once such flags as registered and subscribed for beinh used later with no database requests during session lifetime or special actions: 您可以设置一次registeredsubscribed标记,以便稍后使用,在会话生命周期或特殊操作期间没有数据库请求:

  • Subscribed - no actions expected. 订阅 - 未预期的行动。 User already satisfied all expectations. 用户已满足所有期望。
  • Registered - can only subscribe. 已注册 - 只能订阅。 So, before subscribe action met, user environment can keep database idle until subscribe request. 因此,在满足订阅操作之前,用户环境可以使数据库空闲直到订阅请求。
  • Not Registered - Can only register. 未注册 - 只能注册。 No stored data. 没有存储的数据。 No database requests. 没有数据库请求。

When session starts, both subscribed and registered flags will be set to FALSE until authentication passed. 会话启动时, subscribedregistered标志都将设置为FALSE直到验证通过。 So, my opinion - database must be called only three times per session: when user authenticates, registers or subscribes. 所以,我的意见 - 每个会话只需要调用三次数据库:用户进行身份验证,注册或订阅时。 As code shows, you transfer data via POST . 如代码所示,您通过POST传输数据。 Add sid hidden field to all forms to keep session id. sid隐藏字段添加到所有表单以保持会话ID。 And SELECT from database all needed data (not password, of course) that can be useful during the session to interact with user. 并且从数据库中SELECT所有需要的数据(当然不是密码),这些数据在会话期间与用户交互时非常有用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM