为花图相册开启全站https

是否为花图相册开启全站https其实考虑已久:一来是发现目前越来越多的网站都走向了https,即便不是电子商务的网站,例如百度。二是服务器计算能力越来越强大,似乎也不必要在乎加解密,握手所消耗的这点资源。三是出于程序员思维,好东西自然要折腾一番,且不说在Chrome地址栏中绿色的scheme是多么的诱人。

1. SSL证书分类

SSL证书大致可以分为Class 1~5这五种,级别依次递增,数字越高,信用级别越高,自然收费也更高。
Class 1(DV),只验证域名真实性,即保证所访问的域名是真实有效的,这个级别的证书申请也是最简单的,只需提供域名管理者邮箱或者以域名为后缀的几个特定邮箱即可。通常用于个人或邮件。
Class 2(IV)验证域名和个人身份证明。
Class 3(OV)验证域名和组织机构信息。
Class 4(EV)电子商务,网上交易等。
Class 5私有组织或政府部门,比较少见。

这里虽然有等级之分,其实也就是准入门槛越来越高而已,增加了造假难度,但底层加密方式可以都是一样的。

2. 证书申请

这里不得不提到两家免费证书提供商,国内的Wosign和国外的StartSSL,这两家都有试用过,这里简单记录下使用过程:

Wosign的免费证书申请地址隐藏比较深,官网拉倒最下面才有个注册入口。由于是国内的,注册就容易多了,基本上顺着向导很容易就能拿到证书,而且提供的证书非常全面,涵盖了常用的几个服务器,包括Apache,Nginx,Tomcat等,可以说非常便捷。

StartSSL就麻烦多了,首先注册时候填写的资料必须是个人的真实的,或者说看起来要真实。其二需要人工审核,一般也很快,十分钟左右就能收到邮件。而这家最该死的设计就是以后的登录必须靠首次登陆所安装的证书来识别,因此需要备份好证书。可惜的是我这里Chrome下首次安装失败了,建议用IE。意味着我只要退出登录了就再也进不来了。不过并不妨碍继续申请域名证书,首先是验证域名,再申请证书,得到的私钥需要用openssl解密,或者在toolbox中在线解密。然后再下载他们家的sub class1证书附加到域名证书中,才可以正常使用。

在实际测试中,Wosign的免费证书顶级CA居然是StartSSL签发的,Wosign作为一个二级CA,有点奇怪,证书链比StartSSL多一层,理论上验证也要花费更多时间。

3. Nginx服务器配置

主要是增加443端口监听,别忘了在防火墙上也要允许通过443端口。开启ssl,很简单。ssl_ciphers可能还需要优化,目前来讲RC4算法已经不太安全了,可以禁用。由于加密所需要的握手次数和时间都增加不少,因此很有必要调节下ssl_session的缓存大小和超时时间。官方资料是1m cache大概能存储4000个会话,timeout 10m是十分钟。部分代码如下:

listen       80;
listen       443 ssl;
server_name  snapast.com www.snapast.com;

ssl			on;
ssl_certificate		/etc/nginx/ssl/ssl.crt;
ssl_certificate_key	/etc/nginx/ssl/ssl.key;
ssl_protocols		TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 		EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers	on;
ssl_session_cache	shared:SSL:10m;
ssl_session_timeout	10m;

开启后,还需要将非https请求从定向到https,这里我做了两步处理:首先host不等于snapast.com的转为snapast.com,即将带www的转为不带www。scheme不是https的转到带https,同时带上上下文和参数。

if ($host != 'snapast.com') {
rewrite ^/(.*)$ https://snapast.com/$1 permanent;
}

if ($scheme != "https") {
rewrite ^/(.*)$ https://snapast.com/$1 permanent;
}

4. 其他说明

Tomcat也可以开启http所访问,但由于都是内网,因此没必要再开启https了,但需要注意的是,在Tomcat看来还是http的请求,所以通过request.getScheme()得到的依然是http,凡有用到的需要注意一下,容易出现在拼接basePath的地方。

出于安全规范,全站https后,若有脚本等资源还是http引入的浏览器都会拒绝加载,而我之前用的51.la统计工具便不支持https,试过腾讯统计也不支持,目前发现百度统计可以用,因为他的脚本没有限定scheme。

到此就算完成了,欢迎访问花图相册看效果:https://snapast.com:cool:

