Web系统安全综合方案,例如小型校园网站 Web 安全防护系统设计与开发等

생성일: 2026년 5월 25일

답변에 사용된 모델 GPT-5.5 Thinking by Chat01

질문

Web系统安全综合方案,例如小型校园网站 Web 安全防护系统设计与开发等

사고

undefined

사고

Thought for 11s

답변

下面给你一个**“Web 系统安全综合方案”**模板,主题可以定为:

小型校园网站 Web 安全防护系统设计与开发

该选题适合课程设计、毕业设计、实训项目或网络安全综合实践。核心思路是:先开发一个小型校园网站,再围绕它设计身份认证、权限控制、输入校验、攻击拦截、日志审计、告警与后台管理等安全防护功能

OWASP ASVS 可作为安全需求参考,它本身就是面向 Web 应用和 Web 服务的安全控制验证标准;OWASP Top 10 也可作为风险分类依据,目前 OWASP 官网显示最新发布版本为 Top 10 2025。(OWASP Foundation)


一、项目背景

随着校园信息化建设的发展,学校官网、教务通知系统、社团报名系统、校园论坛、失物招领平台等 Web 系统被广泛使用。这类系统通常规模不大,但包含用户登录、信息发布、留言评论、文件上传、后台管理等功能,容易受到 SQL 注入、XSS、CSRF、暴力破解、越权访问、恶意文件上传等攻击。

因此,本项目以“小型校园网站”为对象,设计并实现一套轻量级 Web 安全防护系统,提高网站的安全性、可管理性和可审计性。


二、项目目标

本系统的目标不是做大型商业 WAF,而是实现一个适合教学和小型网站使用的综合安全防护平台,包括:

  1. 实现一个基础校园网站:公告、新闻、留言、用户登录、后台管理等。
  2. 增加身份认证与角色权限控制,区分学生、教师、管理员。
  3. 对常见 Web 攻击进行防护,例如 SQL 注入、XSS、CSRF、恶意上传、暴力登录等。
  4. 记录访问日志、安全事件日志和管理员操作日志。
  5. 提供后台安全管理页面,可查看拦截记录、登录异常、风险 IP、系统配置。
  6. 设计安全测试方案,对防护效果进行验证。

三、总体架构设计

系统可以采用三层架构:

text
用户浏览器 Nginx / HTTPS / 限流 Web 安全防护层 ├── 请求过滤模块 ├── SQL 注入检测模块 ├── XSS 检测模块 ├── CSRF 防护模块 ├── 文件上传检测模块 ├── 登录防暴力破解模块 └── 日志审计模块 校园网站业务系统 ├── 用户模块 ├── 新闻公告模块 ├── 留言评论模块 ├── 文件上传模块 └── 后台管理模块 数据库 / Redis / 日志存储

四、技术选型建议

可以根据你的能力选择一种技术路线:

层级推荐技术
前端Vue / React / HTML + Bootstrap
后端Spring Boot / Django / Flask / Node.js
数据库MySQL / PostgreSQL
缓存与限流Redis
Web 服务器Nginx
日志MySQL 日志表 / Elasticsearch 可选
安全组件Spring Security / Shiro / JWT / Session
测试工具Burp Suite、OWASP ZAP、Postman

如果是毕业设计,推荐组合:

text
Vue + Spring Boot + MySQL + Redis + Nginx

如果想降低开发难度,可以选:

text
HTML + Bootstrap + Flask/Django + SQLite/MySQL

五、核心功能模块设计

1. 用户认证模块

功能包括:

  • 用户注册、登录、退出;
  • 密码加密存储;
  • 登录验证码;
  • 登录失败次数限制;
  • Session 或 JWT 登录状态管理;
  • 管理员、教师、学生三类角色。

OWASP 的认证和会话管理资料强调,认证用于确认用户身份,会话管理用于在多次 HTTP 请求中维持用户状态,因此本系统应把登录、会话、Cookie、安全退出等作为重点设计内容。(OWASP Cheat Sheet Series)

建议实现:

text
连续登录失败 5 次 → 锁定账号 10 分钟 同一 IP 高频登录 → 触发验证码或临时封禁 密码存储 → BCrypt / Argon2 哈希 Cookie 设置 → HttpOnly、Secure、SameSite

2. 权限控制模块

采用 RBAC 角色权限模型:

角色权限
学生浏览公告、留言、提交信息
教师发布课程通知、审核留言
管理员用户管理、内容管理、安全配置、日志查看

权限控制应覆盖:

  • URL 访问权限;
  • 菜单权限;
  • 接口权限;
  • 数据权限;
  • 后台管理权限。

例如:

