使用Nginx配合免费的七牛帐号打造自己的CDN加速服务

初次领略nginx的神奇

之前有听说过nginx,当时只不过认为是普通的HTTP前置机一样的服务,确实没想这么好用:反向代理、URL转发、负载均衡,统统集中在这不到2M的软件中,真是让人惊叹。

下载安装nginx

最新版的 nginx 是 nginx-1.11.4
我在本地PC安装测试的是Windows版,Linux版下载和配置是一样的。
下载回来后解压,可以直接鼠标点击nginx.exe运行。(PS:运行之前保证80端口是空闲的)
运行后一闪而过,根本不知道发生了什么。这时候请打开浏览器,输入https://localhost,如果看到nginx的欢迎页面,说明刚才双击nginx.exe已经产生了作用,nginx服务器已经在后台帮你默默运行着一个超轻量级的WEB服务器。

配置nginx实现反向代理Tomcat

OK,目前我们运行的nginx还只能给浏览器响应简单的HTML、CSS、js等静态资源,下面我们通过简单几行配置,让ngnix与处理J2EE的tomcat进行连通,当客户端浏览器访问指定格式的URL时,由nginx统一接管,然后nginx主动访问本地或远程的Tomcat(本地的tomcat需运行在另一个端口上,不要与nginx冲突),得到响应后,再由nginx转发给客户端浏览器,这个流程就叫做「反向代理(Reverse Proxy)」
具体实现方法如下:

  • 设置Tomcat的端口号,避开80端口,使用8080,or any others you like.设置tomcat的conf文件夹下的server.xml,此处不表。
  • 对nginx的配置文件nginx.conf进行修改,此配置文件在nginx文件夹中的conf文件夹中:
http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;

        #转交所有URL给tomcat
        location / {
            proxy_pass https://localhost:8080;
        }

        #下面这个配置将URL指向resources文件夹的路径全部转发给了远程CDN
        location ^~ /resources/ {
            rewrite /resources/(.*) https://files.hexcode.cn/resources/$1;
            expires 7d;
        }
    }
}

原版的nginx.conf文件里面密密麻麻好多注释,第一次使用确实有点绕脑,也有很多名词不曾接触过,我建议立刻马上去除那些注释,去掉那些注释后,真正有用的配置脚本不超过20行,行行重要,字字玑珠。

location / {
    proxy_pass https://localhost:8080;
}

原版配置文件是用root属性,指向了磁盘路径,这里改成proxy_pass属性,指向domain+端口号组成的URL,表示所有请求使用反向代理,让nginx帮客户端浏览器去https://localhost:8080取数据。

将静态资源转移到七牛CDN上

你一定注意到了上面的配置文件,我写上了这么一段:

#下面这个配置将URL指向resources文件夹的路径全部转发给了远程CDN
location ^~ /resources/ {
    rewrite /resources/(.*) https://files.hexcode.cn/resources/$1;
    expires 7d;
}