使用Spring Session做分布式会话管理

  在Web项目开发中,会话管理是一个很重要的部分,用于存储与用户相关的数据。通常是由符合session规范的容器来负责存储管理,也就是一旦容器关闭,重启会导致会话失效。因此打造一个高可用性的系统,必须将session管理从容器中独立出来。而这实现方案有很多种,下面简单介绍下:

  第一种是使用容器扩展来实现,大家比较容易接受的是通过容器插件来实现,比如基于Tomcat的tomcat-redis-session-manager,基于Jetty的jetty-session-redis等等。好处是对项目来说是透明的,无需改动代码。不过前者目前还不支持Tomcat 8,或者说不太完善。个人觉得由于过于依赖容器,一旦容器升级或者更换意味着又得从新来过。并且代码不在项目中,对开发者来说维护也是个问题。

  第二种是自己写一套会话管理的工具类,包括Session管理和Cookie管理,在需要使用会话的时候都从自己的工具类中获取,而工具类后端存储可以放到Redis中。很显然这个方案灵活性最大,但开发需要一些额外的时间。并且系统中存在两套Session方案,很容易弄错而导致取不到数据。

  第三种是使用框架的会话管理工具,也就是本文要说的spring-session,可以理解是替换了Servlet那一套会话管理,既不依赖容器,又不需要改动代码,并且是用了spring-data-redis那一套连接池,可以说是最完美的解决方案。当然,前提是项目要使用Spring Framework才行。

  这里简单记录下整合的过程:

  如果项目之前没有整合过spring-data-redis的话,这一步需要先做,在maven中添加这两个依赖:

<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-redis</artifactId>
	<version>1.5.2.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session</artifactId>
	<version>1.0.2.RELEASE</version>
</dependency>

  再在applicationContext.xml中添加以下bean,用于定义redis的连接池和初始化redis模版操作类,自行替换其中的相关变量。

<!-- redis -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
</bean>

<bean id="jedisConnectionFactory"
	class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
	<property name="hostName" value="${redis.host}" />
	<property name="port" value="${redis.port}" />
	<property name="password" value="${redis.pass}" />
	<property name="timeout" value="${redis.timeout}" />
	<property name="poolConfig" ref="jedisPoolConfig" />
	<property name="usePool" value="true" />
</bean>

<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
	<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>

<!-- 将session放入redis -->
<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
	<property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>

  这里前面几个bean都是操作redis时候使用的,最后一个bean才是spring-session需要用到的,其中的id可以不写或者保持不变,这也是一个约定优先配置的体现。这个bean中又会自动产生多个bean,用于相关操作,极大的简化了我们的配置项。其中有个比较重要的是springSessionRepositoryFilter,它将在下面的代理filter中被调用到。maxInactiveIntervalInSeconds表示超时时间,默认是1800秒。写上述配置的时候我个人习惯采用xml来定义,官方文档中有采用注解来声明一个配置类。

  然后是在web.xml中添加一个session代理filter,通过这个filter来包装Servlet的getSession()。需要注意的是这个filter需要放在所有filter链最前面

<!-- delegatingFilterProxy -->
<filter>
	<filter-name>springSessionRepositoryFilter</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>springSessionRepositoryFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

  这样便配置完毕了,需要注意的是,spring-session要求Redis Server版本不低于2.8

  验证:使用redis-cli就可以查看到session key了,且浏览器Cookie中的jsessionid已经替换为session。

127.0.0.1:6379> KEYS *
1) "spring:session:expirations:1440922740000"
2) "spring:session:sessions:35b48cb4-62f8-440c-afac-9c7e3cfe98d3"

使用Java控制路由器获取公网IP

  不知道是公网IP不够用了,还是什么鬼原因,近期我这的联通ADSL拨号很大程度上获取的是一个10.开头的内网IP。虽说通常情况下无需关心,但跑PT,VPN等速度上大打折扣。投诉无果后只能自己写个脚本来自动更换IP。

  其原理很简单,模拟登录到路由器上检查WANIP是否是10.或0.开头,如是则断开重连,以此循环。代码是Java编写,无任何依赖,运行在树莓派上,24小时监视,在运营商完全分配内网IP之前还可以挣扎一阵子。有需要的朋友可以参考下。

  我这用的是TP-LINK WR720N路由器,设置了局域网IP为192.168.30.1 端口88,通过Chrome登录到路由器,可以在开发者工具中查看到Basic加密的Key,替换相应的位置即可。

查询IP的链接

http://192.168.30.1:88/userRpm/StatusRpm.htm

断开拨号的链接

http://192.168.30.1:88/userRpm/StatusRpm.htm?Disconnect=%B6%CF%20%CF%DF&wan=1

重新拨号的链接

http://192.168.30.1:88/userRpm/StatusRpm.htm?Connect=%C1%AC%20%BD%D3&wan=1

