ThinkPHP8 + 原生 JS:前端主导的异步表单安全防护实践(防 CSRF / 篡改 / 重放)

前端安全防护实践:防御未授权访问与数据篡改

这些是可以增加校验的额外方法,不使用不影响本节内容,仅作为暂时补充

中间件kong 以及konga安装部署方法

中间件认证的基础插件

加强密钥管理,避免敏感信息泄露

增强数据校验以及数据库存储数据的安全性

Gitea安装部署

Git 安全使用

JumpServer终端审计录像以及回访

Zabbix服务器监控

背景与目标

2025-10-14 12:05:16 星期二
本文基于ThinkPHP8,以及原始Javascript,在传统的表单验证中,或者是说在我以前的时候,使用简单的异步请求,导致前端发送的数据完整性处于未知状态,也就是说从前端离手,到后端接手这个过程中,

  1. 第一时间未知:当前端发送表单数据后,即使过了一个小时?两个小时?由于未作时间校验,我无法对此进行判断
  2. 数据完整性未知:当用户发送完表单数据,是否在中途被篡改,这也是未知的
  3. 重放行为:由于未做ssrf,从前端来看就容易被二次使用,所以这也是未知的

总结,我希望在这份笔记从前后端的交互中解决上述问题,重点在前端,为什么是前端,在整个信息系统与交互的友好性以及Js的灵活性上看,其承担的意义还是十分明显的

  • 动态内容更新,减少冗余交互传统静态页面依赖 “请求 - 刷新” 模式(如早期 PHP、JSP 页面),用户每操作一次(如切换选项、提交表单)都需要重新加载整个页面,体验卡顿且效率低下。JavaScript 通过操作 DOM(文档对象模型),可以在不刷新页面的情况下直接修改局部内容(如动态加载列表、切换标签页、更新数据统计)。例如:电商平台的 “加入购物车” 按钮点击后,无需刷新页面即可更新购物车数量,大幅减少用户等待时间。
  • 即时反馈,降低用户操作成本信息系统的交互效率很大程度上依赖 “操作 - 反馈” 的及时性。JavaScript 支持客户端实时验证(如表单输入校验),用户输入手机号、邮箱时可即时提示格式错误,无需等待服务器响应,减少无效提交;同时支持动态提示(如搜索框的联想建议、输入框的字数统计),引导用户更高效地完成操作。
  • 异步通信(AJAX),优化数据交互模式借助 XMLHttpRequest 或 Fetch API,JavaScript 实现了客户端与服务器的异步数据交换:用户操作时,JS 可在后台悄悄发送 / 获取数据,不阻塞页面其他操作。例如:社交平台的 “下拉加载更多” 功能,用户滑动页面时,JS 异步请求新数据并渲染,既保持页面流畅,又实现数据增量更新,避免一次性加载大量数据导致的性能问题。
  • 数据验证:JavaScript支持复杂的函数以及异步方法,通过这些方法我们可以完成复杂的混淆逻辑以及验证逻辑,同时减少Python 带来的自动化攻击,同时也实现数据校验,保证性能的同时大大提高破解复杂度以及安全性

1. JavaScript事件处理机制 2. Token注入策略以增强访问控制 3. 数据加密与完整性验证 4. 时效性验证

基础异步表单提交分析

初始代码实现

假设我们有一段数据交互逻辑,就将其当作统一认证的表单,这是一段经典的异步交互代码,收集表单数据,传递表单信息,在这个代码之中,我们存在上述的所有可能情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<form id="userForm" class="space-y-4">
<div>
<label for="name" class="form-label">姓名</label>
<input type="text" id="name" name="name" required class="form-input" placeholder="请输入您的姓名">
</div>
<div>
<label for="email" class="form-label">邮箱</label>
<input type="email" id="email" name="email" required class="form-input" placeholder="请输入您的邮箱">
</div>
<div>
<label for="message" class="form-label">留言</label>
<textarea id="message" name="message" rows="3" class="form-input" placeholder="请输入您的留言"></textarea>
</div>
<div>
<button type="submit" id="submitBtn" class="btn-primary w-full flex items-center justify-center">
<span>提交表单</span>
<i class="fa fa-paper-plane ml-2"></i>
</button>
</div>
</form>

