30、PHP 网站登录与权限管理系统搭建

PHP 网站登录与权限管理系统搭建

1. 接口与返回类型

为了保证代码的一致性,将 $jokesTable 设为类变量,同时把 Authentication 对象移到构造函数中,并通过 getAuthentication 方法返回。在 EntryPoint 中添加身份验证检查之前,先在 Routes 接口里添加 getAuthentication 方法:

<?php
namespace Ninja;
interface Routes
{
    public function getRoutes();
    public function getAuthentication();
}

添加此方法后,实现该接口的类必须有 getAuthentication 方法。为增强安全性,可对返回值进行类型提示:

<?php
namespace Ninja;
interface Routes
{
    public function getRoutes(): array;
    public function getAuthentication(): \Ninja\Authentication;
}

修改接口后, IjdbRoutes 类也需相应修改:

public function getRoutes(): array {
    $jokeController =
        new \Ijdb\Controllers\Joke($this->jokesTable,
            $this->authorsTable);
    // …
    return $routes;
}
public function getAuthentication(): \Ninja\Authentication {
    return $this->authentication;
}

这样的接口明确了使用该框架构建网站时, Routes 类需包含 getRoutes (返回数组)和 getAuthentication (返回 Authentication 对象)方法,有助于代码共享和协作。

2. 使用身份验证类

EntryPoint.php 中添加检查,若路由数组里的 login 键存在且为 true ,而用户未登录,则重定向到登录页面;否则正常显示页面:

if (isset($routes[$this->route]['login']) &&
    isset($routes[$this->route]['login']) &&
    !$authentication->isLoggedIn()) {
    header('location: /login/error');
}
else {
    $controller = $routes[$this->route]
        [$this->method]['controller'];
    $action = $routes[$this->route][$this->method]
        ['action'];
    $page = $controller->$action();
    $title = $page['title'];
    if (isset($page['variables'])) {
        $output = $this->loadTemplate($page['template'],
            $page['variables']);
    }
    else {
        $output = $this->loadTemplate($page['template']);
    }
    include  __DIR__ . '/../../templates/layout.html.php';
}

需注意,若没有 else 语句,即使重定向到登录页面,控制器动作仍会执行,可能导致数据库操作异常。

3. 登录错误信息

测试上述代码时,若尝试添加笑话,会被重定向到 /login/error 页面。可按以下步骤创建该页面显示有意义的错误信息:
1. 在 templates 目录添加 loginerror.html.php

<h2>You are not logged in</h2>
<p>You must be logged in to view this page. <a
        href="/login">Click here to log in</a> or <a
        href="/author/register">Click here to register an
        account</a></p>
  1. Ijdb\Controllers 目录添加 Login.php 控制器:
<?php
namespace Ijdb\Controllers;

class Login
{
    public function error()
    {
        return ['template' => 'loginerror.html.php', 'title'
            => 'You are not logged in'];
    }
}
  1. IjdbRoutes.php 中实例化控制器并添加路由:
public function getRoutes(): array {
    $jokeController = new \Ijdb\Controllers\Joke
    ($this->jokesTable, $this->authorsTable);
    $authorController = new \Ijdb\Controllers\Register
    ($this->authorsTable);
    $loginController = new \Ijdb\Controllers\Login();
    $routes = [
        'author/register' => [
            'GET' => [
                'controller' => $authorController,
                'action' => 'registrationForm'
            ],
            'POST' => [
                'controller' => $authorController,
                'action' => 'registerUser'
            ]
        ],
        // …
        'login/error' => [
            'GET' => [
                'controller' => $loginController,
                'action' => 'error'
            ]
        ]
    ];
}
4. 创建登录表单

登录检查完成后,需创建登录表单。在 Login 控制器中添加构造函数和显示表单的方法:

<?php
namespace Ijdb\Controllers;

class Login
{
    private $authentication;

    public function __construct(\Ninja\Authentication
                                $authentication)
    {
        $this->authentication = $authentication;
    }