代码如下:

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CheckIP {
	private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
			"yyyy-MM-dd HH:mm:ss");

	public static void main(String[] args) throws Exception {
		do {
			String currentIP = getIP();
			if (currentIP.startsWith("10.") || currentIP.startsWith("0.")) {
				System.out.println(simpleDateFormat.format(new Date())
						+ " 检测到异常:" + currentIP);
				getHtml("http://192.168.30.1:88/userRpm/StatusRpm.htm?Disconnect=%B6%CF%20%CF%DF&wan=1");
				Thread.sleep(1000 * 1);
				getHtml("http://192.168.30.1:88/userRpm/StatusRpm.htm?Connect=%C1%AC%20%BD%D3&wan=1");
				Thread.sleep(1000 * 3);
			}
			Thread.sleep(1000 * 3);
		} while (true);
	}

	private static String getIP() throws Exception {
		String wanPara = getHtml("http://192.168.30.1:88/userRpm/StatusRpm.htm");
		if (null != wanPara) {
			wanPara = wanPara.substring(wanPara.indexOf("var wanPara"),
					wanPara.length());
			wanPara = wanPara.substring(0, wanPara.indexOf(");") + 2);
		}
		return getFirstIp(wanPara);
	}

	private static String getHtml(String address) throws Exception {
		URL url = new URL(address);
		URLConnection connection = url.openConnection();
		connection.setRequestProperty("Authorization", "Basic YWRtaW46d3Npa3Nr");
		connection.connect();
		InputStream inputStream = null;
		StringBuffer stringBuffer = new StringBuffer();
		inputStream = connection.getInputStream();
		BufferedReader bufferedReader = new BufferedReader(
				new InputStreamReader(inputStream));
		String line;
		while ((line = bufferedReader.readLine()) != null) {
			stringBuffer.append(line);
		}
		bufferedReader.close();
		inputStream.close();
		return stringBuffer.toString();
	}

	private static String getFirstIp(String packet) {
		Pattern p = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+");
		Matcher m = p.matcher(packet);
		if (m.find()) {
			return m.group();
		} else {
			return null;
		}
	}
}

编译

pi@raspberrypi ~ $ javac CheckIP.java

后台运行

pi@raspberrypi ~ $ nohup java CheckIP &

日志

pi@raspberrypi ~ $ tail -f nohup.out 

Ubuntu下部署FastDFS 5.05

  很早前得知这样一个开源分布式的文件存储方案,一直没有正式的使用过,正好这次相册升级可以试试了,起初以为fastdfs也是通过block来优化小文件存储的,实际上也是文件形式管理,可以预见大量的小文件还是会有问题,可能更好的解决方案是淘宝TFS。不过fastdfs已远远满足我的需求了。而且及其轻量,这货在google code上早已停止更新,最新的版本可以在这里获取,目前下载的最新版本是5.05,更新于2014-12-02。在这里可以找到更多的说明。

  下载好后,server端分为两个部分,一个是tracker,一个是storage。顾名思义,前者调度管理,负载均衡,后者则是实际的存储节点。两个都能做成集群,以防止单点故障。以前的4.x版本依赖libevent,现在不需要了,只需要libfastcommon。安装方法如下:

1. 下载安装libfastcommon

git clone https://github.com/happyfish100/libfastcommon.git
cd libfastcommon/
./make.sh
./make.sh install

确认make没有错误后,执行安装,64位系统默认会复制到/usr/lib64下。

这时候需要设置环境变量或者创建软链接

export LD_LIBRARY_PATH=/usr/lib64/
ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so

2. 下载安装fastdfs

tar xzf FastDFS.tar.gz
cd FastDFS/
./make.sh
./make.sh install

确认make没有错误后,执行安装,默认会安装到/usr/bin中,并在/etc/fdfs中添加三个配置文件。

3. 修改配置文件
首先将三个文件的名字去掉sample,暂时只修改以下几点,先让fastdfs跑起来,其余参数调优的时候再考虑。
tracker.conf 中修改

base_path=/usr/fastdfs #用于存放日志。

storage.conf 中修改

base_path=/usr/fastdfs-storaged #用于存放日志。
store_path0=/usr/fastdfs0 #存放数据,若不设置默认为前面那个。
tracker_server=192.168.29.132:22122 #指定tracker服务器地址。

client.conf 中同样要修改

base_path=/usr/fastdfs #用于存放日志。
tracker_server=192.168.29.132:22122 #指定tracker服务器地址。

4. 启动tracker和storage

/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf

5. 检查进程

