【IT168 评论】目前市面上有许多SSO的产品,实现方式也不尽相同。但这些产品相对于已经投入使用的系统来说,存在诸多不适用之处:一,繁琐的配置不仅增加运维人员的学习成本,更有可能因为操作不当造成稳定运行系统的崩溃;二,目前市面上的SSO产品基本上只解决了用户认证的功能,很少将权限授予考虑进去,即便有也是需要满足其权限授予的标准,这对已经上线的系统很难适用;三,市面上SSO产品一般价格都不低,一定程度上也会增加项目的费用。
现在公司内部已经投入使用了三套系统,分别为APP1、APP2和APP3,需要一个SSO的站点不仅要实现一次登录同时使用上述三套系统,还需要在SSO站点建立三套系统的角色、分配用户角色的功能。为了满足上述需求,需要设计一套折中的单点登录方案,尽量减少对已上线系统的侵入,同时又能够保证一次登录所有系统都可以使用,以及分配用户的角色。
本文综合考虑适用性、快速集成性等功能特性,提出了一种以jsessionid为基础的集成方案,通过开放相应接口,子系统只需要实现少量集成的接口就可接入SSO中。HttpClient组件又能够帮助我们在同一个session下完成各种POST/GET请求,例如菜单的抓取、角色分配等,无须修改原系统的任务逻辑,相比于市面上的SSO产品,本方案减少了许多配置工作,对系统的侵入也基本上能够做到零侵入。
1. 基于jsessionid单点登录的实现原理
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一,其实现机制不尽相同,大体可分为Cookie和Session机制两大类。本文提出的单点登录方案本质上是通过session的交互来实现的。利用WEB容器中jsessionid的特性,在不侵入原来系统的认证方案的同时,又能够保证用户一次登录使用所有系统的功能,能够快速地实现身份认证的统一管理。
单点登录功能主要包括以下几步,实现过程见图1:
同步登录
登录SSO时,需要同时向各个子系统发送登录请求,登录成功后将用户拥有的权限菜单上发到SSO进行展示。超级管理员能够新增、修改、删除角色,同时能够为用户分配角色。发送到APP端的登录请求是不经过过滤器拦截的,因此可直接进行身份认证。
URL重写
身份认证成功后,跳转到子系统的URL中需要注入jsessionid,这样才保证与登录时的session相同。
Session过期处理
当子系统session过期后,SSO端能够感知,并重新登录,其过程与同步登录相同。除了登录请求,Session过期处理实质是由一个Filter来完成对所有请求的拦截,从而判断该请求是否是新的Session创建。
Session同步
保证子系统的session变更后,SSO主系统能够感知。而SSO的session需要子系统通过不段刷新机制,保证其session保持在一定的数量。
▲图1基于jsessionid单点登录的整体流程
2. 利用HttpClient组件实现系统登录
单点登录的第一步就是同步登录,在登录主系统SSO的同时能够各个子系统,这里采用HttpClient组件来模拟实现HTTP请求的发送。HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
利用HttpClient,登录主系统SSO成功后,携带用户信息发送请求到子系统的登录Action,同时返回子系统的SessionID,以子系统的系统名作为Key,SessionID作为Value进行Map映射,并保存在SSO的Session中,同时,SSO的sessionid保留在APP的Session中用于后续的刷新,其实现过程见图2。
▲图2 HttpClient登录流程
为了便于操作,将HttpClient发送HTTP请求封装在了一个类的静态方法,使用的时候只需要传递APPNAME,APP_SESSIONID,URL,参数封装的MAP。
public
class
HttpUtil {
private
static
final String HTTP_CONTENT_CHARSET
=
"
utf-8
"
;
public
static
final
int
MAX_TIME_OUT
=
1000
*
1
;
//
最大连接时间为30秒
public
static
final
int
MAX_IDLE_TIME_OUT
=
60000
;
public
static
final
int
MAX_CONN
=
100
;
public
static
String sendSimplePostRequest(String url,
Map param,String appSessionId) {
...
}
}
而处理所有SSO发送的HTTP请求统一由继承AbsAppService的Servlet实现,该抽象类的核心方法perform处理所有http://app_name/upfw的请求。
private
String perform(HttpServletRequest req, HttpServletResponse resp){
...
Long threadId
=
Thread.currentThread().getId();
try
{
HTTP_REQ_MAP.put(Thread.currentThread().getId(), req);
Object ret
=
method.invoke(
this
,
params
);
HTTP_REQ_MAP.remove(threadId);
String resultInfo
=
""
;
if
(ret instanceof AppLoginInfo){
Result result
=
Result.genOkResult();
result.setData((AppLoginInfo)ret);
Type type
=
new
TypeToken
>
(){}.getType();
resultInfo
=
JsonUtil.toJson(result, type);
}
else
{
Result result
=
Result.genOkResult(ret);
resultInfo
=
JsonUtil.toJson(result, Result.
class
);
}
return
resultInfo;
}
catch
(Throwable e) {
HTTP_REQ_MAP.remove(threadId);
Result result
=
Result.genExceptionResult(e);
return
JsonUtil.toJson(result, Result.
class
);
}
}
3. 基于jsessionid的URL重写
当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识,称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。
保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为http://...../xxx;jsessionid=ByOK3vjFD75aPnrF7C2H。
利用这一特性,我们在登录SSO后,就不需要再进行身份认证就可以访问子系统的URL。由于子系统的session id一直都在变化,因此URL重写时添加的jsessionid需要以AJAX的方式根据子系统的KEY值动态地从SSO的Session中获取,具体实现见图3。
▲图3 URL重写步骤
4 Session同步机制与Session过期处理
4.1 Session同步
session同步机制本质是就是定时刷新SSO的Session保证其不过期。为了记录SSO的session,我们定义了一个抽象类AbsAppHttpSession,主要用来方便获取SSO端的sessionid,登录的用户信息,及其他session生命周期相关的辅助信息。
public
abstract
class
AbsAppHttpSession {
protected
HttpSession session;
public
AbsAppHttpSession(HttpSession session) {
this
.session
=
session;
}
/*
*该session是否关联了登录用户信息,关联了表示登录,未关联表示游客身份登录
*/
public
abstract
boolean isUserLogin();
/*
*是否是upfw登录的,有可能是本地app系统入口登录的
*/
public
abstract
boolean isUpfwLogin();
/*
*返回upfwSessionId
*/
public
abstract
String getUpfwSessionId();
/*
*返回登录用户Id
*/
public
abstract
Long getUserId();
/*
*返回登录用户名
*/
public
abstract
String getLoginName();
/*
*返回登录用户姓名
*/
public
abstract
String getUserName();
/*
*最近session访问时间,按单位秒返回
*/
public
long
getLastAccessedTime(){
return
this
.session.getLastAccessedTime()
/
1000
;
}
/*
*该session会话超时时间,按单位秒返回
*/
public
int
getSessionTimeOut(){
return
this
.session.getMaxInactiveInterval();
}
/*
*当前session剩余存活时间,按单位秒返回
*/
public
long
getRemainLiveTime(){
long
currentTime
=
System.currentTimeMillis();
long
lastAccessedTime
=
this
.session.getLastAccessedTime();
return
(currentTime
-
lastAccessedTime)
/
1000
;
}
/*
*返回appSessionId
*/
public
String getAppSessionId(){
return
this
.session.getId();
}
public
HttpSession getHttpSession(){
return
this
.session;
}
}
而Session的创建与销毁都由抽象类AbsAppSessionListener来监听,该抽象类实现HttpSessionListener接口,这样就记录下为SSO端的Session列表,能够轻松地实现Session刷新,本质就是对SSO发起一个空的请求。SSO端刷新由abstract class AbsUpfwTaskServlet extends HttpServlet implements Runnable定时地去刷新,其核心方法即实现Runnable接口的方法run。
@Override
public
void
run() {
Timer timer
=
new
Timer();
TimerTask timerTask
=
new
TimerTask() {
@Override
public
void
run() {
...
}
};
timer.schedule(timerTask, delay,period);
}
4.2 Session过期处理
由于SSO的Session一直由APP在定时刷新,因此不存在过期现象;而APP的session过期时则由SSO重新登录生成新的Session,其过程类似于同步登录的过程。该过程主要由拦截器abstract class AbsUPFWAppFilter来实现,其核心方法如下。
@Override
public
void
doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
...
boolean b
=
httpRequest.isRequestedSessionIdFromURL();
if
(b){
Cookie cookie
=
new
Cookie(
"
JSESSIONID
"
, httpRequest.getSession().getId());
httpResponse.addCookie(cookie);
String upfwSessionId
=
httpRequest.getParameter(Constants.REQ_UPFW_SESSIONID_KEY);
if
(upfwSessionId
!=
null
&&
!
""
.equals(upfwSessionId)){
...
}
}
b
=
this
.hasSessionUser(httpRequest);
if
(b)
//
如果用户已经登录则放行
chain.doFilter(request, response);
else
{
//
该请求是否允许游客访问
b
=
this
.canVisitorAccess(httpRequest);
if
(b)
//
允许允许游客访问则放行
chain.doFilter(request, response);
else
{
//
判断cookie中是否包含upfwSessionId
b
=
this
.hasCookieUpfwSession(httpRequest);
if
(
!
b){
this
.appLoginTimeOut(httpRequest, httpResponse);
}
else
{
//
判断upfwSession是否过期
b
=
this
.isUpfwSessionTimeOut(
this
.getUpfwSessionIdFromCookie(httpRequest));
if
(b)
//
upfwSession过期,则跳到upfwSession过期处理
this
.upfwLoginTimeOut(httpRequest, httpResponse);
else
{
//
重新登录成功后继续放行
this
.reLoginUpfw(httpRequest, httpResponse);
chain.doFilter(request, response);
}
}
}
}
}
5. 小结
本文使用Web容器中jsessionid特性实现了一种折中的单点登录方案,即不侵入原有系统的认证方案,同时又能保证SSO登录其他APP都可以使用。这一单点登录方案,主要包括同步登录、URL重写、Session同步、Session过期处理,每一步都存在SSO的sessionid与APP的sessionid的交换,SSO的session因为有APP端的定时刷新不存在过期现象,而APP端session过期后,SSO会重新登录APP,从而建立起SSO与APP的session映射。这一方案对快速集成已经投入使用的系统身份认证具有一定的参考意义。
6. 作者简介
俞超 软件开发工程师
任职于某大型IT外资企业,主要从事J2EE开发、设计工作。
参考资料
http://www.cnblogs.com/kevinGao/archive/2012/08/10/2671010.html
http://www.cnblogs.com/zsuxiong/archive/2011/11/09/2241622.html
http://wentao365.iteye.com/blog/1242768
http://sunxboy.iteye.com/blog/217262
来自:http://tech.it168.com/a2013/0922/1536/000001536558_all.shtml