vert.x笔记:3.使用vert.x发布restful接口

vert.x重要概念介绍:

在第2偏笔记中,我们写了第一个vert.x的hello world代码,这里,我们把代码中用到的几个重要概念解释下。

Vertx基类:

Vertx类,是所有vert.x代码的入口,官方代码注释为:

The entry point into the Vert.x Core API.

即该类是所有vert.x core包API的总入口,简单理解就是,所有核心功能的API,都需要该类去调用,所有的核心功能也都需要该类提供环境上下文。

HttpServer:

官方注释:

An HTTP and WebSockets server

http/https/websockets服务器,vert.x发布restful服务,不需要tomcat提供servlet容器,它自己就是一台性能强大的web服务器,并且原生支持负载均衡,后面我们会讲到。

Router类:

先看官方代码注释:

A router receives request from an HttpServer and routes it to the first matching Route that it contains. A router can contain many routes.

Router类可以理解为一个路由器,他接收httpserver带来的请求,并将不同的请求分发到不同的路由中,如果简单对比一下spring mvc的话,可以将router理解为spring mvc中的dispatcher。

route:

route代表一条路由,同样,对比spring mvc,相当于spring中的@RequestMapping,他指定了restful api的请求接口路径,并将其交给handler来处理该条路由。

Handler:

首先来看官方代码注释:

Specify a request handler for the route. The router routes requests to handlers depending on whether the various criteria such as method, path, etc match. There can be only one request handler for a route. If you set this more than once it will overwrite the previous handler.

handler处理具体的路由请求,字面上讲就是处理某个具体的restful api。他与httpserver,router,route的关系可以用如下流程表示:

来自httpserver的request请求-->交由路由器做分发处理-->路由器匹配到具体的路由规则-->路由到最终的handler去处理请求

vert.x默认提供了很多处理器,包括但不局限于以下:

AuthHandler 处理权限校验支持
BodyHandler 提供所有请求上下文
CookieHandler 提供cookie支持
SessionHandler 提供session支持

RoutingContext:

官方代码注释:

Represents the context for the handling of a request in Vert.x-Web.

很简单,请求上下文,可以理解为servlet中的httprequest和httpresponse
####Verticle:

A verticle is a piece of code that can be deployed by Vert.x.

verticle是vert.x中,可被部署运行的最小代码块,可以理解为一个verticle就是一个最小化的业务处理引擎。
verticle被发布部署后,会调用其内部的start方法,开始业务逻辑处理,完成后会调用stop方法,对该代码块执行销毁动作

vert.x发布restufl api

新建类:RestServer,代码如下

package com.heartlifes.vertx.demo.simple;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;

public class RestServer extends AbstractVerticle {

    public static void main(String[] args) {
        // 获取vertx基类
        Vertx vertx = Vertx.vertx();
        // 部署发布rest服务
        vertx.deployVerticle(new RestServer());
    }

    // 重写start方法,加入我们的rest服务处理逻辑
    @Override
    public void start() throws Exception {
        // 实例化一个路由器出来,用来路由不同的rest接口
        Router router = Router.router(vertx);
        // 增加一个处理器,将请求的上下文信息,放到RoutingContext中
        router.route().handler(BodyHandler.create());
        // 处理一个post方法的rest接口
        router.post("/post/:param1/:param2").handler(this::handlePost);
        // 处理一个get方法的rest接口
        router.get("/get/:param1/:param2").handler(this::handleGet);
        // 创建一个httpserver,监听8080端口,并交由路由器分发处理用户请求
        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
    }

    // 处理post请求的handler
    private void handlePost(RoutingContext context) {
        // 从上下文获取请求参数,类似于从httprequest中获取parameter一样
        String param1 = context.request().getParam("param1");
        String param2 = context.request().getParam("param2");

        if (isBlank(param1) || isBlank(param2)) {
            // 如果参数空,交由httpserver提供默认的400错误界面
            context.response().setStatusCode(400).end();
        }

        JsonObject obj = new JsonObject();
        obj.put("method", "post").put("param1", param1).put("param2", param2);

        // 申明response类型为json格式,结束response并且输出json字符串
        context.response().putHeader("content-type", "application/json")
                .end(obj.encodePrettily());
    }

    // 逻辑同post方法
    private void handleGet(RoutingContext context) {
        String param1 = context.request().getParam("param1");
        String param2 = context.request().getParam("param2");

        if (isBlank(param1) || isBlank(param2)) {
            context.response().setStatusCode(400).end();
        }
        JsonObject obj = new JsonObject();
        obj.put("method", "get").put("param1", param1).put("param2", param2);

        context.response().putHeader("content-type", "application/json")
                .end(obj.encodePrettily());
    }