root@ubuntu:~# ps -ef |grep fdfs
root       7819      1  0 15:24 ?        00:00:00 /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf
root       8046      1  0 15:36 ?        00:00:01 fdfs_storaged /etc/fdfs/storage.conf start

表示启动ok了,若有错误,可以在/usr/fastdfs目录下检查日志。

6. 上传/删除测试
使用自带的fdfs_test来测试,使用格式如下:

root@ubuntu:~# fdfs_test /etc/fdfs/client.conf upload /home/steven/01.jpg 
...
group_name=group1, ip_addr=192.168.29.132, port=23000
storage_upload_by_filename
group_name=group1, remote_filename=M00/00/00/wKgdhFTV0ZmAP3AZAPk-Io7D4w8580.jpg
...
example file url: http://192.168.29.132/group1/M00/00/00/wKgdhFTV0ZmAP3AZAPk-Io7D4w8580.jpg
storage_upload_slave_by_filename
group_name=group1, remote_filename=M00/00/00/wKgdhFTV0ZmAP3AZAPk-Io7D4w8580_big.jpg
...
example file url: http://192.168.29.132/group1/M00/00/00/wKgdhFTV0ZmAP3AZAPk-Io7D4w8580_big.jpg

使用fdfs_delete_file来删除文件,格式如下:

fdfs_delete_file /etc/fdfs/client.conf group1/M00/00/00/wKgdhFTV11uAXgKWAPk-Io7D4w8667.jpg

可以看到,上传ok了,这里会生成两个文件,这是fastdfs的主/从文件特性,以后再介绍。example file url是不能在浏览器中直接打开的,除非配合nginx使用,这里我不需要了。删除文件需要完整的group_name和remote_filename。

Ubuntu下编译和部署Hadoop 2.2

  在Ubuntu下编译各种项目总会遇到不少的问题,尤其是缺少各种依赖,好友都推荐用CentOS。不过个人对Ubuntu这个品牌以及背后的Canonical公司比较喜欢,加之用过其桌面版多年,算是熟悉了。这里记录下编译和部署Hadoop 2.2的一些过程,以供快速参考。

0、虚拟机环境准备
  宿主机器配置i7 8g内存,我这里用VMware虚拟了4台Ubuntu Server 64bit,每台机器分配1g内存。主机名分别为h0,h1,h2,h3,配置好每台机器的/etc/hosts,将各主机名映射到私有ip。

127.0.0.1       localhost
192.168.10.33   h0
192.168.10.176  h1
192.168.10.207  h2
192.168.10.90   h3

  配置免密码ssh登录,这里只将h0作为namenode和secondarynamenode,h1~3作为datanode,应此只需要配置h0到h1~3的免密码登录,以及各虚拟机自己的免密码登录。

ssh-keygen -t rsa
cat id_rsa.pub >> authorized_keys
scp authorized_keys ubuntu@h1:~/.ssh/authorized_keys_from_h0
scp authorized_keys ubuntu@h2:~/.ssh/authorized_keys_from_h0
scp authorized_keys ubuntu@h3:~/.ssh/authorized_keys_from_h0

1、安装编译环境
  由于Hadoop官方提供的只有32位环境,所以需要自己编译64位的,以下步骤仅在h0上操作。首先准备编译需要的组件,这里随系统决定所需。当然Java运行环境也是必须的,apt-get安装OpenJDK即可。

sudo apt-get install subversion maven cmake libssl-dev

2、签出Hadoop 2.2源代码

svn checkout http://svn.apache.org/repos/asf/hadoop/common/tags/release-2.2.0/

3、添加jetty-util依赖
  编辑vim hadoop-common-project/hadoop-auth/pom.xml,添加:

<dependency>
   <groupId>org.mortbay.jetty</groupId>
   <artifactId>jetty-util</artifactId>
   <scope>test</scope>
</dependency>

4、编译安装protobuf 2.5

wget https://protobuf.googlecode.com/files/protobuf-2.5.0.tar.bz2
tar jxvf protobuf-2.5.0.tar.bz2
cd protobuf-2.5.0/
./configure
make
sudo make install

添加环境变量,或者重启一次

export LD_LIBRARY_PATH=/usr/local/lib/

5、执行编译
首次执行:

mvn package -Pdist,native -DskipTests -Dtar

如果有错误,根据错误原因处理,一般都是缺少依赖。装好后重复执行即可,也可以添加-e -X参数,打印更多的日志来帮助查找错误。成功后可以看到以下的输出:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 46:40.307s
[INFO] Finished at: Fri Jan 17 17:32:23 CST 2014
[INFO] Final Memory: 66M/188M
[INFO] ------------------------------------------------------------------------

