技术开发 频道

基于jsessionid的单点登录经验总结

  【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保持在一定的数量。

基于jsessionid的单点登录经验总结
▲图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。

基于jsessionid的单点登录经验总结
▲图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);
}
}
7