<div id="result" class="mt-6 p-4 bg-gray-50 rounded-md hidden">
<h3 class="font-semibold text-gray-800 mb-2">服务器响应:</h3>
<pre id="responseData" class="text-sm text-gray-600 whitespace-pre-wrap"></pre>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const form = document.getElementById('userForm');
const submitBtn = document.getElementById('submitBtn');
const successMessage = document.getElementById('successMessage');
const errorMessage = document.getElementById('errorMessage');
const result = document.getElementById('result');
const responseData = document.getElementById('responseData');

form.addEventListener('submit', async function(e) {
e.preventDefault();

successMessage.classList.add('hidden');
errorMessage.classList.add('hidden');
result.classList.add('hidden');

submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 提交中...';

try {
const formData = new FormData(form);
formData.append('timestamp', new Date().getTime());

const response = await fetch('http://127.0.0.1:8080/Index/login', {
method: 'POST',
body: formData,
});

if (!response.ok) {
throw new Error(`HTTP错误,状态码: ${response.status}`);
}

const data = await response.json();
successMessage.classList.remove('hidden');
result.classList.remove('hidden');
responseData.textContent = JSON.stringify(data, null, 2);

} catch (error) {
console.error('提交失败:', error);
errorMessage.querySelector('span').textContent = `提交失败: ${error.message}`;
errorMessage.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<span>提交表单</span><i class="fa fa-paper-plane ml-2"></i>';
}
});

由于问题存在后端,时效性未校验,未增加token口令以及明文信息的传递导致数据轻易被抓包,被重放被爆破修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public function login()
    {
        $name = Request()->param("name");
        $email  = Request()->param("email");
        $msg = Request()->param("message");


        return Json([
            'success'  => true,
            'code'     => 200,
            'message'  => "数据接收成功",
            "data"=> [
                "name"      => $name,
                "msg"       => $msg
            ]
        ]);
    }

安全风险识别

基础实现存在以下安全隐患:

  1. 中间人攻击:用户敏感数据明文传输
  2. 重放攻击:请求可被截获并重复使用
  3. 数据篡改:请求参数可被恶意修改

明文传输风险

在下面的教程中,我们逐级拆解我们所做的事情,从一级到四级逐级完善。每一个章节都有较为完整的代码以及实现核心和防护效果

1. CSRF Token防护

做csrf 是为了避免重放攻击,每一次的http请求头仅允许第一次有效,减少黑客试错的机会,提高复杂度

后端实现:
app\controller\Index控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
namespace app\controller;

use app\BaseController;
use think\Request;
use think\facade\View;
use think\facade\Json;
use think\exception\ValidateException;

class Index extends BaseController
{
public function index()
{
$token = Request()->buildToken('__token__', 'sha1');
return View::fetch('login/login',[
'__token__' => $token
]);
}

public function login()
{
$name = Request()->param("name");
$email = Request()->param("email");
$msg = Request()->param("message");
$check = Request()->checkToken('__token__', Request()->param());

if(false === $check) {
throw new ValidateException('invalid token');
}

return Json([
'success' => true,
'code' => 200,
'message' => "数据接收成功",
"data"=> [
"name" => $name,
"msg" => $msg
]
]);
}
}

前端集成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        <form id="userForm" class="space-y-4">
            <div>
                <label for="name" class="form-label">姓名</label>
                <input type="text" id="name" name="name" required class="form-input" placeholder="请输入您的姓名">
                <input type="hidden" name="__token__" value="{:token()}" /><!-- 增加csrf token -->
            </div>
            <div>
                <label for="email" class="form-label">邮箱</label>
                <input type="email" id="email" name="email" required class="form-input" placeholder="请输入您的邮箱">
            </div>
            <div>
                <label for="password" class="form-label">密码</label>
                <input type="password" id="password" name="password" required class="form-input" placeholder="请输入您的密码">
            </div>
            <div>
                <button type="submit" id="submitBtn" class="btn-primary w-full flex items-center justify-center">
                    <span>提交表单</span>
                    <i class="fa fa-paper-plane ml-2"></i>
                </button>
            </div>
        </form>