别着急,听我慢慢道来:
我们 JBlog 的系统,将所有的js,css,fonts等文件全部放在/resources/文件夹下,当时使用SpringMVC的静态资源语法配置的,但这些资源本质上还是要通过本地Tomcat传输给客户端浏览器的,所有的带宽负载和流量计价都是消耗的本地的,如何将这一部分流量和带宽,转嫁到其他云服务上呢,使用nginx会相当方便。
一旦用了nginx,如果你在学SpringMVC时,遇到配置静态资源这一章,可以直接跳过,为什么?因为你配置好nginx后,指定的静态资源压根跑不到tomcat那边去,在nginx这边就帮你转发走了,而且只要你愿意,你可以把这些静态资源全部放到第三方CDN云服务上去加速,这样做是相当有好处的:第一,你的服务器不再传输这些大块头的CSS,JS还有fonts字体文件,节省了带宽,节约了流量;第二,第三方CDN一般带宽会比你买的VPS的带宽高很多,像七牛免费账户有5M以上的带宽给免费用户,还会帮你把文件放置全国多个节点上,让东南西北的浏览器都能极速下载这些静态资源;第三,一般我们很难在Tomcat层级指定某一个CSS文件的缓存策略,但是使用nginx以后,只要你熟悉正则表达式,你可以对任意通配的URL设置独立的缓存策略,这样客户端在访问你动态页面时你可以设置为禁止缓存,但动态页面中嵌入的CSS,你可以独立设置为缓存个一两天。
回到上面的配置文件,我们将URL中指向/resources/的所有文件,重定向到一个七牛免费域名中,同时我们使用七牛的同步软件 qrsbox将resources文件夹完整同步到云端,这样,nginx可以第一时间阻断访问本地资源的操作,让用户全部转向云端服务器下载这些静态资源。其中(.*)是正则表达式,表示任意字符出现任意次数,后面的$1是与其对应的占位值,指向了.*所指代的实际值。
加速,缓存,节约流量,降低本地服务器的带宽负载,nginx太棒了!
nginx还有更多更强大的功能,比如对于分布式服务非常重要的「负载均衡」,我们这里的JBlog项目因为就一台WEB服务器,而且基本上不会有高并发的可能性,遇到DDOS攻击机会也非常小,所以暂时用不到这个高档玩意儿,有兴趣的朋友可以继续研究。

使用Geetest极验进行人机识别

人机验证的历史

「人机识别」在互联网安全领域有着重要而广泛的作用,其主要作用就是防止暴力破解,暴破密码需要高频次的试探操作,每次试探密码要控制在100毫秒级以内才有意义,因此黑客都是编写与我们HTML结构类似的脚本,加上他们自己的黑客字典来自动攻击,黑客字典来自于已破系统的数据库,比如大名鼎鼎的 CSDN ,要注意的是虽然这些字典具有相当高的匹配度,但仍需要多次尝试。
引入人机识别后,脚本被感应屏蔽,只有操作者是真正的人才能通过检测,因此可以大大防范暴力破解,同时降低了系统读取数据库的频次,如果做好拦截策略,甚至可以防范一定程度的DDOS攻击。
那么如何判定操作者是人还是机器呢,随着互联网技术的交替更迭,这一技术有着长远的发展历史。从一开始歪歪扭扭的字符和数字的验证码(这一方法目前是最不安全的,殊不知现在到处的云API提供图形识别的功能,一次识别只需要几分钱,有的甚至免费识别);到弹出一张图片,让用户快速选择是小猫还是小狗,或是其他什么;还有图片上是数学计算公式,让用户计算一下结果;有的是播放音频文件,让用户锻炼一下听力,等等。。。终极的就是发送验证码到用户手机上了,比如一些支付系统。
这些技术要么就是实现起来容易,被破解也容易,要么就是实现起来困难或者会产生额外的成本,破解起来也有难度。今天我想给大家介绍一下远程托管式的人机校验服务。
提起第三方的托管人机校验,最厉害的要数Google的 recaptcha,我试用过两天,部署起来确实方便好用,但可惜的是被墙在外面,一般用户几乎加载不了,体验很差。无奈选择了国产的 Geetest 极验服务。

使用极验

