<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Security on I Code It</title>
    <link>https://icodeit.org/categories/security/</link>
    <description>Recent content in Security on I Code It</description>
    <generator>Hugo -- gohugo.io</generator>
    <lastBuildDate>Tue, 10 May 2016 00:00:00 +0000</lastBuildDate><atom:link href="https://icodeit.org/categories/security/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>会话与API安全 - 上</title>
      <link>https://icodeit.org/2016/05/about-session-and-security-api-1/</link>
      <pubDate>Tue, 10 May 2016 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2016/05/about-session-and-security-api-1/</guid>
      <description>在大部分时候，我们讨论API的设计时，会从功能的角度出发定义出完善的，易用的API。而很多时候，非功能需求如安全需求则会在很晚才加入考虑。而往往这部分会涉及很多额外的工作量，比如与外部的SSO集成，Token机制等等</description>
      <content:encoded><![CDATA[<h2 id="保护你的api">保护你的API</h2>
<p>在大部分时候，我们讨论API的设计时，会从功能的角度出发定义出完善的，易用的API。而很多时候，非功能需求如安全需求则会在很晚才加入考虑。而往往这部分会涉及很多额外的工作量，比如与外部的SSO集成，Token机制等等。</p>
<p>这篇文章会以一个简单的例子，从应用程序和部署架构上分别讨论几种常见的模型。这篇文章是这个系列的第一篇，会讨论两个简单的主题：</p>
<ul>
<li>基于Session的用户认证</li>
<li>基于Token的RESTful API（使用Spring Security）</li>
</ul>
<h3 id="使用session">使用Session</h3>
<p>由于HTTP协议本身是无状态的，服务器需要某种机制来区分每个请求。比如在返回给客户的响应中加入一些ID，客户端再次请求时带上这个ID，这样服务器就可以区分出来每个请求，并完成事务性的操作（完成订单的创建，更新，商品派送等等）。</p>
<p>在多数Web容器中，这种机制通过Session来实现。Web容器会为每个首次请求创建一个Session，并将Session的ID以浏览器Cookie的方式返回给客户端。客户端（常常是浏览器）在后续的请求中带上这个Session的ID来表明自己的身份。这种机制同样被用在了鉴权方面，用户登录系统之后，系统分配一个Session ID给他。</p>
<p>除非Session过期，或者用户从客户端的Cookie中主动删了Session ID，否则在服务器端来看，用户的信息会和这个Session绑定起来。后台系统也可以随时知道请求某个资源的真实用户是谁，并以此来判断该用户时候真的有权限这么做。</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="n">HttpSession</span> <span class="n">session</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="n">String</span> <span class="n">user</span> <span class="o">=</span> <span class="o">(</span><span class="n">String</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">&#34;user&#34;</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="c1">//
</span><span class="c1"></span><span class="o">}</span>
</code></pre></div><h4 id="session的问题">Session的问题</h4>
<p>这种做法在小规模应用中工作良好，随着用户的增多，企业往往需要部署多台服务器形成集群来对外提供服务。在集群模式下，当某个节点挂掉之后，由于Session默认是保存在部署Web容器中的，用户会被误判为未登录，后续的请求会被重定向到登陆页面，影响用户体验。</p>
<p>这种将应用程序状态内置的方法已经完全无法满足应用的扩展，因此在工程实践中，我们会采用将Session外置的方式来解决这个问题。即集群中的所有节点都将Session保存在一个公用的键值数据库中：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="nd">@Configuration</span>
<span class="nd">@EnableRedisHttpSession</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HttpSessionConfig</span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div><p>上面这个例子是在Spring Boot中使用Redis来外置Session。Spring会拦截所有对HTTPSession对象的操作，后续的对Session的操作，Spring都会自动转换为与后台的Redis服务器的交互，从而避免节点挂掉之后Session丢失的问题。</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="n">spring</span><span class="o">.</span><span class="na">redis</span><span class="o">.</span><span class="na">host</span><span class="o">=</span><span class="n">192</span><span class="o">.</span><span class="na">168</span><span class="o">.</span><span class="na">99</span><span class="o">.</span><span class="na">100</span>
<span class="n">spring</span><span class="o">.</span><span class="na">redis</span><span class="o">.</span><span class="na">password</span><span class="o">=</span>
<span class="n">spring</span><span class="o">.</span><span class="na">redis</span><span class="o">.</span><span class="na">port</span><span class="o">=</span><span class="n">6379</span>
</code></pre></div><p>如果你跟我一样懒的话，直接启动一个redis的docker container就可以：</p>
<div class="highlight"><pre class="chroma"><code class="language-sh" data-lang="sh">$ docker run --name redis-server -d redis
</code></pre></div><p>这样，多个应用共享这一个实例，任何一个节点的终止、异常都不会产生Session的问题。</p>
<h3 id="基于token的安全机制">基于Token的安全机制</h3>
<p>上面说到的场景中，使用Session需要额外部署一个组件（或者引入更加复杂的Session同步机制），这会带来另外的问题，比如如何保证这个节点的高可用，除了Production环境之外，Staging和QA环境也需要这个组件的配置、测试和维护。</p>
<p>很多项目现在会采用另外一种更加简单的方式：基于Token的安全机制。即不使用Session，用户在登陆之后，会获得一个Token，这个Token会以HTTP Header的方式发送给客户，同样，客户再后续的资源请求中也需要带着这个Token。通常这个Token还会有过期时间的限制（比如只能使用1周，一周之后需要重新获取）。</p>
<p>基于Token的机制更加简单，和RESTful风格的API一起使用更加自然，相较于传统的Web应用，RESTful的消费者可能是人，也可能是Mobile App，也可能是系统中另外的Service。也就是说，并不是所有的消费者都可以处理一个登陆表单！</p>
<h4 id="restful-api">Restful API</h4>
<p>我们通过一个实例来看使用Spring Security保护受限制访问资源的场景。</p>
<p>对于Controller：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">&#34;/protected&#34;</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ProtectedResourceController</span> <span class="o">{</span>

    <span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">&#34;/{id}&#34;</span><span class="o">)</span>
    <span class="kd">public</span> <span class="n">Message</span> <span class="nf">getOne</span><span class="o">(</span><span class="nd">@PathVariable</span><span class="o">(</span><span class="s">&#34;id&#34;</span><span class="o">)</span> <span class="n">String</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="n">Message</span><span class="o">(</span><span class="s">&#34;Protected resource &#34;</span><span class="o">+</span><span class="n">id</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div><p>我们需要所有请求上都带有一个<code>X-Auth-Token</code>的Header，简单起见，如果这个Header有值，我们就认为这个请求已经被授权了。我们在Spring Security中定义这样的一个配置：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><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="n">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">Exception</span> <span class="o">{</span>
    <span class="n">http</span><span class="o">.</span>
            <span class="nf">csrf</span><span class="o">().</span><span class="na">disable</span><span class="o">().</span>
            <span class="n">sessionManagement</span><span class="o">().</span><span class="na">sessionCreationPolicy</span><span class="o">(</span><span class="n">SessionCreationPolicy</span><span class="o">.</span><span class="na">STATELESS</span><span class="o">).</span>
            <span class="n">and</span><span class="o">().</span>
            <span class="n">authorizeRequests</span><span class="o">().</span>
            <span class="n">anyRequest</span><span class="o">().</span>
            <span class="n">authenticated</span><span class="o">().</span>
            <span class="n">and</span><span class="o">().</span>
            <span class="n">exceptionHandling</span><span class="o">().</span>
            <span class="n">authenticationEntryPoint</span><span class="o">(</span><span class="k">new</span> <span class="n">RestAuthenticationEntryPoint</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div><p>我们使用<code>SessionCreationPolicy.STATELESS</code>无状态的Session机制（即Spring不使用HTTPSession），对于所有的请求都做权限校验，这样Spring Security的拦截器会判断所有请求的Header上有没有&quot;X-Auth-Token&quot;。对于异常情况（即当Spring Security发现没有），Spring会启用一个认证入口：<code>new RestAuthenticationEntryPoint</code>。在我们这个场景下，这个入口只是简单的返回一个401即可：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RestAuthenticationEntryPoint</span> <span class="kd">implements</span> <span class="n">AuthenticationEntryPoint</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">commence</span><span class="o">(</span><span class="n">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
                         <span class="n">AuthenticationException</span> <span class="n">authException</span> <span class="o">)</span> <span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
        <span class="n">response</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span> <span class="n">HttpServletResponse</span><span class="o">.</span><span class="na">SC_UNAUTHORIZED</span><span class="o">,</span> <span class="s">&#34;Unauthorized&#34;</span> <span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div><p>这时候，如果我们请求这个受限制的资源：</p>
<div class="highlight"><pre class="chroma"><code class="language-sh" data-lang="sh">$ curl http://api.kanban.com:9000/api/protected/1 -s <span class="p">|</span> jq .
<span class="o">{</span>
  <span class="s2">&#34;timestamp&#34;</span>: 1462621552738,
  <span class="s2">&#34;status&#34;</span>: 401,
  <span class="s2">&#34;error&#34;</span>: <span class="s2">&#34;Unauthorized&#34;</span>,
  <span class="s2">&#34;message&#34;</span>: <span class="s2">&#34;Unauthorized&#34;</span>,
  <span class="s2">&#34;path&#34;</span>: <span class="s2">&#34;/api/protected/1&#34;</span>
<span class="o">}</span>
</code></pre></div><h4 id="过滤器filter及预认证preauthentication">过滤器（Filter）及预认证（PreAuthentication）</h4>
<p>为了让Spring Security可以处理用户登录的case，我们需要提供一个<code>Filter</code>。当然，Spring Security提供了丰富的<code>Filter</code>机制，我们这里使用一个预认证的<code>Filter</code>（即假设用户已经在别的外部系统如SSO中登录了）:</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">KanBanPreAuthenticationFilter</span> <span class="kd">extends</span> <span class="n">AbstractPreAuthenticatedProcessingFilter</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">SSO_TOKEN</span> <span class="o">=</span> <span class="s">&#34;X-Auth-Token&#34;</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">SSO_CREDENTIALS</span> <span class="o">=</span> <span class="s">&#34;N/A&#34;</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">KanBanPreAuthenticationFilter</span><span class="o">(</span><span class="n">AuthenticationManager</span> <span class="n">authenticationManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">setAuthenticationManager</span><span class="o">(</span><span class="n">authenticationManager</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="n">Object</span> <span class="nf">getPreAuthenticatedPrincipal</span><span class="o">(</span><span class="n">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="n">SSO_TOKEN</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="n">Object</span> <span class="nf">getPreAuthenticatedCredentials</span><span class="o">(</span><span class="n">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">SSO_CREDENTIALS</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div><p>过滤器在获得Header中的Token后，Spring Security会尝试去认证用户：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><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="n">AuthenticationManagerBuilder</span> <span class="n">builder</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">Exception</span> <span class="o">{</span>
    <span class="n">builder</span><span class="o">.</span><span class="na">authenticationProvider</span><span class="o">(</span><span class="n">preAuthenticationProvider</span><span class="o">());</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="n">AuthenticationProvider</span> <span class="nf">preAuthenticationProvider</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">PreAuthenticatedAuthenticationProvider</span> <span class="n">provider</span> <span class="o">=</span> <span class="k">new</span> <span class="n">PreAuthenticatedAuthenticationProvider</span><span class="o">();</span>
    <span class="n">provider</span><span class="o">.</span><span class="na">setPreAuthenticatedUserDetailsService</span><span class="o">(</span><span class="k">new</span> <span class="n">KanBanAuthenticationUserDetailsService</span><span class="o">());</span>

    <span class="k">return</span> <span class="n">provider</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div><p>这里的<code>KanBanAuthenticationUserDetailsService</code>是一个实现了Spring Security的UserDetailsService的类：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">KanBanAuthenticationUserDetailsService</span>
        <span class="kd">implements</span> <span class="n">AuthenticationUserDetailsService</span><span class="o">&lt;</span><span class="n">PreAuthenticatedAuthenticationToken</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="n">UserDetails</span> <span class="nf">loadUserDetails</span><span class="o">(</span><span class="n">PreAuthenticatedAuthenticationToken</span> <span class="n">token</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">UsernameNotFoundException</span> <span class="o">{</span>
        <span class="n">String</span> <span class="n">principal</span> <span class="o">=</span> <span class="o">(</span><span class="n">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="k">if</span><span class="o">(!</span><span class="n">StringUtils</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">principal</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">new</span> <span class="n">KanBanUserDetails</span><span class="o">(</span><span class="k">new</span> <span class="n">KanBanUser</span><span class="o">(</span><span class="n">principal</span><span class="o">));</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div><p>这个类的职责是，查看从<code>KanBanPreAuthenticationFilter</code>返回的<code>PreAuthenticatedAuthenticationToken</code>，如果不为空，则表示该用户在系统中存在，并正常加载用户。如果返回null，则表示该认证失败，这时根据配置，Spring Security会重定向到认证入口<code>RestAuthenticationEntryPoint</code>。</p>
<p>加上这个过滤器的配置之后：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><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="n">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">Exception</span> <span class="o">{</span>
    <span class="c1">//...
</span><span class="c1"></span>    <span class="n">http</span><span class="o">.</span><span class="na">addFilter</span><span class="o">(</span><span class="n">headerAuthenticationFilter</span><span class="o">());</span>
<span class="o">}</span>

<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="n">KanBanPreAuthenticationFilter</span> <span class="nf">headerAuthenticationFilter</span><span class="o">()</span> <span class="kd">throws</span> <span class="n">Exception</span> <span class="o">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="n">KanBanPreAuthenticationFilter</span><span class="o">(</span><span class="n">authenticationManager</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div><p>这样，当我们在Header上加上<code>X-Auth-Token</code>之后，就会访问到受限的资源了：</p>
<div class="highlight"><pre class="chroma"><code class="language-sh" data-lang="sh">$ curl -H <span class="s2">&#34;X-Auth-Token: juntao&#34;</span> http://api.kanban.com:9000/api/protected/1 -s <span class="p">|</span> jq .
<span class="o">{</span>
  <span class="s2">&#34;content&#34;</span>: <span class="s2">&#34;Protected resource for 1&#34;</span>
<span class="o">}</span>
</code></pre></div><h3 id="总结">总结</h3>
<p>下一篇文章会以另外一个方式来完成鉴权机制和系统的集成问题。我们会在反向代理中做一些配置，将多个Endpoint组织起来。要完成这样的功能，使用Spring Security也可以做到，不过可能会为应用程序本身引入额外的复杂性。</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
