搭建基于OAuth2和SSO的开放平台

搭建基于OAuth2和SSO的开放平台

原创文章,转载或摘录请说明文章来源,谢谢!

开放平台介绍

什么是开放平台

开放平台在百科中的定义:
开放平台(Open Platform) 在软件行业和网络中,开放平台是指软件系统通过公开其应用程序编程接口(API)或函数(function)来使外部的程序可以增加该软件系统的功能或使用该软件系统的资源,而不需要更改该软件系统的源代码。

通俗或者说应景点的说法,开放平台,就是互联网企业,将其内部的资源(一般是数据),比如用户数据,平台业务数据,以技术的手段(一般是RESTFul接口API),开放给受控的第三方合作伙伴,活公司内部的其它一些产品,形成一个安全受控的资源暴露平台。

为什么要搭建开放平台

搭建开放平台的意义,一般在于:
1.搭建基于API的生态体系
2.利用开放平台,搭建基于计费的API数据平台
3.为APP端提供统一接口管控平台,类似于网关的概念
4.为第三方合作伙伴的业务对接提供授信可控的技术对接平台

开放平台体系结构图

open

开放平台核心模块

一个典型的开放平台,至少包含以下几个核心模块:
1.平台门户
平台门户负责向第三方展示用于进行业务及技术集成的管理界面,至少包含以下几个功能:
1.服务商入住(第三方合作伙伴入住)
2.应用配置(第三方应用管理)
3.权限申请(一般包括接口权限和字段权限)
4.运维中心(开放平台当前服务器、接口状态,服务商接口告警等)
5.帮助中心(入住流程说明,快速接入说明,API文档等)

2.鉴权服务
鉴权服务负责整个平台的安全性
1.接口调用鉴权(第三方合作伙伴是否有权限调用某接口)
2.用户授权管理(用户对某个第三方应用获取改用户信息的权限管理)
3.用户鉴权(平台用户的鉴权)
4.应用鉴权(第三方合作伙伴的应用是否有权调用该平台)

3.开放接口
开放接口用于将平台数据暴露给合作伙伴
1.平台用户接口(用于获取公司APP生态链中的用户信息)
2.平台数据接口(平台中的一些开放数据)
3.其它业务接口(平台开放的一些业务数据)

4.运营系统
运营系统是整个平台的后台业务管理系统,负责对第三方合作伙伴提出的各种申请进行审核操作,对当前应用的操作进行审计工作,对当前业务健康度进行监控等
1.服务商管理(对第三方合作伙伴的资质进行审核、操作)
2.应用管理(对第三方应用进行审核、上下线管理)
3.权限管理(对合作伙伴申请的资源进行审核、操作)
4.统计分析(监控平台当前运行状态,统计平台业务数据)

OAuth2介绍

什么是OAuth2

百科:OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。oAuth是Open Authorization的简写。

简单来说:OAuth2协议,定义了一套用户、第三方服务和存储着用户数据的平台之间的交互规则,可以使得用户无需将自己的用户名和密码暴露给第三方,即可使第三方应用获取用户在该平台上的数据,最常见的场景便是现在互联网上的各种使用XXX账号登录。

OAuth2协议中角色介绍

OAuth2协议中,共有四个参与方(角色):
1.resource owner:资源拥有者
即用户
2.resource server:资源服务器
即存储用户数据的服务器,一般对外都以RESTFul API的形式暴露用户数据,client使用access token访问resource server申请被保护起来的用户数据
3.client:客户端
即第三方应用
4.authorization server:授权服务器
用来鉴权第三方应用合法性,并对用户登录、是否授权第三方应用获取数据进行响应,并根据用户操作,想第三应用颁发用户token或者告知授权失败

OAuth2常用协议介绍

OAUTH2标准业务协议,如下图所示
oauth
A.第三方应用向用户请求授权,希望获取用户数据
B.用户同意授权
C.第三方应用拿着用户授权,向平台索要用户access token
D.平台校验第三应用合法性及用户授权真实性后,向平台发放用户access token
E.第三方应用拿着用户access token向平台索要用户数据
F.平台在校验用户access token真实性后,返回用户数据

OAuth2使用场景介绍