6、安装配置
  建立以下文件夹:

~/hadoop/dfs/data
~/hadoop/dfs/name
~/hadoop/temp

  编译好的Hadoop可以在release-2.2.0/hadoop-dist/target/hadoop-2.2.0/找到,我这里将其复制到~/hadoop下,切换到目录:

cd ~/hadoop/hadoop-2.2.0/etc/hadoop/

配置以下几个文件,如果没有的,可以在复制改名对应的template。

hadoop-env.sh
修改JAVA_HOME 为系统Java Home环境,apt-get安装的OpenJDK一般是这个路径。

export JAVA_HOME=/usr/lib/jvm/default-java

core-site.xml
ubuntu为运行Hadoop的用户。

<configuration>
     <property>
          <name>fs.defaultFS</name>
          <value>hdfs://h0:9000</value>
     </property>
     <property>
          <name>io.file.buffer.size</name>
          <value>131072</value>
     </property>
     <property>
          <name>hadoop.tmp.dir</name>
          <value>file:/home/ubuntu/hadoop/temp</value>
          <description>Abase for other temporary directories.</description>
     </property>
     <property>
          <name>hadoop.proxyuser.ubuntu.hosts</name>
          <value>*</value>
     </property>
     <property>
          <name>hadoop.proxyuser.ubuntu.groups</name>
          <value>*</value>
     </property>
</configuration>

hdfs-site.xml

<configuration>
     <property>
          <name>dfs.namenode.secondary.http-address</name>
          <value>h0:9001</value>
     </property>
     <property>
          <name>dfs.namenode.name.dir</name>
          <value>file:/home/ubuntu/hadoop/dfs/name</value>
     </property>
     <property>
          <name>dfs.datanode.data.dir</name>
          <value>file:/home/ubuntu/hadoop/dfs/data</value>
     </property>
     <property>
          <name>dfs.replication</name>
          <value>3</value>
     </property>
     <property>
          <name>dfs.webhdfs.enabled</name>
          <value>true</value>
     </property>
</configuration>

mapred-site.xml

<configuration>
     <property>
          <name>dfs.namenode.secondary.http-address</name>
          <value>h0:9001</value>
     </property>
     <property>
          <name>dfs.namenode.name.dir</name>
          <value>file:/home/ubuntu/hadoop/dfs/name</value>
     </property>
     <property>
          <name>dfs.datanode.data.dir</name>
          <value>file:/home/ubuntu/hadoop/dfs/data</value>
     </property>
     <property>
          <name>dfs.replication</name>
          <value>3</value>
     </property>
     <property>
          <name>dfs.webhdfs.enabled</name>
          <value>true</value>
     </property>
</configuration>

yarn-site.xml

<configuration>
     <property>
          <name>yarn.nodemanager.aux-services</name>
          <value>mapreduce_shuffle</value>
     </property>
     <property>
          <name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name>
          <value>org.apache.hadoop.mapred.ShuffleHandler</value>
     </property>
     <property>
          <name>yarn.resourcemanager.address</name>
          <value>h0:8032</value>
     </property>
     <property>
          <name>yarn.resourcemanager.scheduler.address</name>
          <value>h0:8030</value>
     </property>
     <property>
          <name>yarn.resourcemanager.resource-tracker.address</name>
          <value>h0:8031</value>
     </property>
     <property>
          <name>yarn.resourcemanager.admin.address</name>
          <value>h0:8033</value>
     </property>
     <property>
          <name>yarn.resourcemanager.webapp.address</name>
          <value>h0:8088</value>
     </property>
</configuration>

slaves
定义节点的主机名:

h1
h2
h3

  至此,配置就算完成了,将Hadoop目录复制到另外三台机器上,放相同的目录下。

scp -r hadoop-2.2.0/ h1:~/hadoop/
scp -r hadoop-2.2.0/ h2:~/hadoop/
scp -r hadoop-2.2.0/ h3:~/hadoop/

7、启动Hadoop

在h0上执行:

./sbin/start-dfs.sh
./sbin/start-yarn.sh

  用jps可以查看Java进程所对应的服务。

ubuntu@h0:~/hadoop/hadoop-2.2.0$ jps
1902 NameNode
1604 SecondaryNameNode
2076 ResourceManager

ubuntu@h1:~$ jps
1354 DataNode
1123 NodeManager

ubuntu@h2:~$ jps
1146 DataNode
1378 NodeManager

ubuntu@h3:~$ jps
1139 DataNode
1371 NodeManager

  到此,namenode和datanode都启动完毕。