官网介绍的极验的验证流程图如下:

  • 用户进入登陆页面时,请求本地服务器授予 Geetest 令牌
  • 服务器在接受这一请求时,使用Geetest提供的多语言开发包,调用授信函数(该函数的参数包括几个注册账号后提供的密钥),这一函数会向极验服务器发送状态请求,获得Geetest认可后构造一个登陆令牌给本地服务器,本地服务器再转发给浏览器。
  • 如果本地服务器与Geetest服务器连接不畅,不能及时获得Geetest的响应,Geetest官方推荐本地采用备用验证框架,比如验证码什么的,不要影响客户登陆。如果你足够信任Geetest的服务器不宕机,或者不遭受DDOS攻击,那么这一步可以免掉。
  • 客户端在接受到Geetest令牌后,使用JS显示极验的拖动UI,有三种模式显示:嵌入式、浮动式、弹出式。
  • 用户在页面上使用鼠标拖动Geetest的图片拖动功能,将拼图拖动到合理区域,完成验证。根据Geetest官网介绍,Geeteset并不仅仅判断用户是否拖动到正确位置,Geetest会同时对用户的拖动轨迹,时间,以及浏览器的Cookie进行检测,利用一些列行为分析的算法最终计算出屏幕面前的是人还是脚本。
  • 当用户的拖动行为得到Geetest的认可时,Geetest会返回一个认证令牌,嵌入到登陆页面的表单中,当用户提交form时,这一串令牌会被到本地服务器接受,并再次通过多语言API进行Geetest服务器的校验。如果认证成功,则放行进行用户层的密码、用户名校验,如果不成功,说明有可能是脚本在操作,需要拦截。

    具体步骤

    了解了上面整个流程后,我们来编码,首先是登陆页面向本地服务器的令牌请求:

    登陆页面主要代码:

    <form action="/login" id="login-form" method="POST" autocomplete="off" onsubmit="return checkSubmit();">
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
      <input type="text" name="jblog_username" placeholder="username">
      <input type="password" name="jblog_password" placeholder="password">
      <div id="popup-captcha"></div>
      <button id="btn-submit">Login</button>
    </form>
    <script src="https://static.geetest.com/static/tools/gt.js"></script>
    <script type="text/javascript">
      var captchaObj;
      $(function(){
          $.get("${rc.contextPath}/getGeetestStep1Data", function(result){
              if(result.data.success == 1){
                  initGeetest({
                      gt: result.data.gt,
                      challenge: result.data.challenge,
                      product: "popup",
                  }, handlerFloat);
              }
          },"json");
          var handlerFloat = function (me) {
              // 将验证码加到id为popup-captcha的元素里
              captchaObj = me;
              captchaObj.bindOn("#btn-submit");
              captchaObj.appendTo("#popup-captcha");
          };
      });
      function checkSubmit(){
          if(captchaObj.getValidate()){
              return true;
          }else{
              captchaObj.show();
              return false;
          }
      }
    </script>
    

    SpringMVC中的令牌响应:

    /**
    * Geetest验证码输出 
    */
    @RequestMapping(value = "/getGeetestStep1Data", method = RequestMethod.GET)  
    public ModelAndView getGeetestStep1Data(HttpSession session) {  
      Result result = new Result();
    
      GeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);
    
      int gtServerStatus = gtSdk.preProcess();
    
      //将服务器状态设置到session中
      session.setAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY, gtServerStatus);//放入session,用于后面的验证
    
      String resStr = gtSdk.getResponseStr();
    
      result.setData(resStr);
    
      return new ModelAndView("ajaxResult", "result", result);
    }
    

    登陆按钮提交后的Spring Security接受:

    public class GeetestFilter extends UsernamePasswordAuthenticationFilter{
    
      @Override
      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
              throws AuthenticationException {
          String username = request.getParameter("jblog_username");
          String password = request.getParameter("jblog_password");
    
          if(BlogUtils.isEmpty(username, password)){
              throw new UsernameNotFoundException("用户名、密码不能为空!");
          }
    
          //进行Geetest极验校验
          GeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);
    
          String challenge = request.getParameter("geetest_challenge");
          String validate  = request.getParameter("geetest_validate");
          String seccode   = request.getParameter("geetest_seccode");
    
          Integer status = (Integer) request.getSession().getAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY);
    
          //根据Geetest的官方说明,如果Geetest服务器宕机,需要切换到本地验证,暂时这边不设本地验证,Geetest宕机直接通过。
          if(status == 1){
              if(BlogUtils.isEmpty(challenge,validate,seccode) || gtSdk.enhencedValidateRequest(challenge, validate, seccode) == 0 ){
                  System.out.println("未通过人机识别");
                  request.getSession().setAttribute("exceptionMsg", "未通过人机识别");
                  throw new UsernameNotFoundException("人机识别失败,请检查您的行为!");
              }
          }
    
          return super.attemptAuthentication(request, response);
      }
    }
    

    关于最后一段使用Spring Security进行Geetest校验的部分,还有待商榷,因为Spring Security实在是太复杂了,以后我会考虑使用Shiro,或者直接用SpringMVC拦截器进行Geetest的校验。
    最后制作一张使用Geetest进行登陆的GIF:

    其他

    与Geetest服务类似还有一些,比如新兴的 「螺丝帽」
    大致的流程都差不多,就看个人喜好使用了。

