Ajax+JavaScript+php+MySQL实现登录

文档说明:该文档以入门Ajax信息交互为主,不建议将此文档所述内容应用于对安全性要求很高的开发项目上面,该文档并未充分考虑登录时所能遇到的安全问题,虽然对于php代码做了预处理语句书写,并且对sql攻击做了预防,但是,实际的应用场景比此文档描述更为复杂,如果你正在做此类应用,请充分考虑以下安全问题:

  1. SQL 注入问题
  2. 跨站脚本(XSS)问题
  3. 密码安全性问题
  4. 会话管理问题
  5. 请求拦截问题

一.环境准备

  • 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为usernamepassword输入框的值,并且把它的值以POST的方式发送给login.php,当login.php接收到前端的请求后连接数据库,查询是否有这个用户名,如果有就检查这个密码是否正确,如果正确就返回一串json数据,并且记录token到数据库,数据结构如下:

json 复制代码
{
    "info": "success",
    "token": "由用户id,登录时间和随机6位数组成的token"
}

如果失败就返回:

json 复制代码
{
    "info": "fail"
}

前端的JavaScript接收到服务器返回的数据之后,把json数据解析,拿到里面的info,判断是success还是fail,如果是success就把整个json记录在名字为usercookie里面,并且跳转到home.html,如果为fail就弹窗登录失败

注册逻辑: 当用户点击register-button时,获取password-again的值,判断是否与register-password的值一样,如果一样就获取register-username的值,让password-again的值和register-username的值一并发送给register.phpregister.php连接数据库,将密码哈希加密,把密码,token,连同用户名一同记录在数据库内,如果注册成功,则返回:

json 复制代码
{
    "info": "success",
    "token": "由用户id,登录时间和随机6位数组成的token"
}

如果注册失败则返回:

json 复制代码
{
    "info": "fail"
}

注册也一样,前端的JavaScript接收到服务器返回的数据之后,把json数据解析,拿到里面的info,判断是success还是fail,如果是success就把整个json记录在名字为usercookie里面,并且跳转到home.html,如果为fail就弹窗注册失败

请注意:这里的token没有规范使用jwt加密,仅做演示!

然后就是防止用户直接访问home.html和登录验证,我们再写一个home.js用来验证用户状态,它主要实现:当home.html一加载完成就判断名字为usercookie是否为空,如果为空,就跳转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

五.逻辑实现

根据刚刚的逻辑解释,创建好相应的jsphp文件:

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文件里面,直接导入!

好了,今天的分享就到这里😀

相关推荐
Marst Code4 分钟前
(Django)初步使用
后端·python·django
代码之光_198011 分钟前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长23 分钟前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
IT果果日记1 小时前
DataX+Crontab实现多任务顺序定时同步
后端
姜学迁2 小时前
Rust-枚举
开发语言·后端·rust
爱学习的小健3 小时前
MQTT--Java整合EMQX
后端
北极小狐3 小时前
Java vs JavaScript:类型系统的艺术 - 从 Object 到 any,从静态到动态
后端
tangdou3690986553 小时前
两种方案手把手教你多种服务器使用tinyproxy搭建http代理
运维·后端·自动化运维
【D'accumulation】3 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
2401_854391083 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端