    public function error()
    {
        return ['template' => 'loginerror.html.php', 'title'
            => 'You are not logged in'];
    }

    public function loginForm() {
        return ['template' => 'login.html.php',
            'title' => 'Log In'];
    }
}

添加 login.html.php 模板:

<?php
if (isset($error)):
    echo '<div class="errors">' . $error . '</div>';
endif;
?>
<form method="post" action="">
    <label for="email">Your email address</label>
    <input type="text" id="email" name="email">
    <label for="password">Your password</label>
    <input type="password" id="password" name="password">
    <input type="submit" name="login" value="Log in">
</form>
<p>Don't have an account? <a
        href="/author/register">Click here to register an
        account</a></p>

IjdbRoutes.php 中添加路由:

public function getRoutes(): array {
    // …
    $loginController = new \Ijdb\Controllers\
    Login($this->authentication);
    $routes = [
        // …
        'login' => [
            'GET' => [
                'controller' => $loginController,
                'action' => 'loginForm'
            ]
        ],
        // …
    ];
}

再添加 POST 动作和登录成功页面:

// IjdbRoutes.php
$routes = [
    // …
    'login' => [
        'GET' => [
            'controller' => $loginController,
            'action' => 'loginForm'
        ],
        'POST' => [
            'controller' => $loginController,
            'action' => 'processLogin'
        ]
    ],
    'login/success' => [
        'GET' => [
            'controller' => $loginController,
            'action' => 'success'
        ],
        'login' => true
    ]
];

// Login.php
public function processLogin() {
    if ($this->authentication->login($_POST['email'],
        $_POST['password'])) {
        header('location: /login/success');
    }
    else {
        return ['template' => 'login.html.php',
            'title' => 'Log In',
            'variables' => [
                'error' => 'Invalid username/password.'
            ]
        ];
    }
}
public function success() {
    return ['template' => 'loginsuccess.html.php',
        'title' => 'Login Successful'];
}

// loginsuccess.html.php
<h2>Login Successful</h2>
<p>You are now logged in.</p>
5. 注销功能

在网站布局中添加登录/注销按钮,修改 layout.html.php 以根据用户登录状态显示不同链接。先修改 EntryPoint.php 中的 include 语句:

echo $this->loadTemplate('layout.html.php', ['loggedIn'
    =>
    $authentication->isLoggedIn(),
    'output' => $output,
    'title' => $title
]);

layout.html.php 中添加链接:

<ul>
    <li><a href="/">Home</a></li>
    <li><a href="/joke/list">Jokes List
        </a></li>
    <li><a href="/joke/edit">Add a new Joke
        </a></li>
    <?php if ($loggedIn): ?>
        <li><a href="/logout">Log out</a>
        </li>
    <?php else: ?>
        <li><a href="/login">Log in</a></li>
    <?php endif; ?>
</ul>

创建注销页面和路由:

// Login.php
public function logout() {
    unset($_SESSION);
    return ['template' => 'logout.html.php',
        'title' => 'You have been logged out'];
}

// IjdbRoutes.php
'logout' => [
    'GET' => [
        'controller' => $loginController,
        'action' => 'logout'
    ]
];

// logout.html.php
<h2>Logged out</h2>
<p>You have been logged out</p>
6. 将添加的笑话关联到登录用户

用户可注册登录后,需将添加的笑话关联到登录用户。当前 Joke 控制器的 saveEdit 方法中 authorId 固定为 1,需修改。先在 Authentication 类中添加 getUser 方法:

public function getUser() {
    if ($this->isLoggedIn()) {
        return $this->users->find($this->usernameColumn,
            strtolower($_SESSION['username']))[0];
    }
    else {
        return false;
    }
}

Joke 控制器中引入 Authentication 类:

<?php
namespace Ijdb\Controllers;
use \Ninja\DatabaseTable;
use \Ninja\Authentication;

class Joke {
    private $authorsTable;
    private $jokesTable;
    public function __construct(DatabaseTable $jokesTable,
                                DatabaseTable $authorsTable,
                                Authentication $authentication) {
        $this->jokesTable = $jokesTable;
        $this->authorsTable = $authorsTable;
        $this->authentication = $authentication;
    }
}