目前,OAuth2协议使用最多的场景还是用以给第三方应用获取用户信息,业务流程如下图所示
case
1.在浏览器中,用户点击第三方应用按钮,由第三方应用发起请求,向平台发起授权请求。
2.平台在接收到第三方应用请求后,浏览器跳转用户登录界面,请求用户进行登录。
3.用户在平台登录界面输入用户名、密码进行登录
4.平台判断用户合法性,校验失败,在浏览器中提示错误原因
5.平台判断用户是否需要对该第三方应用进行授权。(不需要授权的情况有两种:a.平台信任该第三方应用,如公司内部应用,无需用户进行授权,默认给予用户数据。b.该用户之前已经给该应用授予过权限,并且仍在有效期内)
6.如需授权,平台跳转浏览器界面至授权界面,告知用户将授予哪个第三方哪些数据权限
7.用户授权后,将用户授权码回调给第三方url
8.第三方在获取用户授权码后,带着用户授权码访问平台鉴权接口,请求用户token
9.平台在收到第三方请求后,校验授权码真实性,并返回用户token
10.第三方使用用户token向平台请求用户接口
11.平台接口判断用户token真实性,并向第三方返回用户数据

OAuth2核心功能说明

1.应用注册
应用注册后,OAuth2会下发应用app_id和app_secret,用以标记该应用的唯一性,并且这两个参数将贯穿整个OAuth协议,用以对应用合法性进行校验。同时,应用需要提供redirect_uri,用以和平台进行异步交互,获取用户令牌及错误信息。
2.授权/鉴权中心
a.对用户的应户名、密码进行鉴权
b.对第三方应用的app_id,app_secret进行鉴权
c.展示授权界面,并对用户对第三方应用的授权操作进行响应
d.对用户授权码及用户token的真实性进行鉴权
3.token管理
a.创建token、刷新token
b.查询token详细数据
c.校验token时效性

OAuth2体系结构

case

开放平台集成OAuth2体系

1.平台门户:
门户应用入住界面,需要集成OAuth2应用创建接口,录入第三方回调地址,并回显app_id和app_secret参数
2.鉴权服务:
鉴权服务需集成OAuth2的authorize及token接口,用以提供用户授权及code/token鉴权功能
3.开放接口:
开放接口需集成OAuth2的resource server角色,对用户数据进行安全管理,对第三方应用发起的请求做出响应,并对token进行真实性校验
4.运营系统:
运营系统需提供对当前OAuth2应用的管理功能,用户授权列表管理,用户token管理等OAuth2协议相关管理功能。

SSO介绍

什么是SSO

百科:SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。

简单来说,SSO出现的目的在于解决同一产品体系中,多应用共享用户session的需求。SSO通过将用户登录信息映射到浏览器cookie中,解决其它应用免登获取用户session的问题。

为什么需要SSO

开放平台业务本身不需要SSO,但是如果平台的普通用户也可以在申请后成为一个应用开发者,那么就需要将平台加入到公司的整体账号体系中去,另外,对于企业级场景来说,一般都会有SSO系统,充当统一的账号校验入口。

CAS协议中概念介绍

SSO单点登录只是一个方案,而目前市面上最流行的单端登录系统是由耶鲁大学开发的CAS系统,而由其实现的CAS协议,也成为目前SSO协议中的既定协议,下文中的单点登录协议及结构,均为CAS中的体现结构
CAS协议中有以下几个概念:
1.CAS Client:需要集成单点登录的应用,称为单点登录客户端
2.CAS Server:单点登录服务器,用户登录鉴权、凭证下发及校验等操作
3.TGT:ticker granting ticket,用户凭证票据,用以标记用户凭证,用户在单点登录系统中登录一次后,再其有效期内,TGT即代表用户凭证,用户在其它client中无需再进行二次登录操作,即可共享单点登录系统中的已登录用户信息
4.ST:service ticket,服务票据,服务可以理解为客户端应用的一个业务模块,体现为客户端回调url,CAS用以进行服务权限校验,即CAS可以对接入的客户端进行管控
5.TGC:ticket granting cookie,存储用户票据的cookie,即用户登录凭证最终映射的cookies

CAS核心协议介绍