text
/student/** 学生、教师、管理员可访问 /teacher/** 教师、管理员可访问 /admin/** 仅管理员可访问 /security/** 仅安全管理员可访问

3. SQL 注入防护模块

防护思路:

  • 使用参数化查询;
  • 禁止字符串拼接 SQL;
  • 对请求参数进行危险关键字检测;
  • 对异常 SQL 请求进行日志记录;
  • 对高危 IP 进行限流或封禁。

检测规则示例:

text
' or '1'='1 union select drop table -- /* sleep( benchmark(

实现方式:

text
请求进入系统 读取 URL、参数、请求体 匹配 SQL 注入特征 若命中高危规则:拦截请求 + 记录日志 否则放行到业务系统

4. XSS 防护模块

防护目标是避免用户提交恶意脚本并在页面中执行。

防护措施:

  • 输入过滤;
  • 输出编码;
  • 富文本白名单过滤;
  • 设置安全响应头;
  • 对留言、评论、公告标题等字段重点处理。

常见危险内容:

html
<script>alert(1)</script> <img src=x onerror=alert(1)> <a href="javascript:alert(1)">click</a>

推荐响应头:

http
Content-Security-Policy: default-src 'self' X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: no-referrer

5. CSRF 防护模块

适用于修改数据的操作,例如:

  • 修改密码;
  • 发布公告;
  • 删除留言;
  • 用户管理;
  • 文件上传。

防护措施:

text
表单提交必须携带 CSRF Token Token 与当前 Session 绑定 后端校验 Token 是否正确 敏感操作要求重新认证或二次确认

6. 文件上传安全模块

校园网站通常会有头像上传、通知附件上传、作业材料上传等功能。该模块可以作为设计亮点。

防护措施:

风险防护方式
上传木马脚本限制扩展名、校验 MIME 类型
文件名绕过使用随机文件名保存
超大文件攻击限制文件大小
路径穿越禁止 ../ 等路径字符
脚本执行上传目录禁止执行权限
恶意图片可选接入杀毒或内容检测

上传流程:

text
用户上传文件 检查文件大小 检查扩展名白名单 检查 MIME 类型 重命名文件 保存到非执行目录 记录上传日志

7. 登录防暴力破解模块

设计规则:

text
同一账号连续失败 5 次 → 锁定 10 分钟 同一 IP 1 分钟内失败超过 20 次 → 加入风险 IP 高风险 IP 登录 → 强制验证码 管理员登录失败 → 记录高危安全事件

可以结合 Redis 实现计数:

text
login_fail:user:1001 login_fail:ip:192.168.1.10

8. 安全日志审计模块

日志是本系统的重要组成部分。建议至少记录三类日志:

访问日志

text
访问时间、IP、URL、请求方法、状态码、User-Agent

安全事件日志

text
攻击类型、风险等级、请求参数、拦截原因、来源 IP

操作审计日志

text
管理员账号、操作类型、操作对象、操作时间、操作结果

安全事件等级可以分为:

等级示例
低危普通异常参数
中危XSS 尝试、频繁登录失败
高危SQL 注入、后台越权访问
严重文件上传木马、管理员账号暴力破解

9. 安全管理后台

后台页面建议包括:

  1. 安全总览面板
    显示今日访问量、拦截次数、风险 IP 数量、登录失败次数。

  2. 攻击拦截记录
    查看 SQL 注入、XSS、CSRF、恶意上传等记录。

  3. 用户登录审计
    查看异常登录、失败登录、账号锁定情况。

  4. 风险 IP 管理
    支持手动拉黑、解除拉黑、查看来源。

  5. 安全规则配置
    配置关键字规则、限流阈值、文件上传白名单。

  6. 管理员操作日志
    审计后台管理行为。


六、数据库表设计示例

用户表 sys_user

字段说明
id用户 ID
username用户名
password_hash密码哈希
role角色
status状态
login_fail_count登录失败次数
locked_until锁定截止时间
create_time创建时间

安全日志表 security_log

字段说明
id日志 ID
event_type事件类型
risk_level风险等级
source_ip来源 IP
request_url请求地址
request_method请求方法
payload请求参数
action处理方式:放行/拦截
create_time发生时间

IP 黑名单表 ip_blacklist

字段说明
idID
ip_addressIP 地址
reason拉黑原因
expire_time过期时间
create_time创建时间

操作审计表 operation_log

字段说明
idID
user_id操作用户
operation操作内容
target操作对象
ip_address操作 IP
create_time操作时间

七、系统安全策略设计

可以把安全策略分为 5 层:

层级防护内容
网络层HTTPS、Nginx 限流、IP 黑名单
接入层参数过滤、攻击规则匹配
认证层密码加密、验证码、账号锁定
权限层RBAC、接口鉴权、越权检测
审计层日志记录、风险分析、告警展示

八、测试方案

功能测试

测试项预期结果
学生登录登录成功并进入学生首页
管理员登录进入后台管理页面
普通用户访问后台被拒绝
发布公告管理员可发布
上传合法文件上传成功
上传 .jsp/.php/.exe被拒绝

安全测试

攻击类型测试输入预期结果
SQL 注入' or '1'='1请求被拦截
XSS<script>alert(1)</script>内容被过滤或编码
CSRF不带 Token 提交表单请求失败
暴力破解连续错误登录账号锁定
越权访问学生访问 /admin返回 403
文件上传上传脚本文件上传失败

测试时应仅在本地靶场或授权环境中进行,不应对真实网站发起攻击测试。


九、创新点或亮点

可以在论文或答辩中突出这些亮点:

  1. 轻量级 WAF 思想
    在业务系统前增加统一请求过滤层,对恶意请求进行识别和拦截。

  2. 安全日志可视化
    后台展示攻击趋势、风险 IP、攻击类型分布。

  3. RBAC 权限控制
    将用户角色、接口权限和后台菜单结合。

  4. 登录风险控制
    支持验证码、账号锁定、IP 限流。

  5. 可配置安全规则
    管理员可以维护 SQL 注入、XSS、恶意文件类型等规则。

  6. 安全测试闭环
    从风险分析、系统设计、代码实现到安全测试形成完整流程。


十、论文/报告目录建议

可以直接采用下面这个目录:

text
第 1 章 绪论 1.1 研究背景与意义 1.2 国内外研究现状 1.3 主要研究内容 1.4 论文组织结构 第 2 章 相关技术介绍 2.1 Web 应用安全概述 2.2 常见 Web 攻击类型 2.3 Spring Boot / Django 技术介绍 2.4 RBAC 权限模型 2.5 日志审计与安全防护技术 第 3 章 系统需求分析 3.1 功能需求分析 3.2 安全需求分析 3.3 性能需求分析 3.4 可行性分析 第 4 章 系统总体设计 4.1 系统架构设计 4.2 功能模块设计 4.3 安全防护流程设计 4.4 数据库设计 第 5 章 系统详细设计与实现 5.1 用户认证模块实现 5.2 权限控制模块实现 5.3 SQL 注入防护模块实现 5.4 XSS 防护模块实现 5.5 CSRF 防护模块实现 5.6 文件上传安全模块实现 5.7 日志审计模块实现 5.8 安全管理后台实现 第 6 章 系统测试 6.1 测试环境 6.2 功能测试 6.3 安全测试 6.4 测试结果分析 第 7 章 总结与展望 7.1 工作总结 7.2 不足之处 7.3 后续改进方向

十一、可选题目变体

你也可以把题目改得更正式一些:

  1. 基于 Spring Boot 的小型校园网站 Web 安全防护系统设计与实现
  2. 面向校园信息系统的 Web 安全防护平台设计与开发
  3. 基于 RBAC 与日志审计的校园网站安全防护系统设计
  4. 轻量级 Web 应用安全防护系统的设计与实现
  5. 基于 OWASP 风险模型的校园网站安全防护系统设计
  6. 小型 Web 应用安全监测与防护系统设计与实现

十二、推荐最终项目定位

最适合落地的版本是:

基于 Spring Boot + Vue 的小型校园网站 Web 安全防护系统设计与实现

主要实现:

text
校园网站业务功能 + 用户登录认证 + RBAC 权限控制 + SQL 注入/XSS/CSRF 防护 + 文件上传安全检测 + 登录防暴力破解 + IP 黑名单 + 安全日志审计 + 后台可视化管理

这个范围比较完整,既有 Web 开发内容,也有网络安全特色,适合作为综合性项目。

질문

请根据上述写出代码

사고

我先直接生成一个项目压缩包,里面会包含后端主要源码、前端页面、数据库初始化脚本和运行说明。代码偏“教学/毕设原型”,便于你继续扩展成完整系统。

Thought for 19s

Python

python
import os, zipfile, textwrap, json, pathlib, shutil base = "/mnt/data/campus-web-security" if os.path.exists(base): shutil.rmtree(base) def write(path, content): full = os.path.join(base, path) os.makedirs(os.path.dirname(full), exist_ok=True) with open(full, "w", encoding="utf-8") as f: f.write(textwrap.dedent(content).lstrip()) # Root files write("README.md", r""" # 小型校园网站 Web 安全防护系统 技术栈: - 后端:Spring Boot 3 + Spring Security + Spring Data JPA + JWT - 前端:Vue 3 + Vite + Axios - 数据库:默认 H2,生产可切换 MySQL - 安全模块:登录认证、RBAC、SQL 注入/XSS 检测、CSRF Token、防暴力破解、IP 黑名单、文件上传校验、安全日志审计 ## 一、运行后端 进入 backend 目录: ```bash cd backend mvn spring-boot:run

默认地址:

text
http://localhost:8080

H2 控制台:

text
http://localhost:8080/h2-console JDBC URL: jdbc:h2:file:./data/campus_security 用户名: sa 密码: 123456

默认管理员账号:

text
admin / Admin@123456

二、运行前端

进入 frontend 目录:

bash
cd frontend npm install npm run dev

默认地址:

text
http://localhost:5173

三、MySQL 配置

后端默认使用 H2。如果要切换 MySQL,修改:

text
backend/src/main/resources/application.yml

把 datasource 改成:

yaml
spring: datasource: url: jdbc:mysql://localhost:3306/campus_security?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver jpa: database-platform: org.hibernate.dialect.MySQLDialect

并创建数据库:

sql
CREATE DATABASE campus_security DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

四、已实现的接口

认证

  • POST /api/auth/login
  • POST /api/auth/register
  • GET /api/auth/me

校园内容

  • GET /api/notices
  • POST /api/notices,需要教师或管理员
  • DELETE /api/notices/{id},需要管理员
  • GET /api/comments
  • POST /api/comments,需要登录

文件上传

  • POST /api/files/upload,需要登录

安全后台

  • GET /api/admin/security/logs,需要管理员
  • GET /api/admin/security/dashboard,需要管理员
  • POST /api/admin/security/blacklist,需要管理员
  • DELETE /api/admin/security/blacklist/{ip},需要管理员

五、安全请求说明

登录成功后,前端会保存:

text
Authorization: Bearer <jwt> X-CSRF-Token: <csrfToken>

除登录、注册、查询外,POST/PUT/PATCH/DELETE 请求都需要携带 CSRF Token。

六、说明

这是一个适合课程设计/毕业设计的原型项目,重点展示 Web 安全防护设计思路。正式上线前还需要补充:

  • HTTPS 证书
  • 更严格的密码策略
  • Redis 分布式计数
  • Nginx 限流
  • 完整审计报表
  • 单元测试和安全测试报告 """)

write("database/mysql-init.sql", r""" CREATE DATABASE IF NOT EXISTS campus_security DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE campus_security;

-- Spring Data JPA 会自动建表,本文件仅用于创建数据库。 """)

Backend files

write("backend/pom.xml", r""" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>

text
<groupId>com.campus</groupId> <artifactId>campus-security</artifactId> <version>1.0.0</version> <name>campus-security</name> <description>小型校园网站 Web 安全防护系统</description> <properties> <java.version>17</java.version> <spring.boot.version>3.3.4</spring.boot.version> <jjwt.version>0.12.5</jjwt.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.owasp.encoder</groupId> <artifactId>encoder</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.18.1</version> </dependency> <dependency> <groupId>com.googlecode.owasp-java-html-sanitizer</groupId> <artifactId>owasp-java-html-sanitizer</artifactId> <version>20240325.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> </plugin> </plugins> </build>
</project> """)

write("backend/src/main/resources/application.yml", r""" server: port: 8080

spring: application: name: campus-security datasource: url: jdbc:h2:file:./data/campus_security username: sa password: 123456 driver-class-name: org.h2.Driver h2: console: enabled: true path: /h2-console jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false database-platform: org.hibernate.dialect.H2Dialect servlet: multipart: max-file-size: 5MB max-request-size: 8MB

app: jwt: # 毕设演示用。正式环境请使用环境变量注入,并保证长度和随机性。 secret: "campus-security-demo-secret-key-campus-security-demo-secret-key" expiration-minutes: 240 upload: dir: "./uploads" allowed-extensions: "jpg,jpeg,png,pdf,doc,docx,xls,xlsx,txt" security: login-max-failures: 5 login-lock-minutes: 10 attack-block-enabled: true """)

write("backend/src/main/java/com/campus/security/CampusSecurityApplication.java", r""" package com.campus.security;

import com.campus.security.entity.User; import com.campus.security.repository.UserRepository; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.crypto.password.PasswordEncoder;

@SpringBootApplication public class CampusSecurityApplication { public static void main(String[] args) { SpringApplication.run(CampusSecurityApplication.class, args); }

text
@Bean CommandLineRunner initAdmin(UserRepository userRepository, PasswordEncoder passwordEncoder) { return args -> { if (userRepository.findByUsername("admin").isEmpty()) { User admin = new User(); admin.setUsername("admin"); admin.setPasswordHash(passwordEncoder.encode("Admin@123456")); admin.setRole("ADMIN"); admin.setStatus("NORMAL"); userRepository.save(admin); } }; }

} """)

Common response

write("backend/src/main/java/com/campus/security/common/ApiResponse.java", r""" package com.campus.security.common;

public class ApiResponse<T> { private int code; private String message; private T data;

text
public ApiResponse() {} public ApiResponse(int code, String message, T data) { this.code = code; this.message = message; this.data = data; } public static <T> ApiResponse<T> ok(T data) { return new ApiResponse<>(200, "success", data); } public static <T> ApiResponse<T> fail(String message) { return new ApiResponse<>(400, message, null); } public static <T> ApiResponse<T> forbidden(String message) { return new ApiResponse<>(403, message, null); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; }

} """)

write("backend/src/main/java/com/campus/security/common/GlobalExceptionHandler.java", r""" package com.campus.security.common;

import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.*;

@RestControllerAdvice public class GlobalExceptionHandler {

text
@ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ApiResponse<Void> accessDenied(AccessDeniedException e) { return ApiResponse.forbidden("权限不足"); } @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResponse<Void> badRequest(IllegalArgumentException e) { return ApiResponse.fail(e.getMessage()); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse<Void> serverError(Exception e) { return new ApiResponse<>(500, "服务器异常:" + e.getMessage(), null); }

} """)

Entities

write("backend/src/main/java/com/campus/security/entity/User.java", r""" package com.campus.security.entity;

import jakarta.persistence.*; import java.time.LocalDateTime;

@Entity @Table(name = "sys_user", indexes = { @Index(name = "idx_user_username", columnList = "username", unique = true) }) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

text
@Column(nullable=false, unique=true, length=64) private String username; @Column(nullable=false, length=255) private String passwordHash; @Column(nullable=false, length=32) private String role = "STUDENT"; @Column(nullable=false, length=32) private String status = "NORMAL"; private int loginFailCount = 0; private LocalDateTime lockedUntil; private LocalDateTime createTime = LocalDateTime.now(); public boolean isLocked() { return lockedUntil != null && lockedUntil.isAfter(LocalDateTime.now()); } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPasswordHash() { return passwordHash; } public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public int getLoginFailCount() { return loginFailCount; } public void setLoginFailCount(int loginFailCount) { this.loginFailCount = loginFailCount; } public LocalDateTime getLockedUntil() { return lockedUntil; } public void setLockedUntil(LocalDateTime lockedUntil) { this.lockedUntil = lockedUntil; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

} """)

write("backend/src/main/java/com/campus/security/entity/SecurityLog.java", r""" package com.campus.security.entity;

import jakarta.persistence.*; import java.time.LocalDateTime;

@Entity @Table(name = "security_log", indexes = { @Index(name = "idx_security_log_type", columnList = "eventType"), @Index(name = "idx_security_log_ip", columnList = "sourceIp"), @Index(name = "idx_security_log_time", columnList = "createTime") }) public class SecurityLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

text
private String eventType; private String riskLevel; private String sourceIp; @Column(length = 1024) private String requestUrl; private String requestMethod; @Column(length = 4000) private String payload; private String action; @Column(length = 1000) private String reason; private LocalDateTime createTime = LocalDateTime.now(); public static SecurityLog of(String eventType, String riskLevel, String ip, String url, String method, String payload, String action, String reason) { SecurityLog log = new SecurityLog(); log.setEventType(eventType); log.setRiskLevel(riskLevel); log.setSourceIp(ip); log.setRequestUrl(url); log.setRequestMethod(method); log.setPayload(payload); log.setAction(action); log.setReason(reason); return log; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getEventType() { return eventType; } public void setEventType(String eventType) { this.eventType = eventType; } public String getRiskLevel() { return riskLevel; } public void setRiskLevel(String riskLevel) { this.riskLevel = riskLevel; } public String getSourceIp() { return sourceIp; } public void setSourceIp(String sourceIp) { this.sourceIp = sourceIp; } public String getRequestUrl() { return requestUrl; } public void setRequestUrl(String requestUrl) { this.requestUrl = requestUrl; } public String getRequestMethod() { return requestMethod; } public void setRequestMethod(String requestMethod) { this.requestMethod = requestMethod; } public String getPayload() { return payload; } public void setPayload(String payload) { this.payload = payload; } public String getAction() { return action; } public void setAction(String action) { this.action = action; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

} """)

write("backend/src/main/java/com/campus/security/entity/IpBlacklist.java", r""" package com.campus.security.entity;

import jakarta.persistence.*; import java.time.LocalDateTime;

@Entity @Table(name = "ip_blacklist", indexes = { @Index(name = "idx_blacklist_ip", columnList = "ipAddress", unique = true) }) public class IpBlacklist { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

text
@Column(nullable=false, unique=true, length=64) private String ipAddress; @Column(length=500) private String reason; private LocalDateTime expireTime; private LocalDateTime createTime = LocalDateTime.now(); public boolean isActive() { return expireTime == null || expireTime.isAfter(LocalDateTime.now()); } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getIpAddress() { return ipAddress; } public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public LocalDateTime getExpireTime() { return expireTime; } public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

} """)

write("backend/src/main/java/com/campus/security/entity/Notice.java", r""" package com.campus.security.entity;

import jakarta.persistence.*; import java.time.LocalDateTime;

@Entity @Table(name = "campus_notice") public class Notice { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

text
@Column(nullable=false, length=150) private String title; @Column(nullable=false, length=4000) private String content; private String author; private LocalDateTime createTime = LocalDateTime.now(); public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

} """)

write("backend/src/main/java/com/campus/security/entity/Comment.java", r""" package com.campus.security.entity;

import jakarta.persistence.*; import java.time.LocalDateTime;

@Entity @Table(name = "campus_comment") public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

text
@Column(nullable=false, length=1000) private String content; private String author; private LocalDateTime createTime = LocalDateTime.now(); public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

} """)

write("backend/src/main/java/com/campus/security/entity/OperationLog.java", r""" package com.campus.security.entity;

import jakarta.persistence.*; import java.time.LocalDateTime;

@Entity @Table(name = "operation_log") public class OperationLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;

text
private String username; private String operation; private String target; private String ipAddress; private LocalDateTime createTime = LocalDateTime.now(); public static OperationLog of(String username, String operation, String target, String ipAddress) { OperationLog log = new OperationLog(); log.username = username; log.operation = operation; log.target = target; log.ipAddress = ipAddress; return log; } public Long getId() { return id; } public String getUsername() { return username; } public String getOperation() { return operation; } public String getTarget() { return target; } public String getIpAddress() { return ipAddress; } public LocalDateTime getCreateTime() { return createTime; }

} """)

Repositories

for name, body in { "UserRepository.java": """ package com.campus.security.repository;

import com.campus.security.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); boolean existsByUsername(String username); } """, "SecurityLogRepository.java": """ package com.campus.security.repository;

import com.campus.security.entity.SecurityLog; import org.springframework.data.jpa.repository.JpaRepository; import java.time.LocalDateTime; import java.util.List;

public interface SecurityLogRepository extends JpaRepository<SecurityLog, Long> { long countByCreateTimeAfter(LocalDateTime time); long countByEventTypeAndCreateTimeAfter(String eventType, LocalDateTime time); List<SecurityLog> findTop100ByOrderByCreateTimeDesc(); } """, "IpBlacklistRepository.java": """ package com.campus.security.repository;

import com.campus.security.entity.IpBlacklist; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional;

public interface IpBlacklistRepository extends JpaRepository<IpBlacklist, Long> { Optional<IpBlacklist> findByIpAddress(String ipAddress); void deleteByIpAddress(String ipAddress); } """, "NoticeRepository.java": """ package com.campus.security.repository;

import com.campus.security.entity.Notice; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List;

public interface NoticeRepository extends JpaRepository<Notice, Long> { List<Notice> findTop50ByOrderByCreateTimeDesc(); } """, "CommentRepository.java": """ package com.campus.security.repository;

import com.campus.security.entity.Comment; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> { List<Comment> findTop100ByOrderByCreateTimeDesc(); } """, "OperationLogRepository.java": """ package com.campus.security.repository;

import com.campus.security.entity.OperationLog; import org.springframework.data.jpa.repository.JpaRepository;

public interface OperationLogRepository extends JpaRepository<OperationLog, Long> { } """ }.items(): write(f"backend/src/main/java/com/campus/security/repository/{name}", body)

DTOs

write("backend/src/main/java/com/campus/security/dto/LoginRequest.java", r""" package com.campus.security.dto;

public class LoginRequest { private String username; private String password;

text
public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }

} """)

write("backend/src/main/java/com/campus/security/dto/RegisterRequest.java", r""" package com.campus.security.dto;

public class RegisterRequest { private String username; private String password; private String role;

text
public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRole() { return role; } public void setRole(String role) { this.role = role; }

} """)

write("backend/src/main/java/com/campus/security/dto/LoginResponse.java", r""" package com.campus.security.dto;

public class LoginResponse { private String token; private String csrfToken; private String username; private String role;

text
public LoginResponse(String token, String csrfToken, String username, String role) { this.token = token; this.csrfToken = csrfToken; this.username = username; this.role = role; } public String getToken() { return token; } public String getCsrfToken() { return csrfToken; } public String getUsername() { return username; } public String getRole() { return role; }

} """)

write("backend/src/main/java/com/campus/security/dto/NoticeRequest.java", r""" package com.campus.security.dto;

public class NoticeRequest { private String title; private String content;

text
public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; }

} """)

write("backend/src/main/java/com/campus/security/dto/CommentRequest.java", r""" package com.campus.security.dto;

public class CommentRequest { private String content;

text
public String getContent() { return content; } public void setContent(String content) { this.content = content; }

} """)

write("backend/src/main/java/com/campus/security/dto/BlacklistRequest.java", r""" package com.campus.security.dto;

public class BlacklistRequest { private String ip; private String reason; private Integer minutes;

text
public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public Integer getMinutes() { return minutes; } public void setMinutes(Integer minutes) { this.minutes = minutes; }

} """)

Utilities/Security

write("backend/src/main/java/com/campus/security/security/ClientIpUtil.java", r""" package com.campus.security.security;

import jakarta.servlet.http.HttpServletRequest;

public class ClientIpUtil { private ClientIpUtil() {}

text
public static String getClientIp(HttpServletRequest request) { String[] headers = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP" }; for (String header : headers) { String value = request.getHeader(header); if (value != null && !value.isBlank() && !"unknown".equalsIgnoreCase(value)) { return value.split(",")[0].trim(); } } return request.getRemoteAddr(); }

} """)

write("backend/src/main/java/com/campus/security/security/JwtUtil.java", r""" package com.campus.security.security;

import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map;

@Component public class JwtUtil { private final SecretKey key; private final long expirationMillis;

text
public JwtUtil(@Value("${app.jwt.secret}") String secret, @Value("${app.jwt.expiration-minutes}") long expirationMinutes) { this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); this.expirationMillis = expirationMinutes * 60 * 1000; } public String generateToken(String username, String role) { Date now = new Date(); Date exp = new Date(now.getTime() + expirationMillis); return Jwts.builder() .subject(username) .claims(Map.of("role", role)) .issuedAt(now) .expiration(exp) .signWith(key) .compact(); } public Claims parseClaims(String token) { return Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload(); } public String getUsername(String token) { return parseClaims(token).getSubject(); } public String getRole(String token) { return parseClaims(token).get("role", String.class); }

} """)

write("backend/src/main/java/com/campus/security/security/CsrfTokenStore.java", r""" package com.campus.security.security;

import org.springframework.stereotype.Component; import java.security.SecureRandom; import java.util.Base64; import java.util.Map; import java.util.concurrent.ConcurrentHashMap;

@Component public class CsrfTokenStore { private final SecureRandom random = new SecureRandom(); private final Map<String, String> tokenByUsername = new ConcurrentHashMap<>();

text
public String createToken(String username) { byte[] bytes = new byte[32]; random.nextBytes(bytes); String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); tokenByUsername.put(username, token); return token; } public boolean verify(String username, String token) { return token != null && token.equals(tokenByUsername.get(username)); }

} """)

write("backend/src/main/java/com/campus/security/security/CustomUserDetailsService.java", r""" package com.campus.security.security;

import com.campus.security.entity.User; import com.campus.security.repository.UserRepository; import org.springframework.security.core.userdetails.*; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import java.util.List;

@Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository;

text
public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("用户不存在")); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPasswordHash(), "NORMAL".equals(user.getStatus()), true, true, !user.isLocked(), List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole())) ); }

} """)

write("backend/src/main/java/com/campus/security/security/JwtAuthenticationFilter.java", r""" package com.campus.security.security;

import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.*; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.List;

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil;

text
public JwtAuthenticationFilter(JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String auth = request.getHeader("Authorization"); if (auth != null && auth.startsWith("Bearer ")) { try { String token = auth.substring(7); String username = jwtUtil.getUsername(token); String role = jwtUtil.getRole(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( username, null, List.of(new SimpleGrantedAuthority("ROLE_" + role)) ); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (JwtException | IllegalArgumentException ignored) { SecurityContextHolder.clearContext(); } } filterChain.doFilter(request, response); }

} """)

write("backend/src/main/java/com/campus/security/security/CsrfValidationFilter.java", r""" package com.campus.security.security;

import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.Set;

@Component public class CsrfValidationFilter extends OncePerRequestFilter { private static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD", "OPTIONS"); private final CsrfTokenStore csrfTokenStore;

text
public CsrfValidationFilter(CsrfTokenStore csrfTokenStore) { this.csrfTokenStore = csrfTokenStore; } @Override protected boolean shouldNotFilter(HttpServletRequest request) { String uri = request.getRequestURI(); return SAFE_METHODS.contains(request.getMethod()) || uri.startsWith("/api/auth/login") || uri.startsWith("/api/auth/register") || uri.startsWith("/h2-console"); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = authentication == null ? null : String.valueOf(authentication.getPrincipal()); String csrf = request.getHeader("X-CSRF-Token"); if (username == null || "anonymousUser".equals(username) || !csrfTokenStore.verify(username, csrf)) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"CSRF Token 无效或缺失\",\"data\":null}"); return; } filterChain.doFilter(request, response); }

} """)

write("backend/src/main/java/com/campus/security/security/AttackDetector.java", r""" package com.campus.security.security;

import org.springframework.stereotype.Component; import java.util.List; import java.util.regex.Pattern;

@Component public class AttackDetector { private final List<Pattern> sqlPatterns = List.of( Pattern.compile("(?i)(\bunion\b\s+\bselect\b)"), Pattern.compile("(?i)(\bor\b\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?)"), Pattern.compile("(?i)(\bdrop\b\s+\btable\b)"), Pattern.compile("(?i)(\binsert\b\s+\binto\b)"), Pattern.compile("(?i)(\bdelete\b\s+\bfrom\b)"), Pattern.compile("(?i)(\bsleep\s*\()"), Pattern.compile("(?i)(\bbenchmark\s*\()"), Pattern.compile("(?i)(--|/\|\/|;\s*--)") );

text
private final List<Pattern> xssPatterns = List.of( Pattern.compile("(?i)<\\s*script\\b"), Pattern.compile("(?i)javascript\\s*:"), Pattern.compile("(?i)onerror\\s*="), Pattern.compile("(?i)onload\\s*="), Pattern.compile("(?i)<\\s*iframe\\b"), Pattern.compile("(?i)<\\s*object\\b"), Pattern.compile("(?i)<\\s*embed\\b") ); public DetectionResult detect(String text) { if (text == null || text.isBlank()) { return DetectionResult.safe(); } for (Pattern pattern : sqlPatterns) { if (pattern.matcher(text).find()) { return DetectionResult.danger("SQL_INJECTION", "HIGH", pattern.pattern()); } } for (Pattern pattern : xssPatterns) { if (pattern.matcher(text).find()) { return DetectionResult.danger("XSS", "MEDIUM", pattern.pattern()); } } return DetectionResult.safe(); } public record DetectionResult(boolean dangerous, String eventType, String riskLevel, String reason) { public static DetectionResult safe() { return new DetectionResult(false, "SAFE", "LOW", ""); } public static DetectionResult danger(String eventType, String riskLevel, String reason) { return new DetectionResult(true, eventType, riskLevel, reason); } }

} """)

write("backend/src/main/java/com/campus/security/security/WebAttackFilter.java", r""" package com.campus.security.security;

import com.campus.security.entity.IpBlacklist; import com.campus.security.entity.SecurityLog; import com.campus.security.repository.IpBlacklistRepository; import com.campus.security.repository.SecurityLogRepository; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors;

@Component public class WebAttackFilter extends OncePerRequestFilter { private final AttackDetector attackDetector; private final SecurityLogRepository securityLogRepository; private final IpBlacklistRepository ipBlacklistRepository; private final boolean blockEnabled;

text
public WebAttackFilter(AttackDetector attackDetector, SecurityLogRepository securityLogRepository, IpBlacklistRepository ipBlacklistRepository, @Value("${app.security.attack-block-enabled}") boolean blockEnabled) { this.attackDetector = attackDetector; this.securityLogRepository = securityLogRepository; this.ipBlacklistRepository = ipBlacklistRepository; this.blockEnabled = blockEnabled; } @Override protected boolean shouldNotFilter(HttpServletRequest request) { String uri = request.getRequestURI(); return uri.startsWith("/h2-console") || uri.startsWith("/swagger") || uri.startsWith("/v3/api-docs"); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String ip = ClientIpUtil.getClientIp(request); Optional<IpBlacklist> black = ipBlacklistRepository.findByIpAddress(ip); if (black.isPresent() && black.get().isActive()) { writeBlock(response, "IP 已被加入黑名单"); return; } String payload = collectPayload(request); AttackDetector.DetectionResult result = attackDetector.detect(payload); if (result.dangerous()) { securityLogRepository.save(SecurityLog.of( result.eventType(), result.riskLevel(), ip, request.getRequestURI(), request.getMethod(), truncate(payload, 3500), blockEnabled ? "BLOCK" : "PASS", result.reason() )); if (blockEnabled) { writeBlock(response, "检测到恶意请求,已拦截:" + result.eventType()); return; } } filterChain.doFilter(request, response); } private String collectPayload(HttpServletRequest request) { String query = request.getQueryString() == null ? "" : request.getQueryString(); String params = request.getParameterMap() .entrySet() .stream() .map(this::formatParam) .collect(Collectors.joining("&")); return request.getRequestURI() + "?" + query + "&" + params; } private String formatParam(Map.Entry<String, String[]> entry) { return entry.getKey() + "=" + String.join(",", entry.getValue()); } private String truncate(String value, int max) { if (value == null) return ""; return value.length() <= max ? value : value.substring(0, max); } private void writeBlock(HttpServletResponse response, String message) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"" + message + "\",\"data\":null}"); }

} """)

write("backend/src/main/java/com/campus/security/security/SecurityConfig.java", r""" package com.campus.security.security;

import org.springframework.context.annotation.; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.*; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;

@Configuration @EnableMethodSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CsrfValidationFilter csrfValidationFilter; private final WebAttackFilter webAttackFilter;

text
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, CsrfValidationFilter csrfValidationFilter, WebAttackFilter webAttackFilter) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.csrfValidationFilter = csrfValidationFilter; this.webAttackFilter = webAttackFilter; } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .headers(headers -> headers .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'")) .frameOptions(frame -> frame.sameOrigin()) .contentTypeOptions(contentType -> {}) .referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)) ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/h2-console/**").permitAll() .requestMatchers("/api/auth/login", "/api/auth/register").permitAll() .requestMatchers(HttpMethod.GET, "/api/notices", "/api/comments").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers(HttpMethod.POST, "/api/notices").hasAnyRole("TEACHER", "ADMIN") .requestMatchers(HttpMethod.DELETE, "/api/notices/**").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(webAttackFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterAfter(csrfValidationFilter, JwtAuthenticationFilter.class); return http.build(); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } @Bean AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); }

} """)

Services

write("backend/src/main/java/com/campus/security/service/AuthService.java", r""" package com.campus.security.service;

import com.campus.security.dto.*; import com.campus.security.entity.SecurityLog; import com.campus.security.entity.User; import com.campus.security.repository.SecurityLogRepository; import com.campus.security.repository.UserRepository; import com.campus.security.security.CsrfTokenStore; import com.campus.security.security.JwtUtil; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.time.LocalDateTime;

import static com.campus.security.security.ClientIpUtil.getClientIp;

@Service public class AuthService { private final UserRepository userRepository; private final SecurityLogRepository securityLogRepository; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; private final CsrfTokenStore csrfTokenStore; private final int maxFailures; private final int lockMinutes;

text
public AuthService(UserRepository userRepository, SecurityLogRepository securityLogRepository, PasswordEncoder passwordEncoder, JwtUtil jwtUtil, CsrfTokenStore csrfTokenStore, @Value("${app.security.login-max-failures}") int maxFailures, @Value("${app.security.login-lock-minutes}") int lockMinutes) { this.userRepository = userRepository; this.securityLogRepository = securityLogRepository; this.passwordEncoder = passwordEncoder; this.jwtUtil = jwtUtil; this.csrfTokenStore = csrfTokenStore; this.maxFailures = maxFailures; this.lockMinutes = lockMinutes; } public void register(RegisterRequest request) { validateUsername(request.getUsername()); validatePassword(request.getPassword()); if (userRepository.existsByUsername(request.getUsername())) { throw new IllegalArgumentException("用户名已存在"); } String role = request.getRole() == null ? "STUDENT" : request.getRole().toUpperCase(); if (!role.equals("STUDENT") && !role.equals("TEACHER")) { role = "STUDENT"; } User user = new User(); user.setUsername(request.getUsername()); user.setPasswordHash(passwordEncoder.encode(request.getPassword())); user.setRole(role); user.setStatus("NORMAL"); userRepository.save(user); } public LoginResponse login(LoginRequest request, HttpServletRequest httpRequest) { String ip = getClientIp(httpRequest); User user = userRepository.findByUsername(request.getUsername()) .orElseThrow(() -> new IllegalArgumentException("用户名或密码错误")); if (user.isLocked()) { securityLogRepository.save(SecurityLog.of( "LOGIN_LOCKED", "MEDIUM", ip, "/api/auth/login", "POST", request.getUsername(), "BLOCK", "账号临时锁定" )); throw new IllegalArgumentException("账号已被锁定,请稍后再试"); } if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { int failures = user.getLoginFailCount() + 1; user.setLoginFailCount(failures); if (failures >= maxFailures) { user.setLockedUntil(LocalDateTime.now().plusMinutes(lockMinutes)); securityLogRepository.save(SecurityLog.of( "BRUTE_FORCE", "HIGH", ip, "/api/auth/login", "POST", request.getUsername(), "BLOCK", "连续登录失败,账号锁定" )); } userRepository.save(user); throw new IllegalArgumentException("用户名或密码错误"); } user.setLoginFailCount(0); user.setLockedUntil(null); userRepository.save(user); String token = jwtUtil.generateToken(user.getUsername(), user.getRole()); String csrfToken = csrfTokenStore.createToken(user.getUsername()); return new LoginResponse(token, csrfToken, user.getUsername(), user.getRole()); } private void validateUsername(String username) { if (username == null || !username.matches("^[a-zA-Z0-9_]{4,32}$")) { throw new IllegalArgumentException("用户名必须为 4-32 位字母、数字或下划线"); } } private void validatePassword(String password) { if (password == null || password.length() < 8) { throw new IllegalArgumentException("密码长度至少 8 位"); } if (!password.matches(".*[A-Za-z].*") || !password.matches(".*\\d.*")) { throw new IllegalArgumentException("密码必须同时包含字母和数字"); } }

} """)

write("backend/src/main/java/com/campus/security/service/ContentSanitizer.java", r""" package com.campus.security.service;

import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; import org.springframework.stereotype.Component;

@Component public class ContentSanitizer { private final PolicyFactory textPolicy = Sanitizers.FORMATTING.and(Sanitizers.LINKS);

text
public String cleanText(String input) { if (input == null) { return ""; } String cleaned = textPolicy.sanitize(input); if (cleaned.length() > 4000) { return cleaned.substring(0, 4000); } return cleaned; } public String cleanPlain(String input, int maxLen) { if (input == null) { return ""; } String value = input.replaceAll("[\\r\\n\\t]", " ").trim(); value = value.replace("<", "&lt;").replace(">", "&gt;"); return value.length() <= maxLen ? value : value.substring(0, maxLen); }

} """)

write("backend/src/main/java/com/campus/security/service/NoticeService.java", r""" package com.campus.security.service;

import com.campus.security.dto.NoticeRequest; import com.campus.security.entity.Notice; import com.campus.security.repository.NoticeRepository; import org.springframework.stereotype.Service; import java.util.List;

@Service public class NoticeService { private final NoticeRepository noticeRepository; private final ContentSanitizer sanitizer;

text
public NoticeService(NoticeRepository noticeRepository, ContentSanitizer sanitizer) { this.noticeRepository = noticeRepository; this.sanitizer = sanitizer; } public List<Notice> list() { return noticeRepository.findTop50ByOrderByCreateTimeDesc(); } public Notice create(NoticeRequest request, String author) { if (request.getTitle() == null || request.getTitle().isBlank()) { throw new IllegalArgumentException("标题不能为空"); } if (request.getContent() == null || request.getContent().isBlank()) { throw new IllegalArgumentException("内容不能为空"); } Notice notice = new Notice(); notice.setTitle(sanitizer.cleanPlain(request.getTitle(), 150)); notice.setContent(sanitizer.cleanText(request.getContent())); notice.setAuthor(author); return noticeRepository.save(notice); } public void delete(Long id) { noticeRepository.deleteById(id); }

} """)

write("backend/src/main/java/com/campus/security/service/CommentService.java", r""" package com.campus.security.service;

import com.campus.security.dto.CommentRequest; import com.campus.security.entity.Comment; import com.campus.security.repository.CommentRepository; import org.springframework.stereotype.Service; import java.util.List;

@Service public class CommentService { private final CommentRepository commentRepository; private final ContentSanitizer sanitizer;

text
public CommentService(CommentRepository commentRepository, ContentSanitizer sanitizer) { this.commentRepository = commentRepository; this.sanitizer = sanitizer; } public List<Comment> list() { return commentRepository.findTop100ByOrderByCreateTimeDesc(); } public Comment create(CommentRequest request, String author) { if (request.getContent() == null || request.getContent().isBlank()) { throw new IllegalArgumentException("留言不能为空"); } Comment comment = new Comment(); comment.setContent(sanitizer.cleanText(request.getContent())); comment.setAuthor(author); return commentRepository.save(comment); }

} """)

write("backend/src/main/java/com/campus/security/service/FileStorageService.java", r""" package com.campus.security.service;

import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.; import java.util.;

@Service public class FileStorageService { private final Path uploadDir; private final Set<String> allowedExtensions;

text
public FileStorageService(@Value("${app.upload.dir}") String uploadDir, @Value("${app.upload.allowed-extensions}") String allowed) throws IOException { this.uploadDir = Paths.get(uploadDir).toAbsolutePath().normalize(); this.allowedExtensions = new HashSet<>(); for (String ext : allowed.split(",")) { allowedExtensions.add(ext.trim().toLowerCase()); } Files.createDirectories(this.uploadDir); } public Map<String, String> store(MultipartFile file) throws IOException { if (file.isEmpty()) { throw new IllegalArgumentException("文件不能为空"); } String originalName = Optional.ofNullable(file.getOriginalFilename()).orElse("unknown"); String safeName = Paths.get(originalName).getFileName().toString(); if (safeName.contains("..") || safeName.contains("/") || safeName.contains("\\")) { throw new IllegalArgumentException("文件名非法"); } String ext = getExtension(safeName); if (!allowedExtensions.contains(ext)) { throw new IllegalArgumentException("不允许上传该类型文件:" + ext); } String contentType = Optional.ofNullable(file.getContentType()).orElse(""); if (looksDangerousContentType(contentType)) { throw new IllegalArgumentException("文件 MIME 类型不安全:" + contentType); } String newName = UUID.randomUUID() + "." + ext; Path target = uploadDir.resolve(newName).normalize(); Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING); return Map.of( "originalName", safeName, "storedName", newName, "path", target.toString() ); } private String getExtension(String filename) { int index = filename.lastIndexOf("."); if (index < 0 || index == filename.length() - 1) { throw new IllegalArgumentException("文件缺少扩展名"); } return filename.substring(index + 1).toLowerCase(); } private boolean looksDangerousContentType(String contentType) { String lower = contentType.toLowerCase(); return lower.contains("x-php") || lower.contains("x-jsp") || lower.contains("x-msdownload") || lower.contains("html") || lower.contains("javascript"); }

} """)

Controllers

write("backend/src/main/java/com/campus/security/controller/AuthController.java", r""" package com.campus.security.controller;

import com.campus.security.common.ApiResponse; import com.campus.security.dto.; import com.campus.security.service.AuthService; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.; import java.util.Map;

@RestController @RequestMapping("/api/auth") public class AuthController { private final AuthService authService;

text
public AuthController(AuthService authService) { this.authService = authService; } @PostMapping("/register") public ApiResponse<Void> register(@RequestBody RegisterRequest request) { authService.register(request); return ApiResponse.ok(null); } @PostMapping("/login") public ApiResponse<LoginResponse> login(@RequestBody LoginRequest request, HttpServletRequest httpRequest) { return ApiResponse.ok(authService.login(request, httpRequest)); } @GetMapping("/me") public ApiResponse<Map<String, Object>> me(Authentication authentication) { return ApiResponse.ok(Map.of( "username", authentication.getName(), "authorities", authentication.getAuthorities() )); }

} """)

write("backend/src/main/java/com/campus/security/controller/NoticeController.java", r""" package com.campus.security.controller;

import com.campus.security.common.ApiResponse; import com.campus.security.dto.NoticeRequest; import com.campus.security.entity.Notice; import com.campus.security.service.NoticeService; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import java.util.List;

@RestController @RequestMapping("/api/notices") public class NoticeController { private final NoticeService noticeService;

text
public NoticeController(NoticeService noticeService) { this.noticeService = noticeService; } @GetMapping public ApiResponse<List<Notice>> list() { return ApiResponse.ok(noticeService.list()); } @PostMapping public ApiResponse<Notice> create(@RequestBody NoticeRequest request, Authentication authentication) { return ApiResponse.ok(noticeService.create(request, authentication.getName())); } @DeleteMapping("/{id}") public ApiResponse<Void> delete(@PathVariable Long id) { noticeService.delete(id); return ApiResponse.ok(null); }

} """)

write("backend/src/main/java/com/campus/security/controller/CommentController.java", r""" package com.campus.security.controller;

import com.campus.security.common.ApiResponse; import com.campus.security.dto.CommentRequest; import com.campus.security.entity.Comment; import com.campus.security.service.CommentService; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import java.util.List;

@RestController @RequestMapping("/api/comments") public class CommentController { private final CommentService commentService;

text
public CommentController(CommentService commentService) { this.commentService = commentService; } @GetMapping public ApiResponse<List<Comment>> list() { return ApiResponse.ok(commentService.list()); } @PostMapping public ApiResponse<Comment> create(@RequestBody CommentRequest request, Authentication authentication) { return ApiResponse.ok(commentService.create(request, authentication.getName())); }

} """)

write("backend/src/main/java/com/campus/security/controller/FileController.java", r""" package com.campus.security.controller;

import com.campus.security.common.ApiResponse; import com.campus.security.service.FileStorageService; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Map;

@RestController @RequestMapping("/api/files") public class FileController { private final FileStorageService fileStorageService;

text
public FileController(FileStorageService fileStorageService) { this.fileStorageService = fileStorageService; } @PostMapping("/upload") public ApiResponse<Map<String, String>> upload(@RequestParam("file") MultipartFile file) throws IOException { return ApiResponse.ok(fileStorageService.store(file)); }

} """)

write("backend/src/main/java/com/campus/security/controller/AdminSecurityController.java", r""" package com.campus.security.controller;

import com.campus.security.common.ApiResponse; import com.campus.security.dto.BlacklistRequest; import com.campus.security.entity.IpBlacklist; import com.campus.security.entity.SecurityLog; import com.campus.security.repository.IpBlacklistRepository; import com.campus.security.repository.SecurityLogRepository; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.; import java.time.LocalDateTime; import java.util.;

@RestController @RequestMapping("/api/admin/security") public class AdminSecurityController { private final SecurityLogRepository securityLogRepository; private final IpBlacklistRepository ipBlacklistRepository;

text
public AdminSecurityController(SecurityLogRepository securityLogRepository, IpBlacklistRepository ipBlacklistRepository) { this.securityLogRepository = securityLogRepository; this.ipBlacklistRepository = ipBlacklistRepository; } @GetMapping("/logs") public ApiResponse<List<SecurityLog>> logs() { return ApiResponse.ok(securityLogRepository.findTop100ByOrderByCreateTimeDesc()); } @GetMapping("/dashboard") public ApiResponse<Map<String, Object>> dashboard() { LocalDateTime today = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0); return ApiResponse.ok(Map.of( "todaySecurityEvents", securityLogRepository.countByCreateTimeAfter(today), "todaySqlInjection", securityLogRepository.countByEventTypeAndCreateTimeAfter("SQL_INJECTION", today), "todayXss", securityLogRepository.countByEventTypeAndCreateTimeAfter("XSS", today), "blacklistCount", ipBlacklistRepository.count() )); } @PostMapping("/blacklist") public ApiResponse<IpBlacklist> addBlacklist(@RequestBody BlacklistRequest request) { if (request.getIp() == null || request.getIp().isBlank()) { throw new IllegalArgumentException("IP 不能为空"); } IpBlacklist item = ipBlacklistRepository.findByIpAddress(request.getIp()) .orElseGet(IpBlacklist::new); item.setIpAddress(request.getIp()); item.setReason(request.getReason() == null ? "管理员手动加入" : request.getReason()); if (request.getMinutes() != null && request.getMinutes() > 0) { item.setExpireTime(LocalDateTime.now().plusMinutes(request.getMinutes())); } else { item.setExpireTime(null); } return ApiResponse.ok(ipBlacklistRepository.save(item)); } @DeleteMapping("/blacklist/{ip}") @Transactional public ApiResponse<Void> removeBlacklist(@PathVariable String ip) { ipBlacklistRepository.deleteByIpAddress(ip); return ApiResponse.ok(null); }

} """)

Frontend

write("frontend/package.json", r""" { "scripts": { "dev": "vite --host 0.0.0.0", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@vitejs/plugin-vue": "^5.1.4", "axios": "^1.7.7", "vite": "^5.4.8", "vue": "^3.5.10", "vue-router": "^4.4.5" }, "devDependencies": {} } """)

write("frontend/index.html", r"""

<!doctype html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>校园网站安全防护系统</title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.js"></script> </body> </html> """)

write("frontend/src/main.js", r""" import { createApp } from 'vue' import App from './App.vue' import './style.css'

createApp(App).mount('#app') """)

write("frontend/src/api.js", r""" import axios from 'axios'

const api = axios.create({ baseURL: 'http://localhost:8080/api', timeout: 8000 })

api.interceptors.request.use(config => { const token = localStorage.getItem('token') const csrfToken = localStorage.getItem('csrfToken')

if (token) { config.headers.Authorization = Bearer ${token} } if (csrfToken && !['get', 'head', 'options'].includes((config.method || '').toLowerCase())) { config.headers['X-CSRF-Token'] = csrfToken }

return config })

api.interceptors.response.use( response => response.data, error => { const message = error.response?.data?.message || error.message || '请求失败' alert(message) return Promise.reject(error) } )

export default api """)

write("frontend/src/App.vue", r""" <template>

<div class="layout"> <header class="topbar"> <div> <h1>小型校园网站 Web 安全防护系统</h1> <p>认证 · RBAC · 攻击拦截 · CSRF · 文件上传校验 · 安全审计</p> </div> <div class="user-box"> <span v-if="user">当前用户:{{ user.username }} / {{ user.role }}</span> <button v-if="user" @click="logout">退出</button> </div> </header>
text
<main class="grid"> <section class="card"> <h2>登录</h2> <input v-model="loginForm.username" placeholder="用户名,例如 admin" /> <input v-model="loginForm.password" placeholder="密码,例如 Admin@123456" type="password" /> <button @click="login">登录</button> <p class="hint">默认管理员:admin / Admin@123456</p> </section> <section class="card"> <h2>注册学生/教师</h2> <input v-model="registerForm.username" placeholder="用户名" /> <input v-model="registerForm.password" placeholder="密码,至少 8 位且含字母数字" type="password" /> <select v-model="registerForm.role"> <option value="STUDENT">学生</option> <option value="TEACHER">教师</option> </select> <button @click="register">注册</button> </section> <section class="card wide"> <h2>公告列表</h2> <div class="form-row"> <input v-model="noticeForm.title" placeholder="公告标题,教师/管理员可发布" /> <input v-model="noticeForm.content" placeholder="公告内容,支持基础安全富文本" /> <button @click="createNotice">发布</button> </div> <div v-for="item in notices" :key="item.id" class="item"> <h3>{{ item.title }}</h3> <p v-html="item.content"></p> <small>{{ item.author }} · {{ item.createTime }}</small> </div> </section> <section class="card wide"> <h2>留言板</h2> <div class="form-row"> <input v-model="commentForm.content" placeholder="输入留言,系统会过滤 XSS" /> <button @click="createComment">留言</button> </div> <div v-for="item in comments" :key="item.id" class="item"> <p v-html="item.content"></p> <small>{{ item.author }} · {{ item.createTime }}</small> </div> </section> <section class="card"> <h2>文件上传</h2> <input type="file" @change="onFileChange" /> <button @click="uploadFile">上传</button> <p class="hint">只允许 jpg、png、pdf、doc、docx、xls、xlsx、txt 等类型。</p> </section> <section class="card admin"> <h2>安全管理后台</h2> <button @click="loadDashboard">刷新统计</button> <pre>{{ dashboard }}</pre> <h3>安全日志</h3> <button @click="loadLogs">查看最近 100 条日志</button> <div class="log-list"> <div v-for="log in logs" :key="log.id" class="log"> <strong>{{ log.eventType }} / {{ log.riskLevel }}</strong> <span>{{ log.sourceIp }} · {{ log.action }}</span> <small>{{ log.reason }}</small> </div> </div> </section> </main>
</div> </template> <script setup> import { reactive, ref, onMounted } from 'vue' import api from './api' const user = ref(JSON.parse(localStorage.getItem('user') || 'null')) const notices = ref([]) const comments = ref([]) const logs = ref([]) const dashboard = ref({}) const selectedFile = ref(null) const loginForm = reactive({ username: 'admin', password: 'Admin@123456' }) const registerForm = reactive({ username: '', password: '', role: 'STUDENT' }) const noticeForm = reactive({ title: '', content: '' }) const commentForm = reactive({ content: '' }) async function login() { const res = await api.post('/auth/login', loginForm) localStorage.setItem('token', res.data.token) localStorage.setItem('csrfToken', res.data.csrfToken) localStorage.setItem('user', JSON.stringify({ username: res.data.username, role: res.data.role })) user.value = JSON.parse(localStorage.getItem('user')) } function logout() { localStorage.clear() user.value = null } async function register() { await api.post('/auth/register', registerForm) alert('注册成功') } async function loadNotices() { const res = await api.get('/notices') notices.value = res.data } async function createNotice() { await api.post('/notices', noticeForm) noticeForm.title = '' noticeForm.content = '' await loadNotices() } async function loadComments() { const res = await api.get('/comments') comments.value = res.data } async function createComment() { await api.post('/comments', commentForm) commentForm.content = '' await loadComments() } function onFileChange(event) { selectedFile.value = event.target.files[0] } async function uploadFile() { if (!selectedFile.value) { alert('请选择文件') return } const form = new FormData() form.append('file', selectedFile.value) await api.post('/files/upload', form, { headers: { 'Content-Type': 'multipart/form-data' } }) alert('上传成功') } async function loadDashboard() { const res = await api.get('/admin/security/dashboard') dashboard.value = res.data } async function loadLogs() { const res = await api.get('/admin/security/logs') logs.value = res.data } onMounted(() => { loadNotices() loadComments() }) </script>

""")

write("frontend/src/style.css", r"""

  • { box-sizing: border-box; }

body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; background: #f4f6fb; color: #1f2937; }

.layout { max-width: 1180px; margin: 0 auto; padding: 24px; }

.topbar { display: flex; justify-content: space-between; align-items: center; padding: 24px; background: linear-gradient(135deg, #1f2937, #374151); color: white; border-radius: 18px; margin-bottom: 24px; }

.topbar h1 { margin: 0; font-size: 26px; }

.topbar p { margin: 8px 0 0; color: #d1d5db; }

.user-box { display: flex; gap: 10px; align-items: center; }

.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }

.card { background: white; border-radius: 18px; padding: 22px; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08); }

.card h2 { margin-top: 0; }

.wide { grid-column: span 2; }

input, select { width: 100%; padding: 12px 14px; border: 1px solid #d1d5db; border-radius: 12px; margin-bottom: 10px; font-size: 14px; }

button { border: none; padding: 10px 16px; background: #2563eb; color: white; border-radius: 12px; cursor: pointer; font-weight: 600; }

button:hover { background: #1d4ed8; }

.hint { color: #6b7280; font-size: 13px; }

.form-row { display: grid; grid-template-columns: 1fr 2fr auto; gap: 10px; align-items: start; }

.item { padding: 14px 0; border-bottom: 1px solid #e5e7eb; }

.item h3 { margin: 0 0 6px; }

.item p { margin: 0 0 8px; }

.item small { color: #6b7280; }

.admin { grid-column: span 2; }

pre { background: #111827; color: #e5e7eb; padding: 12px; border-radius: 12px; overflow-x: auto; }

.log-list { display: grid; gap: 10px; }

.log { display: grid; gap: 4px; padding: 12px; background: #f9fafb; border-radius: 12px; border: 1px solid #e5e7eb; }

.log span, .log small { color: #6b7280; } """)

Add .gitignore

write("backend/.gitignore", r""" target/ data/ uploads/ *.log """)

write("frontend/.gitignore", r""" node_modules/ dist/ .env """)

Create a short file tree

file_list = [] for root, dirs, files in os.walk(base): for file in files: rel = os.path.relpath(os.path.join(root, file), base) file_list.append(rel)

write("PROJECT_STRUCTURE.md", "# 项目结构\n\ntext\n" + "\n".join(sorted(file_list)) + "\n\n")

Zip

zip_path = "/mnt/data/campus-web-security.zip" if os.path.exists(zip_path): os.remove(zip_path)

with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z: for root, dirs, files in os.walk(base): for file in files: full = os.path.join(root, file) arc = os.path.relpath(full, "/mnt/data") z.write(full, arc)

zip_path

'/mnt/data/campus-web-security.zip'

답변

已根据上面的方案生成了一个可运行的项目代码包,包含:

  • Spring Boot 后端
  • Vue 前端
  • 登录认证
  • RBAC 权限控制
  • SQL 注入 / XSS 请求拦截
  • CSRF Token 校验
  • 文件上传安全校验
  • 登录防暴力破解
  • IP 黑名单
  • 安全日志审计后台
  • README 运行说明
  • MySQL 初始化脚本

下载代码包:

点击下载 campus-web-security.zip

运行方式:

bash
cd backend mvn spring-boot:run

然后运行前端:

bash
cd frontend npm install npm run dev

默认管理员账号:

text
admin Admin@123456

后端默认使用 H2 数据库,方便直接运行。需要改成 MySQL 时,修改 backend/src/main/resources/application.yml 里的数据库连接即可。

이 Q&A 공유하기