js代码保持不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const form = document.getElementById('userForm');
const submitBtn = document.getElementById('submitBtn');
const successMessage = document.getElementById('successMessage');
const errorMessage = document.getElementById('errorMessage');
const result = document.getElementById('result');
const responseData = document.getElementById('responseData');

form.addEventListener('submit', async function(e) {
e.preventDefault();

successMessage.classList.add('hidden');
errorMessage.classList.add('hidden');
result.classList.add('hidden');

submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 提交中...';

try {
const formData = new FormData(form);
formData.append('timestamp', new Date().getTime());

const response = await fetch('http://127.0.0.1:8080/Index/login', {
method: 'POST',
body: formData,
});

if (!response.ok) {
throw new Error(`HTTP错误,状态码: ${response.status}`);
}

const data = await response.json();
successMessage.classList.remove('hidden');
result.classList.remove('hidden');
responseData.textContent = JSON.stringify(data, null, 2);

} catch (error) {
console.error('提交失败:', error);
errorMessage.querySelector('span').textContent = `提交失败: ${error.message}`;
errorMessage.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<span>提交表单</span><i class="fa fa-paper-plane ml-2"></i>';
}
});

CSRF防护效果

2. 前端数据加密

Base64邮箱编码

  • btoa方法
    为了保证安全与体验的平衡,邮箱由于后面还需要进行多因素认证,所以需要可逆的,这里仅仅选中base64编码方式

表单与后端保持不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/md5.min.js"></script>
<script>
        const form = document.getElementById('userForm');
        const rawEmail = document.getElementById("email").value;
        const base64Email = btoa(rawEmail);
        const submitBtn = document.getElementById('submitBtn');
        const successMessage = document.getElementById('successMessage');
        const errorMessage = document.getElementById('errorMessage');
        const result = document.getElementById('result');
        const responseData = document.getElementById('responseData');


        form.addEventListener('submit', async function(e) {
            e.preventDefault();

            successMessage.classList.add('hidden');
            errorMessage.classList.add('hidden');
            result.classList.add('hidden');

            submitBtn.disabled = true;
            submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 提交中...';

            try {
                const formData = new FormData(form);
                formData.append('en_meil', base64Email);
                formData.delete('email');
                formData.append('timestamp', new Date().getTime());

                const response = await fetch('http://192.168.87.177:8080/Index/login', {
                    method: 'POST',
                    body: formData
                });

                if (!response.ok) {
                    throw new Error(`HTTP错误,状态码: ${response.status}`);
                }

                const data = await response.json();

                successMessage.classList.remove('hidden');
                result.classList.remove('hidden');
                responseData.textContent = JSON.stringify(data, null, 2);

            } catch (error) {
                console.error('提交失败:', error);
                errorMessage.querySelector('span').textContent = `提交失败: ${error.message}`;
                errorMessage.classList.remove('hidden');
            } finally {
                submitBtn.disabled = false;
                submitBtn.innerHTML = '<span>提交表单</span><i class="fa fa-paper-plane ml-2"></i>';
            }
        });
    </script>

Base64编码效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/md5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/sha256.min.js"></script>
 <script>
        const form = document.getElementById('userForm');
        const rawEmail = document.getElementById("email").value;
        const base64Email = btoa(rawEmail);
        const password = document.getElementById('password').value;
        const en_password = CryptoJS.MD5(password).toString();
        const submitBtn = document.getElementById('submitBtn');
        const successMessage = document.getElementById('successMessage');
        const errorMessage = document.getElementById('errorMessage');
        const result = document.getElementById('result');
        const responseData = document.getElementById('responseData');


        form.addEventListener('submit', async function(e) {
            e.preventDefault();

            successMessage.classList.add('hidden');
            errorMessage.classList.add('hidden');
            result.classList.add('hidden');

            submitBtn.disabled = true;
            submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 提交中...';

            try {
                const formData = new FormData(form);
                formData.append('en_meil', base64Email);
                formData.delete('email');
                formData.append('en_password',en_password)
                formData.append('timestamp', new Date().getTime());

                const response = await fetch('http://192.168.87.177:8080/Index/login', {
                    method: 'POST',
                    body: formData
                });

                if (!response.ok) {
                    throw new Error(`HTTP错误,状态码: ${response.status}`);
                }

                const data = await response.json();

                successMessage.classList.remove('hidden');
                result.classList.remove('hidden');
                responseData.textContent = JSON.stringify(data, null, 2);

            } catch (error) {
                console.error('提交失败:', error);
                errorMessage.querySelector('span').textContent = `提交失败: ${error.message}`;
                errorMessage.classList.remove('hidden');
            } finally {
                submitBtn.disabled = false;
                submitBtn.innerHTML = '<span>提交表单</span><i class="fa fa-paper-plane ml-2"></i>';
            }
        });
    </script>

