7.1 保护 REST API
在数字化时代,REST API是现代Web应用和微服务架构中数据交互的关键组成部分。然而,随着它们的普及和重要性的增加,保护这些API免受恶意攻击变得尤为重要。本节将探讨保护REST API的基础知识和实用案例。
7.1.1 基础知识详解
在构建和维护REST API时,安全性是一个不容忽视的要素。REST API作为应用程序与外界交互的接口,常常面临着各种安全威胁,包括但不限于身份盗窃、数据泄露、服务拒绝攻击等。因此,采取有效的安全措施保护REST API是至关重要的。以下是保护REST API时需掌握的基础知识。
身份验证 (Authentication)
- 定义:确定请求者的身份,确保只有合法用户可以访问API。
- 方法:
- 基本认证:通过HTTP头传递用户名和密码的简单认证方法,需要使用HTTPS来避免凭证泄露。
- 令牌认证:如JWT,通过签名的令牌确认用户身份,支持无状态认证。
- OAuth/OAuth2:为第三方应用提供限制的访问权限,而无需暴露用户的凭证。
授权 (Authorization)
- 定义:确定已认证的用户可以执行的操作或访问的数据。
- 实现方式:
- 角色基础的访问控制(RBAC):根据用户的角色来决定其权限。
- 属性基础的访问控制(ABAC):根据属性(用户属性、资源属性和环境属性)来动态决定访问权限。
传输安全 (Transport Security)
- HTTPS:使用SSL/TLS加密HTTP请求和响应,防止数据在传输过程中被截获或篡改。
- HSTS(HTTP Strict Transport Security):强制客户端(如浏览器)使用HTTPS与服务器建立连接。
数据保护
- 数据加密:对敏感数据进行加密处理,保护存储在服务器上或传输过程中的数据。
- 数据脱敏:在公开的响应中避免直接展示敏感数据,如用户ID、电子邮件地址等。
输入验证
- 目的:防止恶意输入导致的安全漏洞,如SQL注入、XSS攻击。
- 实践:对所有输入数据进行验证,拒绝不符合预期格式的请求。
错误处理
- 优雅处理:错误信息应足够通用,避免泄露敏感信息或系统细节。
- 日志记录:记录错误日志,但避免在日志中记录敏感信息。
限制与节流 (Rate Limiting and Throttling)
- 目的:防止API滥用,保护后端服务不受恶意攻击或过载。
- 实现:限制来自单一来源的请求频率,当达到限制时返回适当的HTTP状态码。
通过这些基础知识的详解,我们可以看到保护REST API涉及到多个方面,包括但不限于身份验证、授权、传输安全、数据保护和输入验证等。正确实施这些安全措施,可以有效提高API的安全性,保护用户数据和服务的稳定性。
7.1.2 重点案例:使用 JWT 进行身份验证和授权
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在双方之间安全地传输信息作为 JSON 对象。由于其紧凑和自包含的特性,JWT 非常适合用于 REST API 的身份验证和授权。以下案例将引导你实现 JWT 在 Spring Boot 应用中的身份验证和授权。
案例 Demo
步骤 1: 引入 JWT 库依赖
首先,在 Spring Boot 项目的pom.xml
中添加对 JWT 库的依赖。这里我们使用jjwt
库作为示例:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
步骤 2: 创建JWT工具类
创建一个JWT工具类JwtUtil
,用于生成和验证 JWT 令牌:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.stereotype.Component; import java.util.Date; import java.util.function.Function; @Component public class JwtUtil { private String secret = "yourSecretKey"; // 用于签名的密钥 public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } private Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } public String generateToken(String username) { return Jwts.builder().setSubject(username) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时有效期 .signWith(SignatureAlgorithm.HS256, secret).compact(); } public Boolean validateToken(String token, String username) { final String tokenUsername = extractUsername(token); return (username.equals(tokenUsername) && !isTokenExpired(token)); } }
步骤 3: 实现 JWT 请求过滤器
创建JwtRequestFilter
类,它将在每次请求时检查 JWT 令牌的有效性:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private MyUserDetailsService userDetailsService; @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String authorizationHeader = request.getHeader("Authorization"); String username = null; String jwt = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); username = jwtUtil.extractUsername(jwt); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails.getUsername())) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } }
步骤 4: 配置 Spring Security
最后,在 Spring Security 配置中注册JwtRequestFilter
:
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.http.SessionCreation Policy; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtRequestFilter jwtRequestFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().antMatchers("/authenticate").permitAll() .anyRequest().authenticated() .and().sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } }
通过这些步骤,你的 Spring Boot 应用现在能够利用 JWT 进行身份验证和授权,从而保护 REST API 免受未授权访问。记得保密你的 JWT 密钥,并定期更新以维护系统安全。
7.1.3 拓展案例 1:API 密钥认证
API 密钥认证是一种简单但有效的安全措施,用于控制对 REST API 的访问。它适用于服务到服务的通信,其中一个服务需要验证另一个服务的请求。以下案例演示了如何在 Spring Boot 应用中实现 API 密钥认证。
案例 Demo
步骤 1: 定义 API 密钥存储
首先,假设我们有一个简单的方式来存储和验证 API 密钥。在实际应用中,这些密钥可能会存储在数据库或配置文件中。这里我们使用一个简单的 Map 模拟。
import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component public class ApiKeyStore { private final Map<String, String> apiKeys = new HashMap<>(); public ApiKeyStore() { // 初始化一些API密钥,实际应用中应该从安全的地方加载 apiKeys.put("service1", "key-123"); apiKeys.put("service2", "key-456"); } public boolean validateKey(String serviceId, String apiKey) { return apiKey.equals(apiKeys.get(serviceId)); } }
步骤 2: 实现 API 密钥认证过滤器
创建ApiKeyAuthenticationFilter
类,该过滤器负责拦截请求并验证 API 密钥。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { @Autowired private ApiKeyStore apiKeyStore; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String serviceId = request.getHeader("Service-Id"); String apiKey = request.getHeader("API-Key"); if (serviceId == null || apiKey == null || !apiKeyStore.validateKey(serviceId, apiKey)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key"); return; } chain.doFilter(request, response); } }
步骤 3: 在Spring Security 配置中注册 API 密钥认证过滤器
接下来,需要在 Spring Security 配置中添加ApiKeyAuthenticationFilter
。
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.context.annotation.Bean; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public ApiKeyAuthenticationFilter apiKeyAuthenticationFilter() { return new ApiKeyAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(apiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .anyRequest().authenticated() .and() .csrf().disable(); } }
通过以上步骤,Spring Boot 应用现在能够使用 API 密钥进行简单的身份验证。任何未提供有效 API 密钥的请求都将被拒绝访问。
测试API密钥认证
启动应用并尝试发送请求到受保护的端点,确保在请求头中包含有效的Service-Id
和API-Key
。如果密钥验证失败,应收到 HTTP 401 Unauthorized 错误。
这种 API 密钥认证方法虽然简单,但在某些场景下非常有效,尤其是在服务对服务的通信中。记得保护好你的 API 密钥,避免泄露。
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)(中)+https://developer.aliyun.com/article/1487156