case
1.用户在浏览器中访问应用
2.应用发现需要索要用户信息,跳转至SSO服务器
3.SSO服务器向用户展示登录界面,用户进行登录操作,SSO服务器进行用户校验后,映射出TGC
4.SSO服务器向回调应用服务url,返回ST
5.应用去SSO服务器校验ST权限及合法性
6.SSO服务器校验成功后,返回用户信息

CAS基本流程介绍

以下为基本的CAS协议流程,图一为初次登录时的流程,图二为已进行过一次登录后的流程
case
case

代码及示例

spring提供了整套的开源包,用以搭建OAUTH2+SSO的体系:
1.spring-oauth2:用以实现OAuth2协议,提供了上述所有四个角色提供的功能
2.spring-cas:用以实现和cas的集成,将OAuth2的登录、登出功能委托给CAS处理,并提供了统一的回调机制及凭证校验机制
3.CAS,耶鲁大学官方提供的SSO开源实现,本文的单点登录协议即按照CAS进行的说明

本文还提供了基于GO语言实现的简单OAuth2+SSO功能,详见github:
https://github.com/janwenjohn/go-oauth2-sso

搭建基于consul,registrator,nsq的GO体系Docker开发环境

使用GO作为生产服务开发语言,避免不了与以下几大基础组件打交道,他们分别是:

consul,registrator,nsq

consul

Consul 是一个支持多数据中心分布式高可用的服务发现和配置共享的服务软件,通常用于docker 实例的注册与配置共享

registrator

Registrator 去除了需要手动管理Consul服务条目的复杂性,它监视容器的启动和停止,根据容器暴露的端口和环境变量自动注册服务。

nsq

NSQ是一个基于Go语言的分布式实时消息平台

基于Docker容器的组件搭建

以上组件都提供了官方docker镜像,我们只需要拉取镜像启动即可,以下提供组件使用命令,可供单机开发使用,复杂如consul组集群等,不在此讨论

consul启动命令
docker stop dev-consul
docker rm dev-consul
docker run -d --name=dev-consul -p 8400:8400 -p 8500:8500/tcp -p 8600:53/udp -e 'CONSUL_LOCAL_CONFIG={"bootstrap_expect":1,"datacenter":"dc1","data_dir":"/usr/local/bin/consul.d/data","server":true}' consul agent -server -bind=127.0.0.1 -client=0.0.0.0

bind为服务发布地址,默认为网卡发布,否则可能只发布到容器内部ip

registrator启动命令
docker stop registrator
docker rm registrator
docker run -d --restart=always --name=registrator \
-v /var/run/docker.sock:/tmp/docker.sock \
gliderlabs/registrator:latest -ip yourip  consul://yourip:8500

其中ip地址为该服务器ip地址,主动申明的目的在于防止registrator自动将127.0.0.1本地回环作为对外暴露的ip地址注册到consul上。

nsq启动命令

nsqlookupd: docker run -d --restart=always --name lookupd \ -p 4160:4160 \ -p 4161:4161 \ nsqio/nsq /nsqlookupd nsqd: docker run -d --restart=always --name nsqd \ -p 4150:4150 \ -p 4151:4151 \ nsqio/nsq /nsqd --broadcast-address=yourip --lookupd-tcp-address=yourip:4160 nsqadmin: docker run -d --restart=always --name nsqadmin \ -p 4171:4171 \ nsqio/nsq /nsqadmin --lookupd-http-address=yourip:4161

Docker配置使用harbor私服

Docker默认使用docker-hub拉取镜像,使用harbor私服,必须对docker进行配置,否则docker在拉取镜像时会报错

1. 添加harbor私服配置

创建或修改以下文件:

vim /etc/docker/daemon.json

添加或增加harbor配置:

{ "insecure-registries":["myhostname"] }
2. 登录harbor
docker login -u admin -p Harbor12345 yourhostname

登录完成后会在docker配置中增加一条授权记录

cat /root/.docker/config.json

Docker私服Harbor安装

Docker私服一般选用vmware的harbor

官方地址:

https://github.com/vmware/harbor/releases

1. 安装dcoker-compose

harbor依赖docker-compose组件,首先安装docker-compose