修改 saveEdit 方法:

public function saveEdit() {
    $author = $this->authentication->getUser();
    $joke = $_POST['joke'];
    $joke['jokedate'] = new \DateTime();
    $joke['authorId'] = $author['id'];
    $this->jokesTable->save($joke);
    header('location: /joke/list');
}
7. 用户权限管理

测试登录系统时会发现,任何人都能编辑和删除他人的笑话,需添加检查来限制用户权限。
1. 隐藏笑话列表中的编辑和删除按钮
- 在 Joke 控制器的 list 方法中提供作者 ID:

public function list() {
    $result = $this->jokesTable->findAll();
    $jokes = [];
    foreach ($result as $joke) {
        $author = $this->authorsTable->
            findById($joke['authorId']);
        $jokes[] = [
            'id' => $joke['id'],
            'joketext' => $joke['joketext'],
            'jokedate' => $joke['jokedate'],
            'name' => $author['name'],
            'email' => $author['email'],
            'authorId' => $author['id']
        ];
    }
    // …
    $totalJokes = $this->jokesTable->total();
    $author = $this->authentication->getUser();
    return ['template' => 'jokes.html.php',
        'title' => $title,
        'variables' => [
            'totalJokes' => $totalJokes,
            'jokes' => $jokes,
            'userId' => $author['id'] ?? null
        ]
    ];
}
- 在 `jokes.html.php` 中添加 `if` 语句:
// …
echo $date->format('jS F Y');
?>)
<?php if ($userId == $joke['authorId']): ?>
    <a href="/joke/edit?id=<?=$joke['id']?>">
        Edit</a>
    <form action="/joke/delete" method="post">
        <input type="hidden" name="id"
               value="<?=$joke['id']?>">
        <input type="submit" value="Delete">
    </form>
<?php endif; ?>
</p>
</blockquote>
<?php endforeach; ?>
  1. 防止直接访问编辑页面
    • Joke 控制器的 edit 方法中传递用户 ID:
public function edit() {
    $author = $this->authentication->getUser();
    if (isset($_GET['id'])) {
        $joke = $this->jokesTable->findById($_GET['id']);
    }
    $title = 'Edit joke';
    return ['template' => 'editjoke.html.php',
        'title' => $title,
        'variables' => [
            'joke' => $joke ?? null,
            'userId' => $author['id'] ?? null
        ]
    ];
}
- 在 `editjoke.html.php` 中添加检查:
<?php if ($userId == $joke['authorId']): ?>
    <form action="" method="post">
        <input type="hidden" name="joke[id]"
               value="<?=$joke['id'] ?? ''?>">
        <label for="joketext">Type your joke here:
        </label>
        <textarea id="joketext" name="joke[joketext]" rows="3"
                  cols="40"><?=$joke['joketext'] ?? ''?>
        </textarea>
        <input type="submit" name="submit" value="Save">
    </form>
<?php else: ?>
    <p>You may only edit jokes that you posted.</p>
<?php endif; ?>
  1. 防止通过表单提交修改他人笑话
// Joke 控制器的 saveEdit 方法
public function saveEdit() {
    $author = $this->authentication->getUser();
    if (isset($_GET['id'])) {
        $joke = $this->jokesTable->findById($_GET['id']);
        if ($joke['authorId'] != $author['id']) {
            return;
        }
    }
    $joke = $_POST['joke'];
    $joke['jokedate'] = new \DateTime();
    $joke['authorId'] = $author['id'];
    $this->jokesTable->save($joke);
    header('location: /joke/list');
}

// Joke 控制器的 delete 方法
public function delete() {
    $author = $this->authentication->getUser();
    $joke = $this->jokesTable->findById($_POST['id']);
    if ($joke['authorId'] != $author['id']) {
        return;
    }
    $this->jokesTable->delete($_POST['id']);
}