页面中「返回顶部」图标按钮的实现

「返回顶部」现在广泛用于各大网站,尤其是页面高度比较高,干货内容比较多的网站,简直风靡一时
如何将「返回顶部」做得美观得体这是个问题,下面跟我一起来设计网站的「返回顶部」图标按钮。

首先定义一个a标签:

<a href="#" title="回到顶部" style="display:none" class="am-icon-btn am-icon-arrow-up" id="goTop"></a>

使用display:none先将其隐藏,这里应用到了AmazeUI的class名,但是我并不想将AmazeUI的css全部引入,毕竟这个页面除了返回顶部,不需要AmazeUI的其他功能。
那就自己来写这几个class的css吧:

#goTop{
    position:fixed;
    bottom:20px;
    right:20px;
    text-decoration:none;
}

这个很容易理解,固定到屏幕右下方。

.am-icon-btn{
    box-sizing:border-box;
    display:inline-block;
    width:48px;height:48px;
    font-size: 24px;
    line-height:48px;
    border-radius:50%;
    background-color:#CCC;
    color:#555555;
    text-align:center
}

这个类主要定义的边框,一个灰色背景的圆形按钮

.am-icon-arrow-up:before{
    content:"\f062";
    font-family: "FontAwesome"
}

这个是定义图标字体的,并且使用了伪类before,表示将会在a标签内部的开始位置添加一个字符\f062,这个字符来自于FontAwesome字体,对此不太了解的可以参考:https://fontawesome.io/icons/
因为并未使用AmazeUI的整个库,那么FontAwesome就要自己来定义了,借此机会熟悉一下CSS中引入新字体的方法:

@font-face {font-family:'FontAwesome';src:url('../amazeui/fonts/fontawesome-webfont.eot?v=4.6.3');src:url('../amazeui/fonts/fontawesome-webfont.eot?#iefix&v=4.6.3') format('embedded-opentype'), url('../amazeui/fonts/fontawesome-webfont.woff2?v=4.6.3') format('woff2'), url('../amazeui/fonts/fontawesome-webfont.woff?v=4.6.3') format('woff'), url('../amazeui/fonts/fontawesome-webfont.ttf?v=4.6.3') format('truetype');font-weight:normal;font-style:normal}

这些eotwoffttf文件是适配不同浏览器差异的,这些图标字体来自于AmazeUI,或者在https://fontawesome.io/icons/ 下载,体积在100KB左右,浏览器将在能成功寻找到的第一个字体库后结束寻找,一般将这个@font-face放到CSS文件的顶部,免得应用图标时找不到字体。
到此为止就设计出「返回顶部」的样式了,下面要写JS来实现其功能:

$('#goTop').click(function(){$('html,body').animate({scrollTop: '0px'}, 800);return false;});
window.onscroll = function () {
    if (document.documentElement.scrollTop + document.body.scrollTop > 100) {
        document.getElementById("goTop").style.display = "block";
    }
    else {
        document.getElementById("goTop").style.display = "none";
    }
}

代码相当简单,第一行使用jQuery的语法,在800毫秒内将html标签和body标签移动到顶部0像素,也就是最顶部。
第二段是监听页面的滚轮事件的,当页面滚轮距离顶部100像素以内时,隐藏我们的a标签,否则显示a标签。

效果如图所示:

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区