防护三:hash256 数据校验,时效性验证,动态口令验证,数据格式验证

在这里,我设在前端设置两个方法

  • getHashSalt():从后端fetch 一段随机高强度的selt加盐(MD5的timestamp格式)
  • calculateHash(text):text参数拼接编码后的邮箱密码以及用户名进行sha256的hash计算,保证数据完整性

在此,我给出完整可直接执行的代码

完整的前端安全实现:

完整的前端代码 /app/view/login/login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>异步表单提交示例</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/md5.min.js"></script>  
    <script src="https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/sha256.min.js"></script>


    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3b82f6',
                        success: '#10b981',
                        error: '#ef4444',
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .form-input {
                @apply w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-200;
            }
            .form-label {
                @apply block text-sm font-medium text-gray-700 mb-1;
            }
            .btn-primary {
                @apply bg-primary text-white px-4 py-2 rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary/50 transition duration-200;
            }
            .message {
                @apply p-3 rounded-md mb-4 text-sm font-medium hidden;
            }
        }
    </style>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center p-4">
    <div class="w-full max-w-md bg-white rounded-lg shadow-md p-6">
        <h2 class="text-2xl font-bold text-center text-gray-800 mb-6">用户注册</h2>
       
        <div id="successMessage" class="message bg-success/10 text-success border border-success/30">
            <i class="fa fa-check-circle mr-2"></i><span>提交成功!</span>
        </div>
        <div id="errorMessage" class="message bg-error/10 text-error border border-error/30">
            <i class="fa fa-exclamation-circle mr-2"></i><span>提交失败,请重试!</span>
        </div>
       
        <form id="userForm" class="space-y-4">
            <div>
                <label for="name" class="form-label">姓名</label>
                <input type="text" id="name" name="name" required class="form-input" placeholder="请输入您的姓名">
                <input type="hidden" name="__token__" value="{:token()}" />
            </div>
           
            <div>
                <label for="email" class="form-label">邮箱</label>
                <input type="email" id="email" name="email" required class="form-input" placeholder="请输入您的邮箱">
            </div>
            <div>
                <label for="password" class="form-label">密码</label>
                <input type="password" id="password" name="password" required class="form-input" placeholder="请输入您的密码">
            </div>
           
            <div>
                <button type="submit" id="submitBtn" class="btn-primary w-full flex items-center justify-center">
                    <span>提交表单</span>
                    <i class="fa fa-paper-plane ml-2"></i>
                </button>
            </div>
        </form>
        <div id="result" class="mt-6 p-4 bg-gray-50 rounded-md hidden">
            <h3 class="font-semibold text-gray-800 mb-2">服务器响应:</h3>
            <pre id="responseData" class="text-sm text-gray-600 whitespace-pre-wrap"></pre>
        </div>
    </div>
    <script>
        async function getHashSalt() {
          const response = await fetch('http://192.168.87.177:8080/Index/getsalt');
          const data = await response.json();
          if (!data.success) throw new Error('获取盐值失败');
          const salt = data.data?.salt;
          console.log(salt);
          return salt;
        }
        async function calculateHash(text) {
          const salt = await getHashSalt();
          const textWithSalt = text + salt;
          const integrityHash = CryptoJS.SHA256(textWithSalt).toString();
          return { text, integrityHash, salt };
        }

        const form = document.getElementById('userForm');
        const submitBtn = document.getElementById('submitBtn');
        const successMessage = document.getElementById('successMessage');
        const errorMessage = document.getElementById('errorMessage');
        const result = document.getElementById('result');
        const responseData = document.getElementById('responseData');

        form.addEventListener('submit', async function(e) {
          e.preventDefault();

          successMessage.classList.add('hidden');
          errorMessage.classList.add('hidden');
          result.classList.add('hidden');
          submitBtn.disabled = true;
          submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> 提交中...';

          try {
            const name = document.getElementById('name').value;
            const rawEmail = document.getElementById('email').value;
            const rawPassword = document.getElementById('password').value;
            const token = document.querySelector('input[name="__token__"]').value;

            const base64Email = btoa(rawEmail);
            const en_password = CryptoJS.MD5(rawPassword).toString();

            const coreDataText = `${name}|${base64Email}|${en_password}`;

            const { integrityHash, salt } = await calculateHash(coreDataText);
            const formData = new FormData();
            formData.append('name', name);
            formData.append('en_meil', base64Email);
            formData.append('en_password', en_password);
            formData.append('__token__', token);
            formData.append('timestamp', new Date().getTime());

            formData.append('integrity_hash', integrityHash);
            formData.append('salt', salt);

            const response = await fetch('http://192.168.87.177:8080/Index/login', {
              method: 'POST',
              body: formData
            });
   
            if (!response.ok) {
              throw new Error(`HTTP错误,状态码: ${response.status}`);
            }

            const data = await response.json();
            successMessage.classList.remove('hidden');
            result.classList.remove('hidden');
            responseData.textContent = JSON.stringify(data, null, 2);
   
          } catch (error) {
            console.error('提交失败:', error);
            errorMessage.querySelector('span').textContent = `提交失败: ${error.message}`;
            errorMessage.classList.remove('hidden');
          } finally {
            submitBtn.disabled = false;
            submitBtn.innerHTML = '<span>提交表单</span><i class="fa fa-paper-plane ml-2"></i>';
          }
        });
      </script>
    </body>
    </html>
