基于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 );
}
}

  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。

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot集成CAS单点登录的步骤如下: 1. 配置CAS服务器 首先需要配置CAS服务器,包括安装和配置,这里不再赘述。 2. 引入CAS客户端依赖 在Spring Boot项目中引入CAS客户端依赖,例如: ``` <dependency> <groupId>org.jasig.cas.client</groupId> <artifactId>cas-client-core</artifactId> <version>3.6.</version> </dependency> ``` 3. 配置CAS客户端 在Spring Boot项目中配置CAS客户端,包括CAS服务器地址、CAS客户端地址、CAS登录地址等,例如: ``` cas.server.url=https://cas.example.com/cas cas.client.host.url=https://example.com cas.login.url=https://cas.example.com/cas/login ``` 4. 配置Spring Security 在Spring Boot项目中配置Spring Security,包括登录页面、登录成功后的跳转页面等,例如: ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CasAuthenticationProvider casAuthenticationProvider; @Autowired private CasAuthenticationEntryPoint casAuthenticationEntryPoint; @Autowired private CasAuthenticationFilter casAuthenticationFilter; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(casAuthenticationEntryPoint) .and() .addFilter(casAuthenticationFilter) .logout() .logoutUrl("/logout") .logoutSuccessUrl("/") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(casAuthenticationProvider); } } ``` 5. 配置CAS认证提供者 在Spring Boot项目中配置CAS认证提供者,例如: ``` @Configuration public class CasConfig { @Value("${cas.server.url}") private String casServerUrl; @Value("${cas.client.host.url}") private String casClientHostUrl; @Value("${cas.login.url}") private String casLoginUrl; @Bean public CasAuthenticationEntryPoint casAuthenticationEntryPoint() { CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint(); entryPoint.setLoginUrl(casLoginUrl); entryPoint.setServiceProperties(serviceProperties()); return entryPoint; } @Bean public ServiceProperties serviceProperties() { ServiceProperties serviceProperties = new ServiceProperties(); serviceProperties.setService(casClientHostUrl + "/login/cas"); serviceProperties.setSendRenew(false); return serviceProperties; } @Bean public CasAuthenticationFilter casAuthenticationFilter() throws Exception { CasAuthenticationFilter filter = new CasAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); return filter; } @Bean public CasAuthenticationProvider casAuthenticationProvider() { CasAuthenticationProvider provider = new CasAuthenticationProvider(); provider.setServiceProperties(serviceProperties()); provider.setTicketValidator(new Cas30ServiceTicketValidator(casServerUrl)); provider.setUserDetailsService(userDetailsService()); provider.setKey("casAuthProviderKey"); return provider; } @Bean public UserDetailsService userDetailsService() { return new InMemoryUserDetailsManager(Collections.emptyList()); } @Bean public AuthenticationManager authenticationManager() throws Exception { return new ProviderManager(Collections.singletonList(casAuthenticationProvider())); } } ``` 6. 编写登录页面 在Spring Boot项目中编写登录页面,例如: ``` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <form action="/login/cas" method="post"> <input type="submit" value="Login"> </form> </body> </html> ``` 7. 运行项目 最后运行Spring Boot项目,访问登录页面,输入CAS服务器的用户名和密码,即可实现CAS单点登录

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值