通过以上步骤,可构建一个功能完善的登录系统,包括登录、注销、权限管理等功能,确保用户数据安全和操作的合理性。

以下是一个简单的流程图,展示用户登录和操作的基本流程:

graph TD;
    A[用户访问页面] --> B{是否需要登录};
    B -- 否 --> C[显示页面];
    B -- 是 --> D{是否登录};
    D -- 是 --> E[显示页面];
    D -- 否 --> F[重定向到登录页面];
    F --> G{输入登录信息};
    G -- 成功 --> E;
    G -- 失败 --> H[显示错误信息];
    H --> G;
    E --> I{是否有操作权限};
    I -- 是 --> J[执行操作];
    I -- 否 --> K[显示无权限信息];

表格总结各功能对应的文件和代码:
| 功能 | 文件 | 代码 |
| ---- | ---- | ---- |
| 接口定义 | Routes.php | 定义 Routes 接口及方法 |
| 身份验证检查 | EntryPoint.php | 检查用户登录状态并处理重定向 |
| 登录错误页面 | loginerror.html.php Login.php IjdbRoutes.php | 显示登录错误信息及路由配置 |
| 登录表单 | login.html.php Login.php IjdbRoutes.php | 显示登录表单及处理登录逻辑 |
| 注销功能 | layout.html.php Login.php IjdbRoutes.php | 显示注销按钮及处理注销逻辑 |
| 关联笑话作者 | Joke.php Authentication.php | 将笑话与登录用户关联 |
| 用户权限管理 | Joke.php jokes.html.php editjoke.html.php | 限制用户对笑话的操作权限 |

PHP 网站登录与权限管理系统搭建(续)

8. 权限管理的进一步优化

虽然前面已经对用户权限进行了基本的管理,但仍有一些细节可以进一步优化,以提高系统的安全性和用户体验。

8.1 权限验证的封装

为了避免在多个方法中重复编写权限验证的代码,可以将权限验证逻辑封装到一个单独的方法中。例如,在 Joke 控制器中添加一个 checkPermission 方法:

private function checkPermission($jokeId) {
    $author = $this->authentication->getUser();
    $joke = $this->jokesTable->findById($jokeId);
    return $joke['authorId'] == $author['id'];
}

然后在 saveEdit delete 方法中调用这个方法:

public function saveEdit() {
    if (isset($_GET['id']) && !$this->checkPermission($_GET['id'])) {
        return;
    }
    $author = $this->authentication->getUser();
    $joke = $_POST['joke'];
    $joke['jokedate'] = new \DateTime();
    $joke['authorId'] = $author['id'];
    $this->jokesTable->save($joke);
    header('location: /joke/list');
}

public function delete() {
    if (!isset($_POST['id']) || !$this->checkPermission($_POST['id'])) {
        return;
    }
    $this->jokesTable->delete($_POST['id']);
}

这样可以使代码更加简洁和易于维护。

8.2 错误信息的统一处理

当用户没有权限进行操作时,除了简单地返回,还可以统一处理错误信息,给用户更友好的提示。可以在 Joke 控制器中添加一个 showNoPermissionMessage 方法:

private function showNoPermissionMessage() {
    return ['template' => 'nopermission.html.php',
        'title' => 'No Permission',
        'variables' => [
            'message' => 'You do not have permission to perform this action.'
        ]
    ];
}

然后在权限验证失败时调用这个方法:

public function saveEdit() {
    if (isset($_GET['id']) && !$this->checkPermission($_GET['id'])) {
        return $this->showNoPermissionMessage();
    }
    $author = $this->authentication->getUser();
    $joke = $_POST['joke'];
    $joke['jokedate'] = new \DateTime();
    $joke['authorId'] = $author['id'];
    $this->jokesTable->save($joke);
    header('location: /joke/list');
}

public function delete() {
    if (!isset($_POST['id']) || !$this->checkPermission($_POST['id'])) {
        return $this->showNoPermissionMessage();
    }
    $this->jokesTable->delete($_POST['id']);
}

同时,创建 nopermission.html.php 模板:

