Web 安全核心机制——从 XSS/CSRF 到 HTTPS OAuth2
一、引言
Web 安全是每一个后端开发者的必修课。OWASP(开放 Web 应用程序安全项目)每年发布 Top 10 Web 安全风险,其中注入攻击、跨站脚本(XSS)、安全配置错误等始终位列前茅。在当今 API 化、微服务化的架构趋势下,Web 安全的范畴已从传统的服务端渲染页面扩展到涵盖 REST API、SPA 前端、移动端和第三方集成的全面安全防护。
本文将从最基础的攻击防御出发,依次深入 HTTPS/TLS 通信安全、JWT 和 OAuth2 认证授权体系,最后分析 Spring Security 的核心工作流程,构建完整的 Web 安全知识体系。
二、常见 Web 攻击与防御
2.1 XSS(跨站脚本攻击)
XSS 攻击者将恶意脚本注入到网页中,当其他用户浏览时执行该脚本。
三种 XSS 类型
| 类型 | 特点 | 示例 |
|---|---|---|
| 反射型 | 恶意脚本来自当前 HTTP 请求 | https://example.com/search?q= |
| 存储型 | 恶意脚本存储在服务器数据库中 | 论坛评论中嵌入 |
| DOM 型 | 恶意脚本在客户端 DOM 的修改中执行 | document.write(location.hash) |
// ❌ 危险:直接输出用户输入
@GetMapping("/search")
public String search(@RequestParam String q, Model model) {
model.addAttribute("query", q); // 未转义,反射型 XSS
return "search";
}
// ✅ 安全:HTML 转义
@GetMapping("/search")
public String search(@RequestParam String q, Model model) {
model.addAttribute("query", HtmlUtils.htmlEscape(q));
return "search";
}
防御措施:
1. 输出编码:对用户输入进行 HTML Entity 编码(< → <)
2. CSP(内容安全策略):限制脚本执行来源
3. HttpOnly Cookie:禁止 JavaScript 访问 Cookie
// Spring Security 自动启用 CSP
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("script-src 'self'; object-src 'none'"))
);
return http.build();
}
}
2.2 CSRF(跨站请求伪造)
CSRF 攻击诱导用户在已认证的情况下访问恶意链接,利用用户的登录状态执行非自愿操作。
sequenceDiagram
participant User as 用户(已登录)
participant Bank as 银行网站
participant Attacker as 攻击网站
User->>Bank: POST /login (credentials)
Bank-->>User: Set-Cookie: session_id
User->>Attacker: 访问恶意网站
Attacker-->>User: <img src="/transfer?to=attacker&amount=10000">
Note over User,Bank: Cookie 自动携带
User->>Bank: GET /transfer?to=attacker&amount=10000
Bank->>Bank: 验证 session_id
Bank-->>Attacker: 转账成功!
防御措施:
1. CSRF Token:每个表单携带随机 Token,服务器验证
2. SameSite Cookie:Set-Cookie: session_id=...; SameSite=Strict
3. 验证 Referer/Origin:检查请求来源
// Spring Security CSRF 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
2.3 SQL 注入
// ❌ 危险:字符串拼接
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
// 输入:admin' OR '1'='1
// SQL:SELECT * FROM users WHERE username = 'admin' OR '1'='1'
// ✅ 安全:参数化查询
@Query("SELECT u FROM User u WHERE u.username = :username AND u.password = :password")
User findByUsernameAndPassword(@Param("username") String username,
@Param("password") String password);
// 使用 PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
三、HTTPS 与 TLS
3.1 TLS 握手流程
sequenceDiagram
participant Client as Client
participant Server as Server
Client->>Server: ClientHello<br/>TLS Version, Cipher Suites, Random
Server-->>Client: ServerHello<br/>Chosen Cipher, Random, Certificate
Server-->>Client: Certificate (含公钥)
Server-->>Client: ServerHelloDone
Client->>Client: 验证证书链
Client->>Client: 生成 Pre-Master Secret
Client->>Server: ClientKeyExchange (用公钥加密 Pre-Master Secret)
Client->>Client: 计算 Session Key
Server->>Server: 解密 Pre-Master Secret, 计算 Session Key
Client->>Server: ChangeCipherSpec (切换加密)
Server-->>Client: ChangeCipherSpec (切换加密)
Client->>Server: Finished (加密)
Server-->>Client: Finished (加密)
Note over Client,Server: 安全通信建立
3.2 证书链
根证书 (Root CA) —— 自签名,预置于操作系统
└── 中间证书 (Intermediate CA)
└── 服务器证书 (Server Certificate)
├── CN: blog.jydeep.cn
├── 公钥: RSA 2048/4096
├── 签名: 由中间 CA 私钥签名
└── 有效期: 90天 ~ 1年
// Java 中的 SSL 上下文配置
@Configuration
public class SSLConfig {
@Bean
public SSLContext sslContext() throws Exception {
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
// 加载信任证书
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream is = new FileInputStream("truststore.jks")) {
trustStore.load(is, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
return sslContext;
}
}
3.3 加密套件
// JDK 8+ 推荐的 TLS 1.3 加密套件
// TLS_AES_128_GCM_SHA256 (默认)
// TLS_AES_256_GCM_SHA384 (更高安全性)
// TLS_CHACHA20_POLY1305_SHA256 (移动端优化)
// 配置支持的加密套件
SSLServerSocket sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(443);
sslServerSocket.setEnabledCipherSuites(new String[]{
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384"
});
sslServerSocket.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"});
四、JWT 与 OAuth2
4.1 JWT 结构
JWT(JSON Web Token)由三部分组成:Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header(头部):
{
"alg": "HS256",
"typ": "JWT"
}
Payload(负载):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1616239022
}
Signature(签名):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
// Java JWT 编码与解码
public class JwtUtil {
private static final SecretKey key = Keys.hmacShaKeyFor(
"my-very-long-and-secure-secret-key-256-bits-at-least!!".getBytes());
public static String generateToken(String username, Long userId, int expireHours) {
return Jwts.builder()
.subject(username)
.claim("userId", userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expireHours * 3600 * 1000L))
.signWith(key)
.compact();
}
public static Claims validateToken(String token) {
try {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (JwtException e) {
throw new RuntimeException("Invalid token", e);
}
}
}
4.2 OAuth2 授权码流程
OAuth2 是当前最流行的授权框架,授权码(Authorization Code)模式是最安全的流程:
sequenceDiagram
participant User as 用户
participant App as 第三方应用
participant Auth as 授权服务器
participant API as 资源服务器
User->>App: 点击"使用微信登录"
App->>Auth: 1. 跳转授权页面<br/>(client_id, redirect_uri, scope)
User->>Auth: 2. 确认授权
Auth-->>App: 3. 302 重定向: redirect_uri?code=AUTH_CODE
App->>Auth: 4. POST: code + client_secret
Auth-->>App: 5. access_token + refresh_token
App->>API: 6. GET /userinfo (Authorization: Bearer access_token)
API-->>App: 7. 用户信息
Note over App,Auth: 授权码流程的关键:code 是一次性的
// Spring Security OAuth2 客户端配置
spring:
security:
oauth2:
client:
registration:
github:
client-id: your-client-id
client-secret: your-client-secret
scope:
- user:email
- read:user
// 资源服务器配置
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
4.3 Spring Boot JWT 过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
Claims claims = jwtUtil.validateToken(token);
String username = claims.getSubject();
Long userId = claims.get("userId", Long.class);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username, null, List.of(new SimpleGrantedAuthority("ROLE_USER")));
authentication.setDetails(userId);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
五、Spring Security 核心流程
5.1 过滤器链
Spring Security 的核心架构基于 Servlet Filter Chain:
graph LR
Req[Request] --> CORS[CorsFilter]
CORS --> CSRF[CsrfFilter]
CSRF --> AFA[AnonymousAuthenticationFilter]
AFA --> SCM[SecurityContextHolderAwareRequestFilter]
SCM --> AF[AbstractAuthenticationProcessingFilter]
AF --> EPT[ExceptionTranslationFilter]
EPT --> FAS[FilterSecurityInterceptor]
FAS --> S[Service]
FAS -.->|AccessDenied| EPT
EPT -.->|认证失败| Login[Login Page / 401]
EPT -.->|权限不足| Forbidden[403 Forbidden]
5.2 认证流程
// 自定义认证提供者
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
if (!user.isEnabled()) {
throw new DisabledException("Account disabled");
}
return new UsernamePasswordAuthenticationToken(
user, password, user.getAuthorities());
}
@Override
public boolean supports(Class> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
5.3 方法级别安全
@EnableMethodSecurity
@Configuration
public class MethodSecurityConfig {}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/all")
public List<Order> getAllOrders() { }
@PreAuthorize("#order.userId == authentication.principal.id")
@PostMapping
public Order createOrder(@RequestBody Order order) { }
@PostFilter("filterObject.status != 'PRIVATE'")
@GetMapping("/list")
public List<Order> listOrders() { }
@Secured("ROLE_ADMIN")
@DeleteMapping("/{id}")
public void deleteOrder(@PathVariable Long id) { }
}
| 注解 | 作用 | 执行的校验 |
|---|---|---|
@PreAuthorize |
方法执行前 | SpEL 表达式评估 |
@PostAuthorize |
方法执行后 | 对返回值进行校验 |
@PreFilter |
方法执行前 | 过滤集合参数 |
@PostFilter |
方法执行后 | 过滤返回值集合 |
@Secured |
方法执行前 | 检查角色(Spring 原生) |
六、安全配置最佳实践
6.1 密码存储
// BCrypt 密码编码(默认强度 10)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度 12,约 250ms 计算时间
}
// 编码结果示例
// $2a$12$dG6iY5L9W8xR7vQ3z2ZzOuG3rXj5K1mN8qL9W8xR7vQ3z2ZzOuG
6.2 安全 Headers
# Spring Boot 安全 Headers 配置
server:
servlet:
session:
cookie:
http-only: true
secure: true
same-site: strict
# 或通过 Spring Security 配置
http.headers(headers -> headers
.xssProtection(xss -> xss.block(true))
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'"))
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true))
);
七、总结
Web 安全是一个层层递进的防御体系,没有任何单一技术能解决所有安全问题:
- 应用层安全:XSS、CSRF、SQL 注入等攻击需要通过输入验证、输出编码和内容安全策略来防御
- 传输层安全:TLS 1.3 和强加密套件保证了数据传输的机密性和完整性
- 认证授权:JWT 提供无状态认证机制,OAuth2 提供了标准化的三方授权框架
- 框架安全:Spring Security 的过滤器链和方法级安全注解提供了声明式的安全控制
安全是一个持续演进的过程。保持对 OWASP Top 10 等安全威胁的关注,定期进行安全审计和渗透测试,使用依赖扫描工具(如 OWASP Dependency-Check)检查已知漏洞,才能真正构建起纵深防御体系。记住:安全不是功能,而是一种质量属性,它贯穿于软件开发生命周期的每个环节。


暂无评论内容