    private boolean isBlank(String str) {
        if (str == null || "".equals(str))
            return true;
        return false;
    }

}

执行代码,打开浏览器,输入以下接口

http://localhost:8080/get/1/2
http://localhost:8080/post/1/2

处理session代码示例:

package com.heartlifes.vertx.demo.simple;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.Session;
import io.vertx.ext.web.handler.CookieHandler;
import io.vertx.ext.web.handler.SessionHandler;
import io.vertx.ext.web.sstore.LocalSessionStore;

public class SessionServer extends AbstractVerticle {

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new SessionServer());
    }

    @Override
    public void start() throws Exception {
        Router router = Router.router(vertx);
        // 增加cookies处理器,解码cookies,并将其放到context上下文中
        router.route().handler(CookieHandler.create());
        // 增加session处理器,为每次用户请求,维护一个唯一的session,这里使用内存session,后面会讲分布式的session存储
        router.route().handler(
                SessionHandler.create(LocalSessionStore.create(vertx)));
        router.route().handler(routingContext -> {
            // 从请求上下文获取session
                Session session = routingContext.session();
                Integer count = session.get("count");
                if (count == null)
                    count = 0;
                count++;
                session.put("count", count);

                routingContext.response()
                        .putHeader("content-type", "text/html")
                        .end("total visit count:" + session.get("count"));
            });

        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
    }

}

处理cookies代码示例:

package com.heartlifes.vertx.demo.simple;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Cookie;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.CookieHandler;

public class CookieServer extends AbstractVerticle {

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new CookieServer());
    }

    @Override
    public void start() throws Exception {
        Router router = Router.router(vertx);
        router.route().handler(CookieHandler.create());
        router.route().handler(
                routingContext -> {
                    Cookie cookie = routingContext.getCookie("testCookie");
                    Integer c = 0;
                    if (cookie != null) {
                        String count = cookie.getValue();
                        try {
                            c = Integer.valueOf(count);
                        } catch (Exception e) {
                            c = 0;
                        }
                        c++;
                    }

                    routingContext.addCookie(Cookie.cookie("testCookie",
                            String.valueOf(c)));
                    routingContext.response()
                            .putHeader("content-type", "text/html")
                            .end("total visit count:" + c);
                });

        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
    }

}

vert.x笔记:2.hello vert.x–第一个vert.x hello world工程

假设:

本文及以下系列文章,假设你已经对jdk1.8新特性中的函数式编程及lambda匿名函数有一定了解,并会熟练使用maven。

开发环境配置:

使用最新版的vert.x 3.0,需要安装jdk1.8
maven需要3.0以上版本,推荐直接使用最新版
jdk及maven如何配置,参考百度教程

ide需求:myeclipse 2015 stable1.0及以上或者eclipse 4.4及以上

第一个maven工程

新建pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.heartlifes</groupId>
    <artifactId>vertx-demo</artifactId>
    <version>3.0.0</version>
    <dependencies>
        <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-core</artifactId>
        <version>${project.version}</version>
    </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-web</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
    <build>
        <pluginManagement>
            <plugins>
            <!-- We specify the Maven compiler plugin as we need to set it to Java 1.8 -->
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <compilerArgs>
                            <arg>-Acodetrans.output=${project.basedir}/src/main</arg>
                        </compilerArgs>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

生成eclipse工程:

mvn eclipse:eclipse

至此,一个基于vert.x框架空工程就创建完成了

第一个hello world代码

新建HelloWorld类:

package com.heartlifes.vertx.demo.hello;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;

public class HelloWorld extends AbstractVerticle {

    @Override
    public void start() throws Exception {
        Router router = Router.router(vertx);
        router.route().handler(
                routingContext -> {
                    routingContext.response()
                            .putHeader("content-type", "text/html")
                            .end("hello vert.x");
                });

        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
    }

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new HelloWorld());
    }
}

执行代码,在浏览器中输入localhost:8080,看看返回了什么。

至此,第一个vert.x的hello world工程搭建完毕,我们会在后面的章节里解释每行代码的作用。

vert.x笔记:1.vert.x介绍

直接转载csdn上的文章:http://www.csdn.net/article/2015-05-20/2824733-Java

Vert.x简介

在Java20周年之际,Java用户对Java的抱怨与日俱增,比如内存管理、笨重的JavaEE等。而Java依然在TIOBE编程语言排行榜上艰难的维持第一名的位置,随着一些新编程语言的兴起,这个领域目前呈现一种混战的态势。