docker-compose官方地址:

https://github.com/docker/compose/releases

安装命令:

curl -L https://github.com/docker/compose/releases/download/1.13.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
2. 安装harbor安装包

harbor安装包分为离线安装和在线安装,考虑到国内网络环境,一般选择离线安装

下载最新版本,目前为1.1.1
wget https://github.com/vmware/harbor/releases/download/v1.1.1/harbor-offline-installer-v1.1.1.tgz
解压缩
tar xzvf harbor-offline-installer-v1.1.1.tgz
修改配置文件

进入harbor安装目录,修改配置文件

vim harbor.cfg

以下必须修改:

hostname ,harbor启动后的访问ip或者域名

以下酌情修改:

harbor_admin_password ,harbor的默认用户名密码,默认为:admin/Harbor12345

安装harbor
./install.sh
3. 验证harbor

浏览器打开以下url:
http://yourhostname/harbor

4. 启停harbor

harbor使用docker作为容器,启停harbor前,必须首先启动docker服务

停止harbor
docker-compose stop
启动start
docker-compose stop

Centos安装Docker特定版本

某些情况下,不希望安装官方最新版本,这个时候就需要指定版本进行安装

1. 前置条件
必须是CENTOS7,64位
2. 安装源
sudo tee /etc/yum.repos.d/docker.repo
<<-'EOF'
[dockerrepo]
name=Docker Repository
baseurl=https://yum.dockerproject.org/repo/main/centos/7/
enabled=1
gpgcheck=1
gpgkey=https://yum.dockerproject.org/gpg
EOF
3. 选择版本号

进入

https://yum.dockerproject.org/repo/main/centos/7/Packages/

选择需要安装的版本号,如:

docker-engine-1.12.4-1.el7.centos.src.rpm
docker-engine-1.12.4-1.el7.centos.x86_64.rpm
docker-engine-1.12.5-1.el7.centos.src.rpm
docker-engine-1.12.5-1.el7.centos.x86_64.rpm

4. 安装依赖包

docker-engine依赖docker-engine-selinux包,先安装相同版本的docker-engine-selinux包:

yum install docker-engine-selinux-1.12.5-1.el7.centos.noarch.rpm

5. 安装Docker

yum install docker-engine-1.12.5-1.el7.centos.x86_64.rpm

6. 启动Docker
sudo systemctl start docker
7. 测试Docker
sudo docker run hello-world

Centos安装Docker最新版

官方文档:

https://store.docker.com/editions/community/docker-ce-server-centos?tab=description

1. 前置条件
必须是CENTOS7,64位
2. 安装源

安装必要工具集

sudo yum install -y yum-utils

安装docker官方源

sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo

更新yum缓存

sudo yum makecache fast
3. 安装Dcoker
sudo yum -y install docker-ce
4. 启动Docker
sudo systemctl start docker
5. 测试Docker
sudo docker run hello-world

hibernate自动添加永真1=1,导致Druid sql防火墙报错的问题

最近工程突然报错:
java.sql.SQLException: sql injection violation, part alway true condition not allow : select count(*) where this_.id<>? and 1=1
at com.alibaba.druid.wall.WallFilter.check(WallFilter.java:671)
at com.alibaba.druid.wall.WallFilter.connection_prepareStatement(WallFilter.java:214)

错误内容是druid的sql防火墙报警,发现是hibernate自动拼接了1=1的永真条件,而druid只会放行排在第一的永真条件,查看hibernate源代码后发现,当引用Junction生成sql时,如果条件为空,则会自动拼接1=1的永真条件。

解决办法也很简单,修改自己的代码,将引用junction的条件拼接放在第一位即可。

druid升级到最新的1.0.16-SNAPSHOT引发的配置问题

盲升druid至最新的1.0.16-SNAPSHOT版本后,启动工程报如下错误:
[com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker]-[WARN] Unexpected error in ping
Caused by: java.lang.IllegalArgumentException: timeout can’t be negative

查源码后发现,原来validationQueryTimeout变量默认值为-1,而不是0,导致socket接口报错。

解决办法:
在datasource中增加以下配置:

<property name="validationQueryTimeout" value="10000" />

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>

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>