<h2>No Permission</h2>
<p>{{message}}</p>
9. 安全性考虑

在实现登录和权限管理系统时,安全性是至关重要的。以下是一些需要注意的安全问题及解决方法。

9.1 密码加密

在用户注册和登录过程中,密码应该进行加密存储,以防止密码泄露。可以使用 PHP 的 password_hash password_verify 函数来实现密码的加密和验证。

在用户注册时,对密码进行加密:

// Register.php 中的 registerUser 方法
public function registerUser() {
    $author = $_POST['author'];
    $author['password'] = password_hash($author['password'], PASSWORD_DEFAULT);
    $this->authorsTable->save($author);
    header('location: /login');
}

在用户登录时,验证加密后的密码:

// Authentication.php 中的 login 方法
public function login($email, $password) {
    $author = $this->users->find('email', $email)[0];
    if ($author && password_verify($password, $author['password'])) {
        $_SESSION['username'] = $author['email'];
        return true;
    }
    return false;
}
9.2 防止 SQL 注入

在进行数据库查询时,要防止 SQL 注入攻击。可以使用预处理语句来避免这个问题。例如,在 DatabaseTable 类中,使用 PDO 的预处理语句:

// DatabaseTable.php 中的 find 方法
public function find($column, $value) {
    $query = 'SELECT * FROM ' . $this->table . ' WHERE ' . $column . ' = :value';
    $stmt = $this->pdo->prepare($query);
    $stmt->bindValue(':value', $value);
    $stmt->execute();
    return $stmt->fetchAll();
}
10. 性能优化

为了提高系统的性能,可以对一些操作进行优化。

10.1 缓存用户信息

在用户登录后,可以将用户信息缓存起来,避免每次操作都从数据库中查询用户信息。例如,在 Authentication 类中添加一个缓存变量:

class Authentication {
    private $users;
    private $usernameColumn;
    private $cachedUser;

    public function __construct($users, $usernameColumn) {
        $this->users = $users;
        $this->usernameColumn = $usernameColumn;
        $this->cachedUser = null;
    }

    public function getUser() {
        if ($this->isLoggedIn()) {
            if ($this->cachedUser === null) {
                $this->cachedUser = $this->users->find($this->usernameColumn, strtolower($_SESSION['username']))[0];
            }
            return $this->cachedUser;
        }
        return false;
    }
}
10.2 数据库查询优化

可以对数据库查询进行优化,例如添加索引、优化查询语句等。在 joke 表的 authorId 列上添加索引,可以加快根据作者 ID 查询笑话的速度:

ALTER TABLE joke ADD INDEX idx_authorId (authorId);
11. 总结与展望

通过以上步骤,我们成功构建了一个功能完善的 PHP 网站登录与权限管理系统,包括登录、注销、权限管理、安全性和性能优化等方面。以下是一个简单的流程图,展示了系统的整体架构:

graph LR;
    A[用户] --> B[登录系统];
    B --> C{是否登录成功};
    C -- 是 --> D[访问页面或执行操作];
    C -- 否 --> E[显示登录错误信息];
    D --> F{是否有操作权限};
    F -- 是 --> G[执行操作];
    F -- 否 --> H[显示无权限信息];
    G --> I[数据库操作];
    I --> J[更新或查询数据];

表格总结系统的主要功能和对应的优化措施:
| 功能 | 优化措施 |
| ---- | ---- |
| 登录与注销 | 密码加密、缓存用户信息 |
| 权限管理 | 封装权限验证逻辑、统一错误处理 |
| 数据库操作 | 防止 SQL 注入、数据库查询优化 |

未来,可以进一步扩展系统的功能,例如添加角色管理、多因素认证等,以提高系统的安全性和灵活性。同时,可以对系统进行性能测试和优化,确保系统在高并发情况下的稳定性和响应速度。

总之,一个完善的登录与权限管理系统是网站安全和用户体验的重要保障,通过合理的设计和优化,可以为用户提供一个安全、便捷的使用环境。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值