在这种背景下,Java届的小鲜肉框架——Vert.x于2015年5月7日发布了3.0-milestone5版本,距离计划6月22日发布Vert.x3.0.0-final越来越近了,Vert.x用户组的粉丝们近期已经迫不及待地在宇宙中心(注:北京五道口)组织了一次Vert.x中国用户组Meetup,针对Vert.x工程化开发问题以及Vert.x3新特性展开了探讨。Vert.x(http://vertx.io/)是一个基于JVM、轻量级、高性能的应用平台,非常适用于最新的移动端后台、互联网、企业应用架构。

Vert.x基于全异步Java服务器Netty,并扩展出了很多有用的特性。Vert.x的亮点有:

同时支持多种编程语言——目前已经支持了Java、JavaScript、Ruby、Python、Groovy、Clojure、Ceylon等。对程序员来说,直接好处就是可以使用各种语言丰富的LIB,同时也不再为编程语言选型而纠结;

异步无锁编程——经典的多线程编程模型能满足很多Web开发场景,但随着移动互联网并发连接数的猛增,多线程并发控制模型性能难以扩展,同时要想控制好并发锁需要较高的技巧,目前Reactor异步编程模型开始跑马圈地,而Vert.x就是这种异步无锁编程的一个首选;

对各种IO的丰富支持——目前Vert.x的异步模型已支持TCP、UDP、FileSystem、DNS、EventBus、Sockjs等;

极好的分布式开发支持——Vert.x通过EventBus事件总线,可以轻松编写分布式解耦的程序,具有很好的扩展性;

生态体系日趋成熟——Vert.x归入Eclipse基金会门下,异步驱动已经支持了Postgres、MySQL、MongoDB、Redis等常用组件,并且有若干Vert.x在生产环境中的应用案例。

Reactor模式

和传统Java框架的多线程模型相比,Vert.x Netty是 Reactor模式的Java实现。考古了一下Reactor模式, 其理论最早由Washington University的Douglas C. Schmidt教授在1995年提出,在《Proactor – An Object Behavioral Pattern for Demultiplexing and Dispatching Handlers for Asynchronous Events 》这篇论文中做了 完整介绍。

图1-6是对其关键原理部分展开分析。

[请输入图片描述](http://img.ptcms.csdn.net/article/201505/20/555c4aa7a1d95.jpg)

图1 一个经典Web Server在收到Web浏览器请求后的处理过程

[请输入图片描述](http://img.ptcms.csdn.net/article/201505/20/555c4ac80015a.jpg)

图2 一个经典Web Server使用多线程模型,并发处理来自多个Web浏 览器的请求

[请输入图片描述](http://img.ptcms.csdn.net/article/201505/20/555c4ad096c8f.jpg)

图3 Web浏览器连接到一个Reactor模式的Web Server处理过程。利 用了Initiation Dispatcher组件,把耗时的IO操作事件注册到Initiation Dispatcher组件

[请输入图片描述](http://img.ptcms.csdn.net/article/201505/20/555c4aeec8ad2.jpg)

图4 Web浏览器访问一个Reactor模式的Web Server处理过程。耗时IO 操作由其它线程执行,IO执行完成后通知Initiation Dispatcher,再回到 Http Handler执行

[请输入图片描述](http://img.ptcms.csdn.net/article/201505/20/555c4b083e44c.jpg)

图5 Web浏览器连接一个Proactor模式的Web Server处理过程。和Reactor的主要区别是耗时IO操作交给操作系统异步IO库执行(例如 GNU/Linux aio),操作系统异步IO库执行完毕后,通过异步IO通知机制(例如epoll)触发Completion Dispatch,再交给Http Handler执行

[请输入图片描述](http://img.ptcms.csdn.net/article/201505/20/555c4b1251f6b.jpg)

图6 Web浏览器访问一个Proactor模式的Web Server处理过程。和Reactor的主要区别是耗时IO操作交给操作系统异步IO库执行(例如 GNU/Linux aio),操作系统异步IO库执行完毕后,通过异步IO通知机制(例如epoll)触发Completion Dispatch,再交给Http Handler执行

事实上,Vert.x/Netty的Reactor实现部分是在Netty 4.0如上述所示的代码中实现,和上述图中能对应的几个类是io.netty.channel.nio.NioEventLoop,io.netty. channel.epoll.EpollEventLoop,java.nio.channels.spi.SelectorProvide。

Vert.x3.0的更新

Vert.x3.0是对Vert.x2.x的重大升级,不仅仅是package从org.vertx到io.vertx的全面替换,一些重要的核心类也都做了破坏式的重构,几乎很难从vert.x2程序升级到vert.x3.0程序。建议新项目直接从Vert.x3.0开始。以下是Vert.x3的一些功能升级:

Vert.x2.x中的模块体系去掉了。目前Vert.x3.0推荐用Maven的模块体系,当然不仅限于Maven;支持其他语言在Vert.x上的代码生成;
Vert.x3.0项目构建,从Gradle改为Maven;为了更好地利用Java8的Lambdas表达式,只支持Java8;默认采用扁平的classpath结构;
Verticle工厂方式简化;支持用编程的方式实例化Verticle、以及部署Verticle实例;当你阻塞Eventloop主线程时警告,阻塞Reactor主线程是一种错误的使用方式;移除了PlatformManager模块;集群管理可以用编程的方式调用支持集群节点之间的共享数据;完全重写了HTTPclient,更完善;
WebSocketAPI改善;
SSL/TLS的改善;
Eventbus的API改善;
支持Eventbus代理;增加了扩展项目集’ext’stack;
增加了MongoService,支持MongoDB的纯异步驱动;
实现ReactiveStreams;
对reactive-streams的实现;
支持Options类的使用,可以构造函数带参数进去;
更完整的样例工程。请见:https://github.com/vert-x3/example-proj

druid监控配置及sql注入防火墙配置

druid是阿里巴巴开发的为监控而生的数据库连接池,可以非常直观的看到当前应用的数据源、sql执行情况、sql防火墙、web应用、uri监控、spring接口调用监控等。

数据源配置:

<bean id="readAccount" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
    <property name="driverClassName" value="${jdbc_read.driver}" />
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${jdbc_read.url}" />
    <property name="username" value="${jdbc_read.username}" />
    <property name="password" value="${jdbc_read.password}" />
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="${jdbc.pool.minIdle}" />
    <property name="minIdle" value="${jdbc.pool.minIdle}" />
    <property name="maxActive" value="${jdbc.pool.maxActive}" />
    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="30000" />
    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="30000" />
    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="90000" />
    <property name="validationQuery" value="SELECT 'x'" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />
</bean>

开启web监控:

在数据源配置中,增加以下属性

<property name="filters" value="stat" />

在web.xml中增加以下配置

<filter>
    <filter-name>DruidWebStatFilter</filter-name>
    <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
    <init-param>
        <param-name>exclusions</param-name>
        <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>DruidWebStatFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<servlet>
    <servlet-name>DruidStatView</servlet-name>
    <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>DruidStatView</servlet-name>
    <url-pattern>/druid/*</url-pattern>
</servlet-mapping>

开启sql防火墙:

在数据源配置中,增加以下属性

<property name="filters" value="stat,wall"/>

开启spring方法调用监控:

在spring配置文件中增加以下配置

<bean id="druid-stat-interceptor" class="com.alibaba.druid.support.spring.stat.DruidStatInterceptor"></bean>
<bean id="druid-stat-pointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut" scope="prototype">
    <property name="patterns">
        <list>
            <value>com.xxx.*</value>
            <value>com.xxx1.*</value>
        </list>
    </property>
</bean>
<aop:config>
    <aop:advisor advice-ref="druid-stat-interceptor" pointcut-ref="druid-stat-pointcut" />
</aop:config>

常用linux日志查询命令

1.查看实时日志:

tail -f nohup.out

2.分页查看所有日志:

cat nohup.out | more

4.分页查看前N行日志:

tail -n 1000 nohup.out | more

5.查看实时日志并检索关键字:

tail -f nohup.out | grep "关键字"

6.检索日志,并显示该条日志的前后N行记录:

cat nohup.out | grep -n -B10 -A10 "关键字"

7.查看日志,从第1000行开始,显示500行:

cat nohup.out | tail -n +1000| head -n 500
### 8.查看日志,显示1000行到1500行:
```shell
cat nohup.out | head -n 1500| tail -n +1000

9.删除包括关键词的行:

sed -i '/关键词/d' nohup.out

Vmware虚拟机磁盘lvm扩容

背景:

vmware中开虚拟机的时候是直接拷贝镜像的,结果原有磁盘大小不够,于是另外置备了一块磁盘,但是新置备的磁盘不能直接挂上原来的lvm,故需要扩容lvm

扩容lvm步骤:

1.查看硬盘情况

fdisk -l

2.找到新挂载的磁盘,并做分区

fdisk /dev/sda

The number of cylinders for this disk is set to 7832.
There is nothing wrong with that, but this is larger than 1024,
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
(e.g., DOS FDISK, OS/2 FDISK)

Command (m for help): n 说明:新增分区
Command action
e extended
p primary partition (1-4)
p
Partition number (1-4): 3 说明:新增分区号(1,2默认已经用了)
First cylinder (2611-7832, default 2611): 默认回车(最小)
Using default value 2611
Last cylinder or +size or +sizeM or +sizeK (2611-7832, default 7832):默认回车(最大)
Using default value 7832

Command (m for help): t 说明:修改分区类型
Partition number (1-4): 3 说明:修改分区类型对应的分区号
Hex code (type L to list codes): 8e 说明:8e是lvm磁盘类型
Changed system type of partition 3 to 8e (Linux LVM)

Command (m for help): p 说明:打印分区表

Disk /dev/sda: 64.4 GB, 64424509440 bytes
255 heads, 63 sectors/track, 7832 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

Device Boot Start End Blocks Id System
/dev/sda1 * 1 13 104391 83 Linux
/dev/sda2 14 2610 20860402+ 8e Linux LVM
/dev/sda3 2611 7832 41945715 8e Linux LVM

Command (m for help): w 说明:保存退出
The partition table has been altered!

3.重启系统

reboot

4.查看硬盘情况,检查磁盘分区是否成功

fdisk -l

5.查看当前磁盘分区类型

df -T /dev/sda1

6.在新的分区上创建文件系统

mkfs.ext4 /dev/sda3

7.创建pv

pvcreate /dev/sda3

8.查看pv

pvdisplay

7.查看vg

vgdisplay

8.把创建的pv加入7中查看到的vg

vgextend vg_centerostmp /dev/sda3 

9.查看lv

lvdisplay

10.把vg加入9中查看的lv

lvextend -l +25599 /dev/vg_centerostmp/lv_root 

11.调整文件系统大小

resize2fs /dev/vg_centerostmp/lv_root

cas3.5.2集群化部署及定制开发

集群化方案:

1.tomcat集群共享session
2.持久化票根st及tgt
3.持久化service
4.修改ServiceManager,从内存共享改为redis共享

tomcat集群共享session

之所以要共享session,是因为cas使用了spring-webflow,而webflow使用session存储中间变量,如果不共享session,会直接导致登录流程因为缺少中间变量而失败,具体表现为输入正确用户名密码后,界面刷新重新进入登录界面。

session共享在tomcat中有三种方案可供选择,分别是:1.tomcat原始集群共享。2.redis session持久化共享。3.memcache session持久化共享。

这里我选用了tomcat原始的集群共享,原因是redis session持久化实验失败,session是成功持久化到redis中了,但是登录流程还是失败,memcache由于没有环境,没有试验。

配制tomcat集群

1.在server.xml的Engine元素下加入以下配制

    <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
      <Manager className="org.apache.catalina.ha.session.DeltaManager"
             expireSessionsOnShutdown="false"
             notifyListenersOnReplication="true"/>
      <Channel className="org.apache.catalina.tribes.group.GroupChannel">
        <Membership
            className="org.apache.catalina.tribes.membership.McastService"
            address="228.0.0.4"
            port="45564"
            frequency="500"
            dropTime="3000"
            mcastTTL="1"/>
        <Receiver
            className="org.apache.catalina.tribes.transport.nio.NioReceiver"
            address="xxx"
            port="4001"
            autoBind="0"
            selectorTimeout="100"
            maxThreads="6"/>
        <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
          <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
        </Sender>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
      </Channel>
      <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
           filter=".*\.gif;.*\.js;.*\.jpg;.*\.htm;.*\.html;.*\.txt;"/>
      <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
    </Cluster>

上面的address=”xxx”,替换成你自己的服务器ip地址

2.在工程web.xml的开头加入配置项

<distributable />

持久化票根

票根的持久化,cas默认就提供了支持,我们所要做的就是把相应的持久化类使用起来,在配置文件中替换掉原来的内存存储类。
1.pom中增加以下依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.18</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.6.2</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.5.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>4.1.0.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.1.0.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <scope>runtime</scope>
    <version>4.2.0.Final</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.1</version>
</dependency>

以上依赖包括mysql驱动,hibernate相关包,druid数据连接池及redis驱动(redis用于后面service持久化)

2.applicationContext.xml文件修改
增加以下配置项:

<tx:annotation-driven transaction-manager="transactionManager" />

<context:component-scan base-package="com" />

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
    <property name="driverClassName" value="${jdbc.driver}" />
    <!-- 基本属性 url、user、password -->
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="${jdbc.pool.minIdle}" />
    <property name="minIdle" value="${jdbc.pool.minIdle}" />
    <property name="maxActive" value="${jdbc.pool.maxActive}" />
    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="30000" />
    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="30000" />
    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="90000" />
    <property name="validationQuery" value="SELECT 'x'" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"    p:dataSource-ref="dataSource" p:jpaVendorAdapter-ref="jpaVendorAdapter">
    <property name="jpaProperties">
        <props>
        <prop key="hibernate.dialect">${database.dialect}</prop>
        <prop key="hibernate.hbm2ddl.auto">update</prop>
        <prop key="hibernate.jdbc.batch_size">${database.batchSize}</prop>
    </props>
    </property>
</bean>

<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" p:generateDdl="true" p:showSql="true" />

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" />

<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

2.ticketRegistry.xml文件修改
查找ticketRegistry,修改原bean声明如下:

<bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.JpaTicketRegistry" />

持久化service

1.deployerConfigContext.xml文件修改
查找serviceRegistryDao,修改bean声明

<bean id="serviceRegistryDao" class="org.jasig.cas.services.JpaServiceRegistryDaoImpl"      p:entityManagerFactory-ref="entityManagerFactory" />

修改ServiceManger

cas自带的DefaultServicesManagerImpl的集群模式,是通过直接将所有的service存在内存的一个set中,然后通过quartz,每两分钟reload一把全量service,这种2分钟同步一次service的集群模式,显然不能正式上线使用,这里我们通过自己实现ServiceManager,采用redis,对所有service进行统一管理。

1.增加RedisServicesManagerImpl类:

public class RedisServicesManagerImpl implements ServicesManager {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final static String SPLIT = ",";

    private final static String REDIS_KEY = "registedService";

    @NotNull
    private ServiceRegistryDao serviceRegistryDao;

    private RegisteredService disabledRegisteredService;

    private RedisTemplate<String, RegisteredService> redisTemplate;

    private RegexRegisteredService defaultService = new RegexRegisteredService();

    public RedisServicesManagerImpl(final ServiceRegistryDao serviceRegistryDao,            final RedisTemplate<String, RegisteredService> redisTemplate) {
        this.serviceRegistryDao = serviceRegistryDao;
        this.disabledRegisteredService = constructDefaultRegisteredService(new ArrayList<String>());
        this.redisTemplate = redisTemplate;

        constructDefaultService();
    }

    @Transactional(readOnly = false)
    @Audit(action = "DELETE_SERVICE", actionResolverName = "DELETE_SERVICE_ACTION_RESOLVER", resourceResolverName = "DELETE_SERVICE_RESOURCE_RESOLVER")
    public synchronized RegisteredService delete(final long id) {
        final RegisteredService r = findServiceBy(id);
        if (r == null) {
            return null;
        }
        log.info("delete service by id..." + r.getServiceId() + "..."
                + r.getId());
        this.serviceRegistryDao.delete(r);
        String key = r.getId() + SPLIT + r.getServiceId();
        // redisTemplate.opsForValue().getOperations().delete(key);
        redisTemplate.opsForHash().delete(REDIS_KEY, key);
        return r;
    }

    public RegisteredService findServiceBy(final Service service) {
        if (service != null) {
            log.info("find service by service..." + service.getId() + "..."
                    + service.getClass());
        }
        Collection<RegisteredService> c = getAllServices();
        if (c.isEmpty()) {
            log.info("find service by service...service is blank");
            return this.disabledRegisteredService;
        }

        for (final RegisteredService r : c) {
            if (r.matches(service)) {
                log.info("find service by service...service is a match...in service..."
                        + service.getId()
                        + "...with redis..."
                        + r.getServiceId());
                return r;
            }
        }
        log.info("find service by service...service not match");
        return null;
    }

    public RegisteredService findServiceBy(final long id) {
        log.info("find service by id..." + id);
        Map<Object, Object> map = redisTemplate.opsForHash().entries(REDIS_KEY);
        Set<Entry<Object, Object>> set = map.entrySet();
        Iterator<Entry<Object, Object>> it = set.iterator();
        while (it.hasNext()) {
            Entry<Object, Object> entry = it.next();
            String key = entry.getKey().toString();
            RegisteredService value = (RegisteredService) entry.getKey();
            log.info("find service by id...service in redis..." + key);
            if (String.valueOf(id).equals(key.split(SPLIT)[0])) {
                log.info("find service by id...match..." + key);
                try {
                    return (RegisteredService) value.clone();
                } catch (final CloneNotSupportedException e) {
                    return value;
                }
            }
        }
        return null;
    }

    public Collection<RegisteredService> getAllServices() {
        log.info("get all services...");
        Set<RegisteredService> services = new TreeSet<RegisteredService>();
        Map<Object, Object> map = redisTemplate.opsForHash().entries(REDIS_KEY);
        Set<Entry<Object, Object>> set = map.entrySet();
        Iterator<Entry<Object, Object>> it = set.iterator();
        while (it.hasNext()) {
            Entry<Object, Object> entry = it.next();

            log.info("get all services...service in redis..." + entry.getKey()
                    + "..." + entry.getValue().getClass());

            String key = entry.getKey().toString();
            if (entry.getValue() instanceof RegisteredService) {
                RegisteredService value = (RegisteredService) entry.getValue();
                log.info("get all services...service in redis..." + key);
                services.add(value);
            }
        }
        if (!services.contains(defaultService)) {
            services.add(defaultService);
        }
        return services;
    }

    public boolean matchesExistingService(final Service service) {
        return findServiceBy(service) != null;
    }

    @Transactional(readOnly = false)
    @Audit(action = "SAVE_SERVICE", actionResolverName = "SAVE_SERVICE_ACTION_RESOLVER", resourceResolverName = "SAVE_SERVICE_RESOURCE_RESOLVER")
    public synchronized RegisteredService save(
            final RegisteredService registeredService) {
        log.info("save service..." + registeredService.getServiceId());
        final RegisteredService r = this.serviceRegistryDao
                .save(registeredService);

        String key = registeredService.getId() + SPLIT
                + registeredService.getServiceId();
        log.info("save service in redis..." + key);
        // redisTemplate.opsForValue().set(key, registeredService);
        redisTemplate.opsForHash().put(REDIS_KEY, key, registeredService);
        return r;
    }

    private RegisteredService constructDefaultRegisteredService(
            final List<String> attributes) {
        final RegisteredServiceImpl r = new RegisteredServiceImpl();
        r.setAllowedToProxy(true);
        r.setAnonymousAccess(false);
        r.setEnabled(true);
        r.setSsoEnabled(true);
        r.setAllowedAttributes(attributes);

        if (attributes == null || attributes.isEmpty()) {
            r.setIgnoreAttributes(true);
        }

        return r;
    }

    private void constructDefaultService() {
        defaultService.setId(0);
        defaultService.setName("HTTP and IMAP");
        defaultService.setDescription("Allows HTTP(S) and IMAP(S) protocols");
        defaultService.setServiceId("^(https?|imaps?)://.*");
        defaultService.setEvaluationOrder(10000001);
    }

}

2.applicationContext.xml文件修改
增加以下配置项:

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxTotal" value="${redis.pool.maxActive}" />
    <property name="maxIdle" value="${redis.pool.maxIdle}" />
    <property name="maxWaitMillis" value="${redis.pool.maxWait}" />
    <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
</bean>

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

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

修改以下配置项:

<bean id="servicesManager" class="com.wondersgroup.cas.services.RedisServicesManagerImpl">
    <constructor-arg index="0" ref="serviceRegistryDao" />
    <constructor-arg index="1" ref="redisTemplate" />
</bean>

centos下安装,简单配置redis

下载:

wget http://download.redis.io/releases/redis-3.0.2.tar.gz

解压:

tar xzvf redis-3.0.2.tar.gz

安装编译环境:

yum install -y gcc

编译安装依赖:

进入redis的deps目录

编译安装hiredis:

make
make install

编译安装jemalloc:

./configure
make
make install

编译安装linenoise:

make

编译安装lua:

yum install -y readline-devel ncurses-devel
make linux
make install

编译安装:

make

此时报错:

collect2: ld 返回 1

需要安装最新的tcl

wget http://jaist.dl.sourceforge.net/project/tcl/Tcl/8.6.4/tcl8.6.4-src.tar.gz
./configure
make
make test
make install

然后,做以下两步:
在src/Makefile开头加 CFLAGS= -march=x86-64
编辑src/.make-settings里的OPT,改为OPT=-O2 -march=x86-64

重新编译redis:

make
make test
make install

简单配置:

vim redis.conf

修改密码:requirepass xxx

启动:

nohup ./redis-server ../redis.conf &

nginx中使用pfx格式的ssl证书

首先,nginx在编译安装时得安装ssl模块
上传ssl证书到服务器/usr/local/nginx/ssl/xxx.pfx

生成证书crt可key

openssl pkcs12 -in /usr/local/nginx/ssl/xxx.pfx -clcerts -nokeys -out /usr/local/nginx/ssl/xxx.crt
openssl pkcs12 -in /usr/local/nginx/ssl/xxx.pfx -nocerts -nodes -out /usr/local/nginx/ssl/xxx.rsa

验证证书正确性

openssl s_server -www -accept 443 -cert /usr/local/nginx/ssl/xxx.crt -key /usr/local/nginx/ssl/xxx.rsa

配置nginx

server {  
    listen 443;  
    server_name localhost;
    ssl on;  
    ssl_certificate /usr/local/nginx/ssl/xxx.crt;  
    ssl_certificate_key /usr/local/nginx/ssl/xxx.rsa;  
    ssl_session_timeout 5m;  
    ssl_protocols SSLv2 SSLv3 TLSv1;  
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;  
    ssl_prefer_server_ciphers on;  
    location ~ /api/(.*) {
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Ssl on;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://serverAPI;
        }
    }

spring-oauth集群负载的cas单点登出问题

背景:

前端有N台由spring-oauth,spring-cas搭建的提供oauth2服务的服务器,后端有单台cas搭建的sso单点登录服务器,通过nginx的iphash保证用户在同一会话工程中始终登录在固定的一台oauth2服务器上。

现象:

cas3.5默认不支持集群环境下的单点登出,导致当用户使用oauth服务时,出现单点故障,具体表现为:
用户A在浏览器上完成整个oauth流程后,此时
1.用户A在单点登录服务器上点击登出按钮
2.系统提示用户登出成功
3.用户B在同一个浏览器上访问oauth服务器,此时没有要求用户B登录,还是用户A的登录信息,并且后续oauth流程报错

原因:

假设oauth服务部署在A,B两台机器上,提供负载访问。SSO单点服务部署在C机器上。
1.用户在C机上登出时,C机器上的SSO服务删除C服务器中的session,并且清空存在用户浏览器中的cookies
2.C服务器中的sso服务通知A,B中部署的oauth服务,用户已经退出,请求oauth服务清空自己的session缓存。
3.此时,由于A,B是负载设置,CAS通知的oauth登出服务,其实只是通知到了A或B中的一台。
4.假设通知到的是A服务器,此时A服务器删除oauth中的session缓存,而B服务器中的oauth session缓存依旧存在
5.用户再次使用oauth服务,此时,由于集群原因,用户可能正好使用到的是B服务器上的oauth服务,由于B服务器中session依旧存在,结果出现单点登出故障。

解决方案:

翻遍了google中所有的讨论,结果毫无进展。
尝试了使用jedis来存储session,结果发现session中存储的AuthorizationRequest类,没有实现序列化接口,无法实体化到redis中,无奈之下,使用了一种监听广播的方式

1.重写SingleSignOutFilter类中的doFilter方法

if (handler.isTokenRequest(request)) {
    handler.recordSession(request);
} else if (handler.isLogoutRequest(request)) {
    String from = request.getParameter("from");
    if (StringUtils.isEmpty(from)) {
            multiCastToDestroy(request);
    }
    handler.destroySession(request);
    // Do not continue up filter chain
    return;
} else {
    log.trace("Ignoring URI " + request.getRequestURI());
}

增加multiCastToDestroy方法

private void multiCastToDestroy(HttpServletRequest request) {
    String logoutMessage = CommonUtils.safeGetParameter(request,"logoutRequest");
    ExecutorService executors = Executors.newFixedThreadPool(100);
    String[] tmps = multicastUrls.split(",");
    for (String tmp : tmps) {
        String[] urls = tmp.split("=");
        String key = urls[0];
        String url = urls[1];
        executors.submit(new MessageSender(url, logoutMessage, ownKey,5000, 5000, true));
    }
}

增加MessageSender内部类

private static final class MessageSender implements Callable<Boolean> {
    private String url;
    private String message;
    private String from;
    private int readTimeout;
    private int connectionTimeout;
    private boolean followRedirects;

    public MessageSender(final String url, final String message,final String from, final int readTimeout,final int connectionTimeout, final boolean followRedirects) {
        this.url = url;
        this.message = message;
        this.from = from;
        this.readTimeout = readTimeout;
        this.connectionTimeout = connectionTimeout;
        this.followRedirects = followRedirects;
    }

    public Boolean call() throws Exception {
        HttpURLConnection connection = null;
        BufferedReader in = null;
        try {
            System.out.println("Attempting to access " + url);
            final URL logoutUrl = new URL(url);
            final String output = "from=" + from + "&logoutRequest="+ URLEncoder.encode(message, "UTF-8");
            connection = (HttpURLConnection) logoutUrl.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setReadTimeout(this.readTimeout);
            connection.setConnectTimeout(this.connectionTimeout);
            connection.setInstanceFollowRedirects(this.followRedirects);
            connection.setRequestProperty("Content-Length",Integer.toString(output.getBytes().length));
            connection.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            final DataOutputStream printout = new DataOutputStream(connection.getOutputStream());
            printout.writeBytes(output);
            printout.flush();
            printout.close();
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                while (in.readLine() != null) {
                // nothing to do
            }

            System.out.println("Finished sending message to" + url);
            return true;
        } catch (final SocketTimeoutException e) {
            e.printStackTrace();
            return false;
        } catch (final Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (final IOException e) {
                    // can't do anything
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
}

修改oauth配置文件:
增加以下配置:

<bean id="singleLogoutFilter" class="com.xxx.xxx.cas.filter.SingleSignOutFilter">
    <property name="multicastUrls"      value="127.0.0.1=http://127.0.0.1/api/j_spring_cas_security_check,localhost=http://localhost/api/j_spring_cas_security_check" />
    <property name="ownKey" value="localhost" />
</bean>

这样,当CAS通知到A服务器去做登出操作时,A服务器会广播给其他几台服务器同步去做登出操作,通过广播的方式解决单点登出的故障