</body>
</html>

完整的控制器

  • app\controller\Index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<?php
namespace app\controller;

use think\Request;
use think\facade\View;
use think\facade\Json;
use think\exception\ValidateException;
use app\validate\User;
use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        $token = Request()->buildToken('__token__', 'sha1');
        return View::fetch('login/login',[
            '__token__'     => $token
        ]);
    }
    /**
     * 完整性校验私有方法(修复参数顺序、返回值、逻辑完整性)
     * @param string $text 核心拼接文本(前端相同规则)
     * @param string $hashMSG 前端传递的完整性哈希(integrity_hash)
     * @param string $salt 前端传递的盐值(从后端 getsalt 接口获取)
     * @param int $timestamp 前端传递的时间戳(毫秒)
     * @return array
     */
    private function calculationtext(string $text, string $hashMSG, string $salt, $timestamp):array
    {
        $timestamp = (int)$timestamp;
        $currentTime = time() * 1000;
        $allowTimeout = 60000;
        if ($currentTime - $timestamp > $allowTimeout) {
            return [
                'success' => false,
                'code' => 403,
                'message' => "密文过期(请求超时)",
                'data' => []
            ];
        }
        $backendHash = hash('sha256', $text . $salt);
        if ($backendHash === $hashMSG) {
            return [
                'success' => true,
                'code' => 200,
                'message' => "验证正确",
                'data' => []
            ];
        } else {
            return [
                'success' => false,
                'code' => 403,
                'message' => "数据完整性验证失败(可能被篡改)",
                'data' => []
            ];
        }
    }

    /**
     * 登录接口
     * @return Json
     */
    public function login()
    {
        try {
            $name = Request()->param("name");
            $en_meil = Request()->param("en_meil");
            #$token = Request()->param('__token__');
            $en_password = Request()->param("en_password");
            $integrity_hash = Request()->param("integrity_hash");
            $salt = Request()->param("salt");
            $getTextTime = Request()->param("timestamp");
            $checkValidData = [
                'name'      => $name,
                'email'     => base64_decode($en_meil),
                'password'  => $en_password
            ];
            $validate = new \app\validate\User;
            $result = $validate->check($checkValidData);
            if(!$result){
                return $validate->getError();
            }
            $requiredParams = ['name', 'en_meil', 'en_password', 'integrity_hash', 'salt', 'timestamp'];
            foreach ($requiredParams as $param) {
                if (empty(Request()->param($param))) {
                    throw new ValidateException("参数缺失:{$param}");
                }
            }

            $checkToken = Request()->checkToken('__token__');
            if (!$checkToken) {
                throw new ValidateException('CSRF令牌无效或已过期,请刷新页面');
            }

            $text = "{$name}|{$en_meil}|{$en_password}";
            $calcutext = $this->calculationtext($text, $integrity_hash, $salt, $getTextTime);
            if ($calcutext['success']) {
                return json([
                    'success' => true,
                    'code' => 200,
                    'message' => "数据接收成功,完整性验证通过",
                    'data' => [
                        "name" => $name,
                        "en_meil" => $en_meil,
                        "en_password" => $en_password,
                        "integrity_hash" => $integrity_hash,
                        "salt" => $salt
                    ]
                ]);
            } else {
                return json([
                    'success' => false,
                    'code' => $calcutext['code'],
                    'message' => $calcutext['message'],
                    'data' => [
                        "name" => $name,
                        "en_meil" => $en_meil,
                        "integrity_hash" => $integrity_hash,
                        "salt" => $salt
                    ]
                ]);
            }

        } catch (ValidateException $e) {
          return json([
                'success' => false,
                'code' => 400,
                'message' => $e->getMessage(),
                'data' => []
            ]);
        } catch (\Exception $e) {

            return json([
                'success' => false,
                'code' => 500,
                'message' => "服务器内部错误:" . $e->getMessage(),
                'data' => []
            ]);
        }
    }

    /**
     * 以时间为Timestamp生成md5盐值
     * @return array
     */
    public function getsalt()
    {
        $timeWithSecond = date("Y-m-d H:i:s");
        return json([
            'success' => true,
            'code' => 200,
            'message' => "数据接收成功",
            "data" => [
                "salt" => md5($timeWithSecond)
            ]
        ]);
    }
}

