文档说明:该文档以入门Ajax信息交互为主,不建议将此文档所述内容应用于对安全性要求很高的开发项目上面,该文档并未充分考虑登录时所能遇到的安全问题,虽然对于php代码做了预处理语句书写,并且对sql攻击做了预防,但是,实际的应用场景比此文档描述更为复杂,如果你正在做此类应用,请充分考虑以下安全问题:
- SQL 注入问题
- 跨站脚本(XSS)问题
- 密码安全性问题
- 会话管理问题
- 请求拦截问题
一.环境准备
- PHP版本:7.3.3
- 服务器:Apache
- phpMyAdmin版本:5.2.1
二.知识准备
什么是Ajax-(Asynchronous JavaScript and XML)?
相信大家对于Ajax的概念并不陌生,从某种层面上来说:
AJAX = 异步 JavaScript 和 XML
AJAX 并不是新的编程语言,而是一种使用现有标准的新方法,它具体是干什么的呢?它可以保证整个页面在不刷新的情况下与服务器进行数据交换,对需要修改数据的地方进行部分更新,Ajax不依赖于任何的第三方库或者插件,在JavaScript里面就可以使用它。
Ajax的使用与一些属性:
首先,你必须知道浏览器所提供的一个API方法XMLHttpRequest
,因为此方法是Ajax使用的基础,XMLHttpRequest
用于在后台与服务器交换数据。通过创建 XMLHttpRequest
对象,JavaScript 可以向服务器发送请求并接收响应。虽然XMLHttpRequest
名字中包含 XML,但实际上 Ajax 可以用于处理任何类型的数据,而不仅限于 XML。在现代浏览器中也都提供了XMLHttpRequest
方法的支持。
在JavaScript中,你将通过:
new XMLHttpRequest()
方法创建一个XMLHttpRequest
对象,创建成功之后,你将通过:
open(method, url[async, user, password])
方法初始化一个请求,参数包括请求的类型(GET、POST等等)、URL(请求路径)、是否异步发送请求(可选,默认为 true)、用户名(可选,用于基本身份验证)、密码(可选,用于基本身份验证)之后,你将通过:
send([body])
方法发送请求,其中包括请求内容,即请求体。我们把这些信息综合到一起,来写一个简单的例子,功能是:我们向hello.php
发送一个post请求,请求的内容是你好,服务器 ,然后服务器返回一个你好,客户端。首先是前端JavaScript代码:
JavaScript
// 创建 XMLHttpRequest 对象
var xhr = new XMLHttpRequest();
// 设置 POST 请求的 URL
var url = "hello.php";
// 设置 POST 请求的内容
var params = "content=你好,服务器";
// 配置请求
xhr.open("POST", url, true);
// 设置请求头,指定发送的数据类型
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// 处理响应
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
// 服务器响应完成且成功时执行的操作
console.log(xhr.responseText); // 输出服务器返回的内容
}
};
// 发送请求
xhr.send(params);
然后是后端php代码:
php
<?php
// 获取 POST 请求中的内容
$content = $_POST['content'];
// 判断内容是否为 "你好,服务器"
if ($content == "你好,服务器") {
// 返回 "你好,客户端"
echo "你好,客户端";
} else {
// 返回错误信息
echo "请求内容错误";
}
?>
你看,是吧!Ajax非常的简单。
但是有时,Ajax需要用到很多很多的方法来声明一个东西,那就是请求头
请求头都包含了什么?为什么要请求头?
Host:用于指定请求的目标主机和端口号也就是你实际的请求地址。
User-Agent:其中包含了发起请求的用户代理(浏览器、爬虫等)的信息。
Accept:指定客户端可接受的内容类型,通常用于告诉服务器可以返回哪些类型的数据
Accept-Language:指定客户端可接受的语言类型,用于告诉服务器可以返回哪种语言的内容
Accept-Encoding:指定客户端可接受的内容编码方式,用于告诉服务器可以返回哪种编码的内容
Connection:指定是否需要持久连接,或者请求完成后是否关闭连接
Content-Type:指定请求体的数据类型,用于告诉服务器请求中包含的数据的格式,如表单数据、JSON 数据等
Content-Length:指定请求体的长度,用于告诉服务器请求中包含的数据的大小
Authorization:用于进行身份验证,包含了客户端的认证信息,如用户名和密码,通常情况下token就是被它携带着一起发送到服务器端的
Cookie:包含了客户端发送给服务器的 Cookie 信息
Referer:指定了请求的来源页面的 URL
If-Modified-Since:用于条件请求,指定了资源的最后修改时间,服务器将根据该时间判断是否返回资源内容
Cache-Control:指定了请求的缓存控制策略
Pragma:包含了与缓存相关的指令
乍一看这些信息实在是太多了,它们都有什么用?是不是我们需要一个一个的去设置呢?
首先,请求头在请求过程中扮演了重要的角色,它用于控制缓存,描述请求体,传递请求信息等等,它们在客户端(如浏览器)和服务器之间传递元数据信息,描述了请求的各种属性和要求,而这些操作大部分都是为了安全着想,你想,假设没有请求头,只要是前端发送来的数据,服务器都接受,那所有的信息,不论是否重要,大家不就都可以看得到了吗?那这对于许多应用场景来说,肯定是不允许的,你说对吧。
其次,在这些方法中,有许多是浏览器自动分配的,并不需要我们手动去设置它,所以,也不要被这里的信息给吓到。
有了这些知识准备之后,我们正式开始今天的主题Ajax+JavaScript+php+MySQL实现登录
三.登录,注册前端页面
注意:接下来的内容包括了HTML,JavaScript,PHP语言的内容,如果你对这些知识的理解不到位,请查找相应文档!此外,接下来的内容为方法演示,将不包含CSS内容!
首先,在你的页面上应该有两个分区,一个是 登录 的位置,一个是 注册 的位置,登录的位置里面有两个输入框和一个按钮,两个输入框一个用于输入用户名username
,一个用于输入密码password
,按钮则用来提交数据。我们给这几个元素分别取id:
- 用于输入用户名的输入框就叫做:
username
- 用于输入密码的输入框就叫做:
password
- 用于提交的按钮就叫做:
login-button
于是我们便有了下面这些HTML代码:
HTML
<div id="login">
<input id="username">
<input id="password">
<button id="login-button">登录</button>
</div>
之后是注册的位置,注册的位置里面有三个输入框和一个按钮,第一个输入框用来输入用户名register-username
,第二个输入框用来输入密码register-password
,第三个输入框用来确认密码password-again
,按钮则用来提交数据,我们同样也给这几个元素添加id:
- 用于输入用户名的输入框就叫做
register-username
- 用于输入密码的输入框就叫做
register-password
- 用于确认密码的输入框就叫做
password-again
- 用于提交的按钮就叫做
register-button
于是我们便有了下面这些HTML代码:
HTML
<div id="register">
<input id="register-username">
<input id="register-password">
<input id="password-again">
<button id="register-button">注册</button>
</div>
综上所述,我们最终得到的HTML代码一共有以下这些,我们把它放到index.html
里面,就像这样:
HTML
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" id="viewport" content="width=device-width, initial-scale=1">
<title>登录注册</title>
</head>
<div id="login">
<input id="username">
<input id="password">
<button id="login-button">登录</button>
</div>
<div id="register">
<input id="register-username">
<input id="register-password">
<input id="password-again">
<button id="register-button">注册</button>
</div>
<body>
</body>
<script src="ajax.js"></script>
</html>
之后,我们要写一个登录或者注册成功之后的页面,这个页面很简单,就展示一下登录或者注册的用户名是什么吧!然后再包含一个退出登录的按钮,我们把它放到home.html
里面,就像这样:
HTML
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" id="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div>
Hello<div id="username"></div>
</div>
<button id="out">退出登录</button>
</body>
<script src="home.js"></script>
</html>
然后,我们的前端页面就编写完成了!接下来,就是逻辑部分!
四.逻辑部分
我们需要在用户点击登录或者注册按钮的时候执行相应的操作:
登录逻辑: 当用户点击登录按钮login-button
时,获取id为username
和password
输入框的值,并且把它的值以POST的方式发送给login.php
,当login.php
接收到前端的请求后连接数据库,查询是否有这个用户名,如果有就检查这个密码是否正确,如果正确就返回一串json
数据,并且记录token
到数据库,数据结构如下:
json
{
"info": "success",
"token": "由用户id,登录时间和随机6位数组成的token"
}
如果失败就返回:
json
{
"info": "fail"
}
前端的JavaScript接收到服务器返回的数据之后,把json
数据解析,拿到里面的info
,判断是success
还是fail
,如果是success
就把整个json
记录在名字为user
的cookie
里面,并且跳转到home.html
,如果为fail
就弹窗登录失败
注册逻辑: 当用户点击register-button
时,获取password-again
的值,判断是否与register-password
的值一样,如果一样就获取register-username
的值,让password-again
的值和register-username
的值一并发送给register.php
,register.php
连接数据库,将密码哈希加密,把密码,token,连同用户名一同记录在数据库内,如果注册成功,则返回:
json
{
"info": "success",
"token": "由用户id,登录时间和随机6位数组成的token"
}
如果注册失败则返回:
json
{
"info": "fail"
}
注册也一样,前端的JavaScript接收到服务器返回的数据之后,把json
数据解析,拿到里面的info
,判断是success
还是fail
,如果是success
就把整个json
记录在名字为user
的cookie
里面,并且跳转到home.html
,如果为fail
就弹窗注册失败
请注意:这里的token没有规范使用
jwt
加密,仅做演示!
然后就是防止用户直接访问home.html
和登录验证,我们再写一个home.js
用来验证用户状态,它主要实现:当home.html
一加载完成就判断名字为user
的cookie
是否为空,如果为空,就跳转index.html
,如果不为空,就获取cookie
内容,并且解析json
,拿到token
,然后发送一个空请求给user.php
,把token
携带在请求头中,user.php
接收到数据之后获取请求头里面的token
,把里面的id
拿出来,在数据库里面匹配,匹配到数据之后,判断token
是否正确,如果正确就返回:
json
{
"info": "success",
"username": "用户名"
}
给前端,如果不正确,就返回:
json
{
"info": "fail"
}
给前端,前端的JavaScript接收到服务器返回的数据之后,把json
数据解析,拿到里面的info
,判断是success
还是fail
,如果是success
,就把username
解析出来渲染到id为username
的元素上面去,如果是fail
,就跳转index.html
。
五.逻辑实现
根据刚刚的逻辑解释,创建好相应的js
和php
文件:
ajax.js
JavaScript
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('login-button').addEventListener('click', function(event) {
// 获取用户名和密码
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
// 发送POST请求到login.php
var xhr = new XMLHttpRequest();
xhr.open('POST', 'login.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response.info === 'success') {
// 登录成功,设置cookie并跳转到home.html
document.cookie = 'user=' + JSON.stringify(response);
window.location.href = 'home.html';
} else {
// 登录失败,弹窗提示
alert('登录失败');
}
} else {
// 请求失败,弹窗提示
alert('错误请求');
}
}
};
xhr.send('username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password));
});
});
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('register-button').addEventListener('click', function(event) {
// 获取用户名、密码和确认密码
var username = document.getElementById('register-username').value;
var password = document.getElementById('register-password').value;
var confirmPassword = document.getElementById('password-again').value;
// 检查密码和确认密码是否相同
if (password !== confirmPassword) {
alert('密码不一致');
return;
}
// 发送POST请求到register.php
var xhr = new XMLHttpRequest();
xhr.open('POST', 'register.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response.info === 'success') {
// 注册成功,设置cookie并跳转到home.html
document.cookie = 'user=' + JSON.stringify(response);
window.location.href = 'home.html';
} else {
// 注册失败,弹窗提示
alert('注册失败');
}
} else {
// 请求失败,弹窗提示
alert('请求错误');
}
}
};
xhr.send('username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password));
});
});
document.addEventListener('DOMContentLoaded', function() {
// 判断是否存在名为'user'的cookie
var userCookie = getCookie('user');
if (!userCookie) {
// 如果cookie不存在,则跳转到index.html
window.location.href = 'index.html';
} else {
// 如果cookie存在,则发送请求到user.php进行验证
var token = JSON.parse(userCookie).token;
var xhr = new XMLHttpRequest();
xhr.open('GET', 'user.php', true);
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response.info === 'success') {
window.location.href = 'home.js';
} else {
// 验证失败,跳转到index.html
window.location.href = 'index.html';
}
} else {
// 请求失败,跳转到index.html
window.location.href = 'index.html';
}
}
};
xhr.send();
}
});
home.js
JavaScript
document.addEventListener('DOMContentLoaded', function() {
// 判断是否存在名为'user'的cookie
var userCookie = getCookie('user');
if (!userCookie) {
// 如果cookie不存在,则跳转到index.html
window.location.href = 'index.html';
} else {
// 如果cookie存在,则发送请求到user.php进行验证
var token = JSON.parse(userCookie).token;
var xhr = new XMLHttpRequest();
xhr.open('GET', 'user.php', true);
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response.info === 'success') {
// 验证成功,渲染用户名到页面上
document.getElementById('username').innerText = response.username;
} else {
// 验证失败,跳转到index.html
window.location.href = 'index.html';
}
} else {
// 请求失败,跳转到index.html
window.location.href = 'index.html';
}
}
};
xhr.send();
}
});
// 获取cookie的函数
function getCookie(name) {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
if (cookie.startsWith(name + '=')) {
return decodeURIComponent(cookie.substring(name.length + 1));
}
}
return null;
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('out').addEventListener('click', function(event) {
document.cookie = 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
location.reload();
});
});
login.php
php
<?php
// 连接数据库,假设数据库连接信息为$dbHost, $dbUsername, $dbPassword, $dbName
$dbHost = 'localhost';
// 数据库主机名
$dbUsername = 'root';
// 数据库用户名
$dbPassword = '';
// 数据库密码
$dbName = 'user';
// 数据库名称
$conn = new mysqli($dbHost, $dbUsername, $dbPassword, $dbName);
// 检查连接是否成功
if ($conn->connect_error)
{
die("Connection failed: " . $conn->connect_error);
}
// 获取前端发送的用户名和密码
$username = $_POST['username'];
$password = $_POST['password'];
// 查询数据库中是否存在该用户名
$stmt = $conn->prepare("SELECT id, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0)
{
// 用户名存在,验证密码
$row = $result->fetch_assoc();
if (password_verify($password, $row['password']))
{
// 密码验证成功,生成token并记录到数据库
$token = generateToken($row['id']);
$stmt = $conn->prepare("UPDATE users SET token = ? WHERE id = ?");
$stmt->bind_param("si", $token, $row['id']);
$stmt->execute();
// 返回成功信息和token
$response = array('info' => 'success', 'token' => $token);
echo json_encode($response);
}
else
{
// 密码错误,返回失败信息
$response = array('info' => 'fail');
echo json_encode($response);
}
}
else
{
// 用户名不存在,返回失败信息
$response = array('info' => 'fail');
echo json_encode($response);
}
// 生成token的函数
function generateToken($userId)
{
$timestamp = time();
$random = generateRandomString(6);
return md5($userId . $timestamp . $random);
}
// 生成指定长度的随机字符串
function generateRandomString($length)
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0;
$i < $length;
$i++)
{
$randomString .= $characters[rand(0, strlen($characters) - 1)];
}
return $randomString;
}
?>
register.php
php
<?php
// 连接数据库,假设数据库连接信息为$dbHost, $dbUsername, $dbPassword, $dbName
$dbHost = 'localhost';
// 数据库主机名
$dbUsername = 'root';
// 数据库用户名
$dbPassword = '';
// 数据库密码
$dbName = 'user';
// 数据库名称
$conn = new mysqli($dbHost, $dbUsername, $dbPassword, $dbName);
// 检查连接是否成功
if ($conn->connect_error)
{
die("Connection failed: " . $conn->connect_error);
}
// 获取前端发送的用户名和密码
$username = $_POST['username'];
$password = $_POST['password'];
// 检查用户名是否已存在
$stmt = $conn->prepare("SELECT id FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0)
{
// 用户名已存在,返回失败信息
$response = array('info' => 'fail');
echo json_encode($response);
}
else
{
// 用户名不存在,插入新用户信息
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$token = generateToken();
$stmt = $conn->prepare("INSERT INTO users (username, password, token) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $username, $hashedPassword, $token);
$stmt->execute();
// 返回成功信息和token
$response = array('info' => 'success', 'token' => $token);
echo json_encode($response);
}
// 生成token的函数
function generateToken()
{
$userId = uniqid();
// 生成唯一的用户ID
$timestamp = time();
$random = generateRandomString(6);
return md5($userId . $timestamp . $random);
}
// 生成指定长度的随机字符串
function generateRandomString($length)
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0;
$i < $length;
$i++)
{
$randomString .= $characters[rand(0, strlen($characters) - 1)];
}
return $randomString;
}
?>
user.php
php
<?php
// 连接数据库,假设数据库连接信息为$dbHost, $dbUsername, $dbPassword, $dbName
$dbHost = 'localhost';
// 数据库主机名
$dbUsername = 'root';
// 数据库用户名
$dbPassword = '';
// 数据库密码
$dbName = 'user';
// 数据库名称
$conn = new mysqli($dbHost, $dbUsername, $dbPassword, $dbName);
// 检查连接是否成功
if ($conn->connect_error)
{
die("Connection failed: " . $conn->connect_error);
}
// 检查请求头中是否包含Authorization信息
if (!isset($_SERVER['HTTP_AUTHORIZATION']))
{
// 如果没有Authorization信息,则返回验证失败信息
$response = array('info' => 'fail');
echo json_encode($response);
exit();
}
// 从请求头中获取token
$token = substr($_SERVER['HTTP_AUTHORIZATION'], 7);
// 从数据库中查找用户
$stmt = $conn->prepare("SELECT username FROM users WHERE token = ?");
$stmt->bind_param("s", $token);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0)
{
// 用户存在,返回用户名
$row = $result->fetch_assoc();
$username = $row['username'];
$response = array('info' => 'success', 'username' => $username);
echo json_encode($response);
}
else
{
// 用户不存在,返回验证失败信息
$response = array('info' => 'fail');
echo json_encode($response);
}
$conn->close();
?>
数据库sqlusers.sql
:记得创建名字为user的数据库里面创建一个users表或者直接复制:
spl
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- 主机: localhost
-- 生成日期: 2024-04-04 13:29:25
-- 服务器版本: 5.6.38
-- PHP 版本: 7.3.3
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- 数据库: `user`
--
-- --------------------------------------------------------
--
-- 表的结构 `users`
--
CREATE TABLE `users` (
`id` int(255) NOT NULL,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`token` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- 转储表的索引
--
--
-- 表的索引 `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `users`
--
ALTER TABLE `users`
MODIFY `id` int(255) NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
到一个新的sql文件里面,直接导入!
好了,今天的分享就到这里😀