<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Marwin&#39;s Blog</title>
    <link>https://marwincn.github.io/</link>
    
    <atom:link href="/rss2.xml" rel="self" type="application/rss+xml"/>
    
    <description></description>
    <pubDate>Sun, 13 Aug 2023 03:42:22 GMT</pubDate>
    <generator>http://hexo.io/</generator>
    
    <item>
      <title>GraphQL在Spring Boot项目中的实践</title>
      <link>https://marwincn.github.io/2021/09/04/GraphQL%E5%9C%A8Spring%20Boot%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84%E5%AE%9E%E8%B7%B5/</link>
      <guid>https://marwincn.github.io/2021/09/04/GraphQL%E5%9C%A8Spring%20Boot%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84%E5%AE%9E%E8%B7%B5/</guid>
      <pubDate>Sat, 04 Sep 2021 16:00:00 GMT</pubDate>
      <description>
      
        &lt;h1 id=&quot;什么是GraphQL？&quot;&gt;&lt;a href=&quot;#什么是GraphQL？&quot; class=&quot;headerlink&quot; title=&quot;什么是GraphQL？&quot;&gt;&lt;/a&gt;什么是GraphQL？&lt;/h1&gt;&lt;p&gt;GraphQL 是一个用于 API 的查询语言，是一个使用基于类型系统来执行查询的服务端运行时（类型系统由你的数据定义）。它对标的是REST这种接口风格，它重新定义了一种接口模式，让调用方可以指定需要查询的数据，而且没有任何冗余。&lt;/p&gt;
      
      </description>
      
      
      <content:encoded><![CDATA[<h1 id="什么是GraphQL？"><a href="#什么是GraphQL？" class="headerlink" title="什么是GraphQL？"></a>什么是GraphQL？</h1><p>GraphQL 是一个用于 API 的查询语言，是一个使用基于类型系统来执行查询的服务端运行时（类型系统由你的数据定义）。它对标的是REST这种接口风格，它重新定义了一种接口模式，让调用方可以指定需要查询的数据，而且没有任何冗余。</p><span id="more"></span> <p>GraphQL相比现在流行的RESTful接口的优势在哪呢？考虑一种场景：一个购物系统，提供在主页展示商品简略信息的接口，这个接口只用查询出商品的名称、价格和介绍图地址，然后当我进入到商品的详情页面时需要用另一个接口查出商品的更多具体信息。同样是查询商品信息这个操作在不同的场景可能就要定义<code>/product/brief</code>和<code>/product/detail/</code>等多个接口。</p><p>而对于GraphQL，查询简略信息的场景，我们可以用这样一个查询语句：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  product &#123;</span><br><span class="line">    name</span><br><span class="line">    price</span><br><span class="line">    picture</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而在详情页面我们的查询语句是这样：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  product &#123;</span><br><span class="line">    name</span><br><span class="line">    price</span><br><span class="line">    originalPrice</span><br><span class="line">    picture</span><br><span class="line">    morePicture</span><br><span class="line">    publishTime</span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样我们的客户端查询更加灵活，服务端不用编写更多的接口应对更多的场景，可复用性更好。这就是GraphQL最大也最直观的优势了。</p><h1 id="在Spring-Boot项目中编写GraphQL接口"><a href="#在Spring-Boot项目中编写GraphQL接口" class="headerlink" title="在Spring Boot项目中编写GraphQL接口"></a>在Spring Boot项目中编写GraphQL接口</h1><p>首先为项目添加graphql-java的依赖：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&lt;!-- GraphQL --&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;com.graphql-java&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;graphql-java&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;11.0&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;com.graphql-java&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;graphql-java-spring-boot-starter-webmvc&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.0&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><p>然后后在resource目录下添加一个定义Schema的文件，如<code>schema.graphqls</code>。Schema定义了一个Query用于查询，blogById属性返回Blog类型数据，然后定义了几个实体类型。完整的定义如下：</p><p><img src="https://i.loli.net/2021/09/05/ZwGEDuKAxpodB1n.png" alt="截屏2021-09-05 20.17.08"></p><p>然后最重要的部分是实现数据获取的部分。</p><p>首先定义一个GraphQLProvider类，它的作用是结合<code>TypeDefinitionRegistry</code>和<code>RuntimeWiring</code>并生成最终的GraphQL，把GraphQL这个Bean注入到Spring容器中就能对外提供一个<code>/graphql</code>的HTTP接口。下面分别说说<code>TypeDefinitionRegistry</code>和<code>RuntimeWiring</code>是什么。</p><p><code>TypeDefinitionRegistry</code>比较简单就是从之前<code>schema.graphqls</code>读取到的Schema定义；<code>RuntimeWiring</code>要知道如何去获取Schema中定义的类型的数据，这里就需要我们自己实现<code>DataFetcher</code>了。借用一下官方的图展示一下它们的关系：</p><p><img src="https://www.graphql-java.com/images/graphql_creation.png" alt="graphql_creation"></p><p>下面是GraphQLProvider的代码，和官方的图基本一致比较清晰：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">GraphQLProvider</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    GraphQLDataFetchers graphQLDataFetchers;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> GraphQL graphQL;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> GraphQL <span class="title">graphQL</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> graphQL;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostConstruct</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        GraphQLSchema graphQLSchema = buildSchema();</span><br><span class="line">        <span class="keyword">this</span>.graphQL = GraphQL.newGraphQL(graphQLSchema).build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> GraphQLSchema <span class="title">buildSchema</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        TypeDefinitionRegistry typeRegistry = buildRegistry();</span><br><span class="line">        RuntimeWiring runtimeWiring = buildWiring();</span><br><span class="line">        SchemaGenerator schemaGenerator = <span class="keyword">new</span> SchemaGenerator();</span><br><span class="line">        <span class="comment">// 结合 TypeDefinitionRegistry 和 RuntimeWiring 生成 GraphQLSchema</span></span><br><span class="line">        <span class="keyword">return</span> schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> TypeDefinitionRegistry <span class="title">buildRegistry</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        URL url = Resources.getResource(<span class="string">&quot;schema.graphqls&quot;</span>);</span><br><span class="line">        String sdl = Resources.toString(url, Charsets.UTF_8);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> SchemaParser().parse(sdl);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> RuntimeWiring <span class="title">buildWiring</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> RuntimeWiring.newRuntimeWiring()</span><br><span class="line">                .type(newTypeWiring(<span class="string">&quot;Query&quot;</span>)</span><br><span class="line">                        .dataFetcher(<span class="string">&quot;blogById&quot;</span>, graphQLDataFetchers.getBlogByIdDataFetcher()))</span><br><span class="line">                .type(newTypeWiring(<span class="string">&quot;Blog&quot;</span>)</span><br><span class="line">                        .dataFetcher(<span class="string">&quot;author&quot;</span>, graphQLDataFetchers.getAuthorDataFetcher()))</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后看看具体的DataFetchers怎么实现：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">GraphQLDataFetchers</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    BlogService blogService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> DataFetcher&lt;Blog&gt; <span class="title">getBlogByIdDataFetcher</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> dataFetchingEnvironment -&gt; &#123;</span><br><span class="line">            Integer blogId = dataFetchingEnvironment.getArgument(<span class="string">&quot;id&quot;</span>);</span><br><span class="line">            <span class="keyword">return</span> blogService.findById(blogId);</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> DataFetcher&lt;User&gt; <span class="title">getAuthorDataFetcher</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> dataFetchingEnvironment -&gt; &#123;</span><br><span class="line">            Blog blog = dataFetchingEnvironment.getSource();</span><br><span class="line">            Integer aid = blog.getAid();</span><br><span class="line">            <span class="keyword">return</span> userService.findById(aid);</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>DataFetcher设计采用了函数式编程的思想，它是一个函数式接口，提供了dataFetchingEnvironment获取查询参数，返回查询结果，中间如何去查到数据由函数内部实现GraphQL框架并不关注。这里我直接使用之前写好的业务层接口BlogService根据id查询Blog，然后根据Blog的aid用UserService查询author。<strong>写到这里其实大致就能看出来GraphQL它的功能主要体现control层，要在原来的RESTful接口上做迁移也只用修改control层的代码。</strong></p><p>写完了之后运行项目看看效果，我这里使用了<code>Altair GraphQL Client</code>这个浏览器插件调用GraphQL接口：</p><p><img src="https://i.loli.net/2021/09/05/XJeKBYbLO8jESI1.png" alt="截屏2021-09-05 21.08.34"></p><p>这里要注意的是GraphQL接口是基于HTTP协议的，<strong>但是查询语句并不是JSON格式</strong>，但是返回结果是标准的JSON格式，从浏览器控制台可以看到具体的请求格式：</p><p><img src="https://i.loli.net/2021/09/05/9KQyfkWHLoPih1c.png" alt="截屏2021-09-05 21.11.52"></p><h1 id="对GraphQL的一些看法"><a href="#对GraphQL的一些看法" class="headerlink" title="对GraphQL的一些看法"></a>对GraphQL的一些看法</h1><p>前面提到了GraphQL相对传统的RESTful接口的优势非常明显，客户端使用接口更加灵活，服务端接口复用程度高。</p><p>如果想把现有的项目迁移到GraphQL上的话需要只需要修改服务端的control层，但是因为GraphQL的查询语句不是传统的JSON格式，所以客户端也需要做相应的更改。</p><p>而GraphQL另一个重要的问题在哪呢，看上面对项目GraphQL的接口调用可以发现接口都是以<code>/graphql</code>这个路径发布的，原来系统传统的根据路径做鉴权的方法基本都失效了，比如在这个DEMO中我为了调通这个接口，把<code>/graphql</code>这个路径加到鉴权的白名单中：</p><p><img src="https://i.loli.net/2021/09/05/b7O39R4m5PsNcdY.png" alt="截屏2021-09-05 21.24.06"></p><p>另外对于一个实体不同的用户能访问到的字段也不一样，比如用户隐藏了手机号，那么其他用户是不能在GraphQL中查询到他的手机号的，这样就必须实现字段级的鉴权了。</p><p>按照GraphQL官方推荐做法，我们应该把接口鉴权放到业务层，但是原来基于路径的鉴权业务层未鉴权的接口就需要做修改了。</p><p>总的来说GraphQL增强了客户端的能力，但是另一方面也算是带来了更多安全隐患，旧系统想要享受GraphQL带来的便利就得在系统的安全策略上做出合理的调整。</p>]]></content:encoded>
      
      <comments>https://marwincn.github.io/2021/09/04/GraphQL%E5%9C%A8Spring%20Boot%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84%E5%AE%9E%E8%B7%B5/#disqus_thread</comments>
    </item>
    
    <item>
      <title>Github Pages 博客优化</title>
      <link>https://marwincn.github.io/2021/09/02/Github%20Pages%E5%8D%9A%E5%AE%A2%E4%BC%98%E5%8C%96/</link>
      <guid>https://marwincn.github.io/2021/09/02/Github%20Pages%E5%8D%9A%E5%AE%A2%E4%BC%98%E5%8C%96/</guid>
      <pubDate>Thu, 02 Sep 2021 16:00:00 GMT</pubDate>
      <description>
      
        &lt;h1 id=&quot;使用Github-Action自动部署&quot;&gt;&lt;a href=&quot;#使用Github-Action自动部署&quot; class=&quot;headerlink&quot; title=&quot;使用Github Action自动部署&quot;&gt;&lt;/a&gt;使用Github Action自动部署&lt;/h1&gt;&lt;p&gt;Github Action是Github提供的免费CI系统，利用Git Hooks可以在提交代码时触发自动化构建流程。对于我使用hexo搭建的博客来说，在我写完博客后push到Github就能触发自动hexo deploy发布博客。&lt;/p&gt;
      
      </description>
      
      
      <content:encoded><![CDATA[<h1 id="使用Github-Action自动部署"><a href="#使用Github-Action自动部署" class="headerlink" title="使用Github Action自动部署"></a>使用Github Action自动部署</h1><p>Github Action是Github提供的免费CI系统，利用Git Hooks可以在提交代码时触发自动化构建流程。对于我使用hexo搭建的博客来说，在我写完博客后push到Github就能触发自动hexo deploy发布博客。</p><span id="more"></span><p>在项目的<code>.github/workflows/</code>创建一个yaml配置文件，也可以在Github Action页面创建该文件：</p><p><img src="https://i.loli.net/2021/09/03/2mF7YHMbaug39d4.png" alt="截屏2021-09-03 21.39.39"></p><p>然后在配置文件中定义自动化的<code>steps</code>：hexo博客需要安装Node.js环境，然后npm install下载依赖，执行hexo deploy即可。我这里使用了actions-gh-pages这个插件，可以定义部署的Git仓库和分支等信息。</p><p>我之前的博客是在<code>marwincn/blog</code>代码仓的<code>gh-page</code>分支部署的Github Pages，但是Github有个限制就是非username.github.io仓库部署的Github Pages发布的地址会带上仓库名的相对路径，如<code>https://marwincn.github.io/blog/</code>。为了去掉<code>/blog/</code>这个相对路径，我还是决定将博客发布到<code>marwincn/marwincn.github.io</code>代码仓。</p><p>actions-gh-pages插件部署的背后就是将渲染后的静态页面push到代码仓，要成功push就要获取代码仓的push权限，对于自动化脚本来说最简单且安全的方法是使用token。首先在Github设置页面生成一个token：</p><p><img src="https://i.loli.net/2021/09/03/8G9LHgOE4uwfVvQ.png" alt="截屏2021-09-03 21.56.18"></p><p>然后将此token设置到配置了Github Action的代码仓的环境变量中，在ymal配置中可以通过<code>$&#123;&#123; secrets.PERSONAL_TOKEN &#125;&#125;</code>访问该环境变量：</p><p><img src="https://i.loli.net/2021/09/03/qjGPfV8l5LeJNvH.png" alt="截屏2021-09-03 21.59.49"></p><p>最后给出我的yaml配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Node.js</span> <span class="string">CI</span></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [ <span class="string">master</span> ]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [ <span class="string">master</span> ]</span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v2</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Use</span> <span class="string">Node.js</span></span><br><span class="line">      <span class="attr">uses:</span> <span class="string">actions/setup-node@v2</span></span><br><span class="line">      <span class="attr">with:</span></span><br><span class="line">        <span class="attr">node-version:</span> <span class="string">&#x27;14.x&#x27;</span></span><br><span class="line">        <span class="attr">cache:</span> <span class="string">&#x27;npm&#x27;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line">      <span class="attr">run:</span> <span class="string">npm</span> <span class="string">install</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span></span><br><span class="line">      <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span></span><br><span class="line">      <span class="attr">uses:</span> <span class="string">peaceiris/actions-gh-pages@v3</span></span><br><span class="line">      <span class="attr">with:</span></span><br><span class="line">        <span class="attr">personal_token:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.PERSONAL_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">external_repository:</span> <span class="string">marwincn/marwincn.github.io</span></span><br><span class="line">        <span class="attr">publish_branch:</span> <span class="string">main</span></span><br><span class="line">        <span class="attr">publish_dir:</span> <span class="string">./public</span></span><br><span class="line">        <span class="attr">cname:</span> <span class="string">marwin.cn</span></span><br><span class="line">        <span class="attr">commit_message:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.event.head_commit.message</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>最后实现的效果，写完博客直接git push，然后Github Action自动部署静态页面到<code>marwincn/marwincn.github.io</code>代码仓，然后更新了博客页面：</p><p><img src="https://i.loli.net/2021/09/03/HZVg8YOrvGB5Mwk.png" alt="截屏2021-09-03 22.07.23"></p><h1 id="CNAME到自定义域名"><a href="#CNAME到自定义域名" class="headerlink" title="CNAME到自定义域名"></a>CNAME到自定义域名</h1><p>如果你正好有个域名，那么可以将该域名CNAME到Github Page实现通过自定义域名访问博客。</p><p>首先在拥有域名的服务商对域名进行CNAME解析：</p><p><img src="https://i.loli.net/2021/09/03/JMwtRoPNl6UmCSH.png" alt="截屏2021-09-03 22.12.23"></p><p>想要实现CNAME单方面的解析还不够，需要在被解析的域名下放置一个CNAME文件，可以在hexo渲染时就带上CNAME文件，我这里使用了actions-gh-pages插件，添加<code>cname: marwin.cn</code>就可以同时发布一个CNAME文件：</p><p><img src="https://i.loli.net/2021/09/03/n3NsZ1WECXm6BUb.png" alt="截屏2021-09-03 22.17.53"></p><p>最终就可以使用自己的域名访问你的博客了。</p><h1 id="Google-Analytics博客访客分析"><a href="#Google-Analytics博客访客分析" class="headerlink" title="Google Analytics博客访客分析"></a>Google Analytics博客访客分析</h1><p>Google Analytics提供免费的网站访客分析，可以在官网<a href="https://analytics.google.com/%E5%88%9B%E5%BB%BA%E5%AA%92%E4%BD%93%E8%B5%84%E6%BA%90%E5%92%8C%E5%BA%94%E7%94%A8%EF%BC%8C%E6%A0%B9%E6%8D%AE%E6%8F%90%E7%A4%BA%E5%88%9B%E5%BB%BA%E8%B7%9F%E8%B8%AA%E7%BD%91%E7%AB%99%E7%9A%84%E4%BB%A3%E7%A0%81%EF%BC%9A">https://analytics.google.com/创建媒体资源和应用，根据提示创建跟踪网站的代码：</a></p><p><img src="https://i.loli.net/2021/09/03/Kf4XBSNAYokr6ud.png" alt="截屏2021-09-03 22.26.34"></p><p>将这段JavaScript标签放到hexo生成静态页面的模板中，hexo渲染生成的每个页面就会带上这段标签，当用户访问你的博客时就会给Google发送请求记录这次访问：</p><p><img src="https://i.loli.net/2021/09/03/znxVoOrXEmatbP2.png" alt="截屏2021-09-03 22.33.04"></p><p>使用Google分析的缺点就是国内用户可能无法访问Google而无法统计到，当然也可以用百度统计来统计访客，但是百度统计的体验并不好，而且Github Pages屏蔽了百度的爬虫，对于程序员群体来说使用Google应该是比较基本的操作，所以使用Google分析挺好的，最终效果如下：</p><p><img src="https://i.loli.net/2021/09/03/XmG3zKDEae64O9g.png" alt="截屏2021-09-03 22.40.09"></p>]]></content:encoded>
      
      <comments>https://marwincn.github.io/2021/09/02/Github%20Pages%E5%8D%9A%E5%AE%A2%E4%BC%98%E5%8C%96/#disqus_thread</comments>
    </item>
    
    <item>
      <title>使用RSS获取资讯</title>
      <link>https://marwincn.github.io/2020/02/27/%E4%BD%BF%E7%94%A8RSS%E8%8E%B7%E5%8F%96%E8%B5%84%E8%AE%AF/</link>
      <guid>https://marwincn.github.io/2020/02/27/%E4%BD%BF%E7%94%A8RSS%E8%8E%B7%E5%8F%96%E8%B5%84%E8%AE%AF/</guid>
      <pubDate>Thu, 27 Feb 2020 13:24:41 GMT</pubDate>
      <description>
      
        &lt;h1 id=&quot;一、RSS是什么&quot;&gt;&lt;a href=&quot;#一、RSS是什么&quot; class=&quot;headerlink&quot; title=&quot;一、RSS是什么&quot;&gt;&lt;/a&gt;一、RSS是什么&lt;/h1&gt;&lt;p&gt;RSS是一种信息聚合的技术，都是为了提供一种更为方便、高效的互联网信息的发布和共享，用更少的时间分享更多的信息。&lt;/p&gt;
&lt;p&gt;简单来说RSS就是以统一的格式发布内容，订阅了某个RSS源的客户端可以接受到该源的更新推送。类似订阅一份报纸后，当天最新的报纸就会准时递到你手中。而由于RSS统一格式的优势，不拘于新闻，各种博客，网站更新，各种app讯息都能订阅，这种统一的概念正是吸引我的地方之一。另外一个吸引我的地方是RSS只会让你接收你想要接收的内容，沉浸式无广告阅读，不会有“猜你喜欢”擅自用AI技术推送消息给你的情况出现。&lt;/p&gt;
      
      </description>
      
      
      <content:encoded><![CDATA[<h1 id="一、RSS是什么"><a href="#一、RSS是什么" class="headerlink" title="一、RSS是什么"></a>一、RSS是什么</h1><p>RSS是一种信息聚合的技术，都是为了提供一种更为方便、高效的互联网信息的发布和共享，用更少的时间分享更多的信息。</p><p>简单来说RSS就是以统一的格式发布内容，订阅了某个RSS源的客户端可以接受到该源的更新推送。类似订阅一份报纸后，当天最新的报纸就会准时递到你手中。而由于RSS统一格式的优势，不拘于新闻，各种博客，网站更新，各种app讯息都能订阅，这种统一的概念正是吸引我的地方之一。另外一个吸引我的地方是RSS只会让你接收你想要接收的内容，沉浸式无广告阅读，不会有“猜你喜欢”擅自用AI技术推送消息给你的情况出现。</p><span id="more"></span> <p>其实我接触RSS也才一年，这段时间我就已经抛弃了大部分的资讯类的APP。事实上使我相见恨晚的RSS并不是什么新东西，早在十几年前RSS就出现了，在各种资讯APP群魔乱舞的时代RSS始终存在于一个小圈子里。</p><h1 id="二、如何使用RSS"><a href="#二、如何使用RSS" class="headerlink" title="二、如何使用RSS"></a>二、如何使用RSS</h1><p>想要用RSS来阅读内容首先要有RSS阅读软件，这些软件搜一下就能找到很多，我目前使用的是Reeder（macOS，iPadOS），RSS Reader Prime（IOS，独立开发者制作的良心应用，完全免费）。</p><p><img src="https://i.loli.net/2019/11/30/5V86HYL1qocpg9M.png" alt="https://i.loli.net/2019/11/30/5V86HYL1qocpg9M.png"></p><p>拥有一个RSS阅读器后，需要找到你想要的订阅的RSS源，一般是一个链接，如少数派的RSS源地址是 <a href="https://sspai.com/feed">https://sspai.com/feed</a> 。在阅读器中添加该订阅源后就能获取到最新文章的推送，一般的RSS阅读器都有已读未读，分类，更换主题等功能，阅读体验比资讯APP更好。另外推荐一个RSS源的索引： <a href="https://docs.rsshub.app/">https://docs.rsshub.app/</a> ，里面有各种订阅源可以满足你的需要。</p><p><img src="https://i.loli.net/2019/11/30/ujt9grOMGSQloam.png" alt="https://i.loli.net/2019/11/30/ujt9grOMGSQloam.png"></p><h1 id="三、我制作的RSS工具"><a href="#三、我制作的RSS工具" class="headerlink" title="三、我制作的RSS工具"></a>三、我制作的RSS工具</h1><p>出于对RSS的喜爱，我想给我自己的博客（ <a href="http://www.marwin.cn/">http://www.marwin.cn</a> ）添加RSS功能。事实上很多使用框架搭好的博客，如WordPress都自带了RSS功能，无奈我的博客是自己写的后台加静态的前端页面。于是我写了一个定时抓取博客更新并制作成RSS源的软件，在自己设定的时间周期里，绑定的各种类型的Trigger能爬取想要的内容的更新，然后生成RSS源。目前只将我自己的博客生成了RSS源： <a href="http://api.marwin.cn/rss/blog">http://api.marwin.cn/rss/blog</a> 。以后可以爬取其他各种感兴趣的内容然后订阅。</p><p><img src="https://tva1.sinaimg.cn/large/006tNbRwgy1g9gb73yii9j306w04u743.jpg" alt="https://tva1.sinaimg.cn/large/006tNbRwgy1g9gb73yii9j306w04u743.jpg"></p><blockquote><p>更新：由于现在使用hexo搭建博客，配合hexo-rss插件能够直接发布RSS，本博客RSS地址为：<code>https://marwincn.github.io/blog/rss2.xml</code></p></blockquote>]]></content:encoded>
      
      <comments>https://marwincn.github.io/2020/02/27/%E4%BD%BF%E7%94%A8RSS%E8%8E%B7%E5%8F%96%E8%B5%84%E8%AE%AF/#disqus_thread</comments>
    </item>
    
    <item>
      <title>记一次应对挖矿程序的经历</title>
      <link>https://marwincn.github.io/2020/02/27/%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%BA%94%E5%AF%B9%E6%8C%96%E7%9F%BF%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%BB%8F%E5%8E%86/</link>
      <guid>https://marwincn.github.io/2020/02/27/%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%BA%94%E5%AF%B9%E6%8C%96%E7%9F%BF%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%BB%8F%E5%8E%86/</guid>
      <pubDate>Thu, 27 Feb 2020 13:24:41 GMT</pubDate>
      <description>
      
        &lt;h1 id=&quot;一、背景&quot;&gt;&lt;a href=&quot;#一、背景&quot; class=&quot;headerlink&quot; title=&quot;一、背景&quot;&gt;&lt;/a&gt;一、背景&lt;/h1&gt;&lt;p&gt;之前为了远程调试redis，将服务器上的redis绑定到了阿里云的公网IP上。后来上redis时发现多了两个key，查看了这两个key发现里面是curl和wget命令，于是猜到有人远程下载了脚本，但是下载的地址已经失效了，无法判断是什么脚本，但是可以肯定服务器被攻击了。后来检查服务器的资源占用，果然cpu占用间断性的会飙到100%，显然被人用来挖矿了。&lt;/p&gt;
      
      </description>
      
      
      <content:encoded><![CDATA[<h1 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h1><p>之前为了远程调试redis，将服务器上的redis绑定到了阿里云的公网IP上。后来上redis时发现多了两个key，查看了这两个key发现里面是curl和wget命令，于是猜到有人远程下载了脚本，但是下载的地址已经失效了，无法判断是什么脚本，但是可以肯定服务器被攻击了。后来检查服务器的资源占用，果然cpu占用间断性的会飙到100%，显然被人用来挖矿了。</p><span id="more"></span> <h1 id="二、关于漏洞的猜测"><a href="#二、关于漏洞的猜测" class="headerlink" title="二、关于漏洞的猜测"></a>二、关于漏洞的猜测</h1><p>redis绑定到公网IP上很容易中招，网上全是类似的帖子（主要还是安全上没有配置好，然后使用了root用户运行了redis-server，感觉自己的辣鸡服务器不会被人盯上所以大意了）。检查了一下redis的配置，dbfilename被设置成<code>～/.ssh/authorized_keys</code>。原来是将攻击者将他的ssh公钥存到redis的key，然后当redis备份key时就将他的公钥存到了authorized_keys成功免密登录了。</p><p>关于之前看到的两个脚本有一种说法是，将dbfilename设置到<code>/etc/crontabs</code>目录下。crontabs是定时执行任务的工具，会强制执行这个目录的文件，于是脚本得以运行。</p><h1 id="三、解决问题"><a href="#三、解决问题" class="headerlink" title="三、解决问题"></a>三、解决问题</h1><p>第一步关闭redis-server，执行shutdown命令的时候发现无法关闭，可能被动了手脚，于是只能kill掉进程。修改redis配置文件，更换密码删掉dbfilename的配置然后重新运行。</p><p>第二步修改ssh的authorized_keys。中间我发现sudo居然都没有权限修改，sudo chmod也不能执行，这里我相当纳闷，root用户也没有权限？后来发现文件是被chattr锁定了，解除锁定前root权限都无法修改。执行<code>chattr -i</code>解除锁定却发现chattr程序直接被攻击者被删除了。好在服务器能联网，重新下载chattr然后修改了authorized_keys权限删除了攻击者的公钥。到现在也才防止了对方继续连接到我的服务器。</p><p>第三步要停止挖矿程序。虽然监测到cpu占用率经常到100%，但是top命令却没有找到任何占用高的进程，使用ps 也是如此。所以说挖矿程序是被隐藏了起来，找不到这个进程要怎么杀掉它？中间我找了各种办法，找操作历史，找时间段修改的文件，到/proc目录下面找也没有什么异常。有没有可能是top和ps程序被替换了？使用<code>stat /usr/bin/top</code>发现程序并没有被修改。就算top，ps程序没有被修改，它们依赖的库可能被修改了，但是排查起来非常麻烦，终于我找到了一个软件unhide（CentOS下），Ubuntu下面也有类似的sysdig可以检测出隐藏的进程。利用unhide很容易就找到了一些隐藏的进程：</p><p><img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g8veag1qftj309b0ch74a.jpg" alt="https://tva1.sinaimg.cn/large/006y8mN6gy1g8veag1qftj309b0ch74a.jpg"></p><p>在这些进程里一眼就能看出pamdicks不是什么正经程序（攻击者前面为了不让我关掉程序可谓使了很多小心机，结果这里挖矿程序的名字却如此裸露，让我一眼发现）。于是google了一下，果然是挖矿程序，据说杀掉这个进程还会继续重启，删掉进程目录也会重新生成，所以我找到进程目录清空然后用chattr给锁定了（这波啊是以其人之道还治其人之身），再杀掉进程，终于cpu占用降下来了。</p><p><img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g8veeoakymj30jo08ymxa.jpg" alt="https://tva1.sinaimg.cn/large/006y8mN6gy1g8veeoakymj30jo08ymxa.jpg"></p>]]></content:encoded>
      
      <comments>https://marwincn.github.io/2020/02/27/%E8%AE%B0%E4%B8%80%E6%AC%A1%E5%BA%94%E5%AF%B9%E6%8C%96%E7%9F%BF%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%BB%8F%E5%8E%86/#disqus_thread</comments>
    </item>
    
    <item>
      <title>使用hexo搭建博客</title>
      <link>https://marwincn.github.io/2020/02/05/%E4%BD%BF%E7%94%A8hexo%E6%90%AD%E5%BB%BA%E5%8D%9A%E5%AE%A2/</link>
      <guid>https://marwincn.github.io/2020/02/05/%E4%BD%BF%E7%94%A8hexo%E6%90%AD%E5%BB%BA%E5%8D%9A%E5%AE%A2/</guid>
      <pubDate>Wed, 05 Feb 2020 12:13:41 GMT</pubDate>
      <description>
      
        &lt;h1 id=&quot;一、背景&quot;&gt;&lt;a href=&quot;#一、背景&quot; class=&quot;headerlink&quot; title=&quot;一、背景&quot;&gt;&lt;/a&gt;一、背景&lt;/h1&gt;&lt;p&gt;本来在自己的vps上自己写了一个博客，但是很多地方需要自己不断维护，自己写的前端也不太满意所以决定用框架搭一个静态博客部署在github page上，这样也省了在vps上到处迁移。&lt;/p&gt;
&lt;p&gt;我选择了人气比较高的Hexo，记录一下自己搭建的过程，作为官方文档的整理和补充。&lt;/p&gt;
      
      </description>
      
      
      <content:encoded><![CDATA[<h1 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h1><p>本来在自己的vps上自己写了一个博客，但是很多地方需要自己不断维护，自己写的前端也不太满意所以决定用框架搭一个静态博客部署在github page上，这样也省了在vps上到处迁移。</p><p>我选择了人气比较高的Hexo，记录一下自己搭建的过程，作为官方文档的整理和补充。</p><span id="more"></span> <h1 id="二、安装"><a href="#二、安装" class="headerlink" title="二、安装"></a>二、安装</h1><p>首先确认电脑里有git和npm （安装node.js即可）的支持，以下为macOS上使用homebrew安装，其他安装方式参考搜索引擎。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 安装git</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> brew install git</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 安装node.js</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> brew install node</span></span><br></pre></td></tr></table></figure><h2 id="安装Hexo"><a href="#安装Hexo" class="headerlink" title="安装Hexo"></a>安装Hexo</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> npm install -g hexo-cli</span></span><br></pre></td></tr></table></figure><p>安装后可以使用hexo命令行，官方的命令文档：<a href="https://hexo.io/zh-cn/docs/commands">文档</a>。</p><h2 id="新建项目"><a href="#新建项目" class="headerlink" title="新建项目"></a>新建项目</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> hexo init &lt;folder&gt;</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> <span class="built_in">cd</span> &lt;folder&gt;</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> npm install</span></span><br></pre></td></tr></table></figure><p>新建完成后，指定文件夹的目录如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── _config.yml     //项目配置文件</span><br><span class="line">├── package.json    //应用程序的信息</span><br><span class="line">├── scaffolds       //模版文件夹</span><br><span class="line">├── source          //存放用户资源和md文件的地方</span><br><span class="line">|   ├── _drafts</span><br><span class="line">|   └── _posts</span><br><span class="line">└── themes          //主题文件夹</span><br></pre></td></tr></table></figure><p>修改<code>_config.yml</code>为自己的信息，参考配置文档：<a href="https://hexo.io/zh-cn/docs/configuration">文档</a>。</p><h2 id="安装主题"><a href="#安装主题" class="headerlink" title="安装主题"></a>安装主题</h2><p>建立项目后，第一件事可能是找一个喜欢的主题，在<a href="https://hexo.io/themes/">主题市场</a>找到自己满意的主题进行接下来的安装，当然也可以自己写一个主题。</p><p>找到选好的主题的github项目，将其clone到你新建的项目的themes目录下，这里的默认的一个主题是landscape。修改项目根目录下的<code>_config.yml</code>（项目配置文件，一般主题目录下也有一个<code>_config.yml</code>，请勿混淆），theme的值改为刚才clone的主题名称。到这里就可以将主题应用到项目上，其他配置参考每个主题的文档。</p><h1 id="三、部署"><a href="#三、部署" class="headerlink" title="三、部署"></a>三、部署</h1><p>根据github的规定，仓库名为<code>&lt;your github name&gt;.github.io</code>时默认该仓库的master分支为github page部署分支，此时首页的url为<code>https://&lt;your github name&gt;.github.io/</code>。而其他命名的仓库可以指定一个分支，如gh-pages作为部署分支，此时页面的url为<code>https://&lt;your github name&gt;.github.io/repositorie-name/</code>。</p><p>由于这种差异，可以有两种部署方式。一是将hexo项目在本地渲染静态页面，然后push到远程；二是将项目代码同步到github仓库，使用CI将远程渲染后的结果发布到gh-pages分支。</p><blockquote><p>GitHub已经可以免费使用GitHub Action做CI了，写完博客直接push就可以部署，参加我的另一篇博客<a href="https://marwin.cn/2021/09/02/Github%20Pages%E5%8D%9A%E5%AE%A2%E4%BC%98%E5%8C%96/">Github Pages 博客优化</a></p></blockquote><h2 id="本地渲染"><a href="#本地渲染" class="headerlink" title="本地渲染"></a>本地渲染</h2><p>由于是部署到github page，所以使用git发布到远程。按照文档，可以使用SFTP等方式发布到自己的服务器上，也可以设置多个发布地址，按顺序发布。</p><p>安装hexo-deployer-git：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> npm install hexo-deployer-git --save</span></span><br></pre></td></tr></table></figure><p>修改<code>_config.yml</code>的参数：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line">  type: git</span><br><span class="line">  repo: &lt;repository url&gt; #git@github.com:xxxx/xxxx.github.io.git</span><br><span class="line">  branch: [branch]</span><br><span class="line">  message: [message]</span><br></pre></td></tr></table></figure><p>需要提前将本地ssh pubkey添加到github的<code>SSH keys</code>里，作为push命令的身份认证。然后只需一条命令就能将网站部署到服务器上：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> hexo clean &amp;&amp; hexo deploy</span></span><br></pre></td></tr></table></figure><h2 id="远程渲染"><a href="#远程渲染" class="headerlink" title="远程渲染"></a>远程渲染</h2><p>参考官方文档：<a href="https://hexo.io/zh-cn/docs/github-pages">github-pages</a></p>]]></content:encoded>
      
      <comments>https://marwincn.github.io/2020/02/05/%E4%BD%BF%E7%94%A8hexo%E6%90%AD%E5%BB%BA%E5%8D%9A%E5%AE%A2/#disqus_thread</comments>
    </item>
    
    <item>
      <title>Spring Boot + Spring Security 应用开启接口跨域</title>
      <link>https://marwincn.github.io/2019/04/07/SpringBoot%E5%BC%80%E5%90%AF%E6%8E%A5%E5%8F%A3%E8%B7%A8%E5%9F%9F/</link>
      <guid>https://marwincn.github.io/2019/04/07/SpringBoot%E5%BC%80%E5%90%AF%E6%8E%A5%E5%8F%A3%E8%B7%A8%E5%9F%9F/</guid>
      <pubDate>Sun, 07 Apr 2019 03:19:33 GMT</pubDate>
      <description>
      
        &lt;h1 id=&quot;一、为何要提供跨域资源服务&quot;&gt;&lt;a href=&quot;#一、为何要提供跨域资源服务&quot; class=&quot;headerlink&quot; title=&quot;一、为何要提供跨域资源服务&quot;&gt;&lt;/a&gt;一、为何要提供跨域资源服务&lt;/h1&gt;&lt;p&gt;最近在写的一个项目后端使用了Spring Boot和Spring Security，前端使用Vue和axios（发送ajax）。为了实现前后端分离，前端和后端能部署在不同的域名下，后端要开启&lt;code&gt;CORS（跨站资源共享）&lt;/code&gt;，保证后端接口能被不同域名下的前端正常请求。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于浏览器的同源策略，跨域请求一般会被拒绝，导致ajax无法访问到后端资源。&lt;br&gt;跨域的几种场景，可以判断自己的网站是否跨域：  &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;域名不同&lt;/li&gt;
&lt;li&gt;域名相同，端口不同&lt;/li&gt;
&lt;li&gt;域名相同，协议不同，如http和https  &lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
      
      </description>
      
      
      <content:encoded><![CDATA[<h1 id="一、为何要提供跨域资源服务"><a href="#一、为何要提供跨域资源服务" class="headerlink" title="一、为何要提供跨域资源服务"></a>一、为何要提供跨域资源服务</h1><p>最近在写的一个项目后端使用了Spring Boot和Spring Security，前端使用Vue和axios（发送ajax）。为了实现前后端分离，前端和后端能部署在不同的域名下，后端要开启<code>CORS（跨站资源共享）</code>，保证后端接口能被不同域名下的前端正常请求。</p><blockquote><p>由于浏览器的同源策略，跨域请求一般会被拒绝，导致ajax无法访问到后端资源。<br>跨域的几种场景，可以判断自己的网站是否跨域：  </p><ol><li>域名不同</li><li>域名相同，端口不同</li><li>域名相同，协议不同，如http和https  </li></ol></blockquote><span id="more"></span> <p>了解了为什么要开启CORS后可以开始编写开启CORS的代码了。</p><h1 id="二、Spring-Boot配置里开启CORS"><a href="#二、Spring-Boot配置里开启CORS" class="headerlink" title="二、Spring Boot配置里开启CORS"></a>二、Spring Boot配置里开启CORS</h1><p>Spring Boot开启CORS只需在配置里添加CORS的支持，直接编写配置类即可：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.cors.CorsConfiguration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.cors.UrlBasedCorsConfigurationSource;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.filter.CorsFilter;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CorsConfig</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> CorsConfiguration <span class="title">buildConfig</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        CorsConfiguration corsConfiguration = <span class="keyword">new</span> CorsConfiguration();</span><br><span class="line">        corsConfiguration.addAllowedOrigin(<span class="string">&quot;*&quot;</span>); <span class="comment">// 允许的域名</span></span><br><span class="line">        corsConfiguration.addAllowedHeader(<span class="string">&quot;*&quot;</span>); <span class="comment">// 允许的请求头</span></span><br><span class="line">        corsConfiguration.addAllowedMethod(<span class="string">&quot;*&quot;</span>); <span class="comment">// 允许的方法（post、get等）</span></span><br><span class="line">        <span class="keyword">return</span> corsConfiguration;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> CorsFilter <span class="title">corsFilter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        UrlBasedCorsConfigurationSource source = <span class="keyword">new</span> UrlBasedCorsConfigurationSource();</span><br><span class="line">        source.registerCorsConfiguration(<span class="string">&quot;/**&quot;</span>, buildConfig()); <span class="comment">// 对接口配置跨域设置</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> CorsFilter(source);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="三、Spring-Security配置"><a href="#三、Spring-Security配置" class="headerlink" title="三、Spring Security配置"></a>三、Spring Security配置</h1><p>由于使用Spring Security后请求都会被它拦截，需要对security也进行配置：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableWebSecurity</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">WebSecurityConfig</span> <span class="keyword">extends</span> <span class="title">WebSecurityConfigurerAdapter</span> </span>&#123;</span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">configure</span><span class="params">(HttpSecurity http)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        http.authorizeRequests()</span><br><span class="line">                <span class="comment">// 如果有允许匿名的url，填在下面</span></span><br><span class="line">                .anyRequest()</span><br><span class="line">                .permitAll()</span><br><span class="line">                .and()</span><br><span class="line">                .formLogin()</span><br><span class="line">                .loginProcessingUrl(<span class="string">&quot;/user/login&quot;</span>)</span><br><span class="line">                <span class="comment">// 自定义处理器，对ajax请求响应json数据</span></span><br><span class="line">                .successHandler(<span class="keyword">new</span> AuthenticationSuccessHandler() &#123;</span><br><span class="line">                    <span class="meta">@Override</span></span><br><span class="line">                    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onAuthenticationSuccess</span><span class="params">(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)</span> <span class="keyword">throws</span> IOException, ServletException </span>&#123;</span><br><span class="line">                        httpServletResponse.setContentType(<span class="string">&quot;application/json;charset=utf-8&quot;</span>);</span><br><span class="line">                        PrintWriter out = httpServletResponse.getWriter();</span><br><span class="line">                        <span class="comment">// 将结果转换成Json数据，Result是自己编写的结果类</span></span><br><span class="line">                        JSONObject result = JSONObject.fromObject(<span class="keyword">new</span> Result(Result.SUCCESS, <span class="string">&quot;登录成功！&quot;</span>));</span><br><span class="line">                        out.write(result.toString());</span><br><span class="line">                        out.flush();</span><br><span class="line">                        out.close();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;)</span><br><span class="line">                <span class="comment">// 自定义处理器，对ajax请求响应json数据</span></span><br><span class="line">                .failureHandler(<span class="keyword">new</span> AuthenticationFailureHandler() &#123;</span><br><span class="line">                    <span class="meta">@Override</span></span><br><span class="line">                    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onAuthenticationFailure</span><span class="params">(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)</span> <span class="keyword">throws</span> IOException, ServletException </span>&#123;</span><br><span class="line">                        httpServletResponse.setContentType(<span class="string">&quot;application/json;charset=utf-8&quot;</span>);</span><br><span class="line">                        PrintWriter out = httpServletResponse.getWriter();</span><br><span class="line">                        <span class="comment">// 将结果转换成Json数据，Result是自己编写的结果类</span></span><br><span class="line">                        JSONObject result = JSONObject.fromObject(<span class="keyword">new</span> Result(Result.ERROR, <span class="string">&quot;登录失败！&quot;</span>));</span><br><span class="line">                        out.write(result.toString());</span><br><span class="line">                        out.flush();</span><br><span class="line">                        out.close();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;)</span><br><span class="line">                .usernameParameter(<span class="string">&quot;username&quot;</span>)</span><br><span class="line">                .passwordParameter(<span class="string">&quot;password&quot;</span>)</span><br><span class="line">                .permitAll()</span><br><span class="line">                .and()</span><br><span class="line">                .logout()</span><br><span class="line">                .logoutUrl(<span class="string">&quot;/logout&quot;</span>)</span><br><span class="line">                .permitAll();</span><br><span class="line">        <span class="comment">// 开启CORS，关闭CSRF跨域</span></span><br><span class="line">        http.cors().and().csrf().disable();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">configure</span><span class="params">(WebSecurity web)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 设置拦截忽略文件夹，可以对静态资源放行</span></span><br><span class="line">        web.ignoring().antMatchers(<span class="string">&quot;/css/**&quot;</span>, <span class="string">&quot;/js/**&quot;</span>, <span class="string">&quot;/js/**&quot;</span>, <span class="string">&quot;/lib/**&quot;</span>, <span class="string">&quot;/upload/**&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里有两个坑：</p><h2 id="设置CORS和CSRF"><a href="#设置CORS和CSRF" class="headerlink" title="设置CORS和CSRF"></a>设置CORS和CSRF</h2><p>一是，下面这一句必须要设置，否则请求会直接403 Forbidden</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http.cors().and().csrf().disable();</span><br></pre></td></tr></table></figure><h2 id="自定义登录处理器"><a href="#自定义登录处理器" class="headerlink" title="自定义登录处理器"></a>自定义登录处理器</h2><p>二是，SpringSecurity通常配置有<code>http.formLogin().successForwardUrl().failureForwardUrl()</code>这几个配置，但是这些配置都是基于服务器渲染页面的模式，在前后端分离项目中登录只用发送携带登录信息的ajax，服务器响应结果即可，开启这些跳转配置会使ajax无法正常获取响应。  </p><p>所以要自定义处理器，登录成功响应对应的Json结果，写到<code>HttpServletResponse</code>中:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> AuthenticationSuccessHandler() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onAuthenticationSuccess</span><span class="params">(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)</span> <span class="keyword">throws</span> IOException, ServletException </span>&#123;</span><br><span class="line">        httpServletResponse.setContentType(<span class="string">&quot;application/json;charset=utf-8&quot;</span>);</span><br><span class="line">        PrintWriter out = httpServletResponse.getWriter();</span><br><span class="line">        <span class="comment">// 将结果转换成Json数据，Result是自己编写的结果类</span></span><br><span class="line">        JSONObject result = JSONObject.fromObject(<span class="keyword">new</span> Result(Result.SUCCESS, <span class="string">&quot;登录成功！&quot;</span>));</span><br><span class="line">        out.write(result.toString());</span><br><span class="line">        out.flush();</span><br><span class="line">        out.close();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="四、前端设置"><a href="#四、前端设置" class="headerlink" title="四、前端设置"></a>四、前端设置</h1><p>在跨域请求的时候，通常会自动发送两个请求。第一个请求方法为OPTION，目的是探测后端是否支持跨域，如果支持跨域再发送真实请求。这里是自动发送不用自己编写代码。  </p><p>然后在发送登录请求的时候，会遇到前端ajax发送了username和password参数，但是后端收不到参数而验证失败。这里的原因是，前端ajax比如axios通常以json格式发送数据，请求头中<code>content-type</code>为<code>application/json</code>，但是Spring Security的登录不支持这种格式所以找不到参数。  </p><p>解决方法有两种，一是重写Spring Security的登录Filter，使其支持json类型数据，操作比较复杂网上有很多相关博客这里就不提了。二是在登录时前端设置数据格式为SpringSecurity支持的<code>application/x-www-form-urlencoded</code>。</p><h1 id="五、总结"><a href="#五、总结" class="headerlink" title="五、总结"></a>五、总结</h1><p>至此CORS就已经开启，前端就可以愉快地使用ajax访问后端了。</p>]]></content:encoded>
      
      <comments>https://marwincn.github.io/2019/04/07/SpringBoot%E5%BC%80%E5%90%AF%E6%8E%A5%E5%8F%A3%E8%B7%A8%E5%9F%9F/#disqus_thread</comments>
    </item>
    
  </channel>
</rss>
