Web 安全核心机制——从 XSS/CSRF 到 HTTPS OAuth2

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 CookieSet-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 安全是一个层层递进的防御体系,没有任何单一技术能解决所有安全问题:

  1. 应用层安全:XSS、CSRF、SQL 注入等攻击需要通过输入验证、输出编码和内容安全策略来防御
  2. 传输层安全:TLS 1.3 和强加密套件保证了数据传输的机密性和完整性
  3. 认证授权:JWT 提供无状态认证机制,OAuth2 提供了标准化的三方授权框架
  4. 框架安全:Spring Security 的过滤器链和方法级安全注解提供了声明式的安全控制

安全是一个持续演进的过程。保持对 OWASP Top 10 等安全威胁的关注,定期进行安全审计和渗透测试,使用依赖扫描工具(如 OWASP Dependency-Check)检查已知漏洞,才能真正构建起纵深防御体系。记住:安全不是功能,而是一种质量属性,它贯穿于软件开发生命周期的每个环节。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容