完整的数据校验类
name字段长度限制在15以内,email格式校验,MD5格式password校验

  • app\validate\User.php
1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\validate;
use think\Validate;

class User extends Validate
{
    protected $rule = [
        'name'      => 'require|max:15',
        'email'     => 'email',
        'password'  => 'require| max:32'
    ];
}

完整安全请求

安全防护效果

成功验证

验证成功

CSRF令牌防护

CSRF拦截

到此,本文结束,总结一下

前端安全措施

  1. 用户交互表单:基于Tailwind CSS的响应式表单,包含客户端必填验证
  2. 异步提交机制:使用Fetch API实现无刷新提交,提升用户体验
  3. 数据安全处理
    • 邮箱Base64编码传输
    • 密码MD5哈希处理
    • 动态盐值完整性验证
  4. 状态反馈机制:完整的加载状态、成功/错误提示
  5. CSRF集成:无缝集成后端令牌系统

后端防护体系

  1. CSRF防护:基于ThinkPHP的令牌生成与验证
  2. 动态盐值:时间戳基础的临时盐值生成
  3. 完整性验证:SHA256哈希完整性校验
  4. 请求时效:60秒请求超时机制
  5. 数据验证:严格的数据格式与长度验证
  6. 异常处理:统一的错误响应机制

安全威胁防护矩阵

攻击类型 防护措施 实现机制
CSRF攻击 Token验证 后端生成一次性令牌,前端携带验证
数据篡改 完整性哈希 SHA256(核心数据+盐值)验证
重放攻击 时间戳校验 60秒请求有效期限制
明文泄露 数据编码 Base64邮箱 + MD5密码哈希
参数缺失 必填验证 关键参数空值检测
信息泄露 异常封装 统一错误响应,避免敏感信息暴露

通过这套完整的前后端协同安全方案,有效提升了系统的安全性,一定程度上完善了交互逻辑,当然这还不够,在数据库层面我们仍然需要进行数据校验,确保数据库存储的是完整数据,未经过修改的数据,由于时间原因,项目暂时搁置,本次仍缺失的内容

  • 更安全的数据签证(后端加密,数据库校验)
  • 基于角色的验证(RABC原则)
  • 基于中间件的认证防护(Kong)

如果不了解,可以看看下面的文章

中间件kong 以及konga安装部署方法

中间件认证的基础插件

加强密钥管理,避免敏感信息泄露

增强数据校验以及数据库存储数据的安全性

Gitea安装部署

Git 安全使用

JumpServer终端审计录像以及回访

Zabbix服务器监控