您的位置:首页 > 其它

SSO(单点登录)实施中遇到的几个问题

2013-03-27 19:56 453 查看
单点登录应用中,遇到如下的几个问题:1.超时问题;2.jsessionid问题;3.单点退出时有时子系统未能正常退出;4.有些请求路径不需要单点登录过滤器拦截;5.不同应用服务实现可能要求SSO客户端做适应性改造。我们具体分析一下,并提出解决方法。

1.超时问题

我们提供的CAS开源单点登录SSO组件,它部署节点主要有2个:SSO服务器(部署内容为一个web应用)、应用系统客户端(部署内容为cas客户端casclient.jar包和相关配置文件)。因此我们根据SSO机制分析一下什么情况下会出现超时。多个应用系统进行SSO集成后,SSO单点登录过程中,登录成功后,应用系统客户端(以下用浏览器客户端为例)的session会保存认证后的用户上下文,SSO服务器会生成一个用户凭证票据(TGT)并缓存起来,浏览器客户端会保存TGC(浏览器cookie中存储的TGT),TGT是作为发放SSO访问服务的票据(ST)的一个凭证票据,发放ST票据后才能正常访问。而浏览器客户端的session会超时(如一般web应用客户端可以设置session的timeout值为30分钟或更长),超时后会让session失效,清空用户上下文,TGC因为仍然是保存在浏览器cookie中,只有关闭浏览器才会清除。SSO服务器端的超时主要是TGT、ST超时,我们一般会设置超时值TGT为2小时,ST为5分钟。关于ST票据使用,一般在首次SSO访问服务时携带着该票据参数,验证票据后能正常访问后,SSO服务器就将此ST销毁失效了;关于TGT票据的使用,一般是正常访问时一直保持为超时时间(2小时),除非做单点退出会销毁TGT。

基于以上分析,我们可以得出结论,SSO的超时主要涉及2个要素:浏览器的session超时值、TGT的超时值。一般系统设置TGT的超时值>浏览器的session超时值,那么可能有2种超时情况:1.TGT超时(浏览器session超时值小,自然也超时);2.浏览器session超时,TGT不超时。

第一种“1.TGT超时”,这个处理很简单,用户的有效凭证票据都失效了,自然要重新取得有效凭证票据TGT,需要做的就是重新跳转到登录页面重新登录。

第二种”2.浏览器session超时,TGT不超时“,这时SSO服务器的TGT票据,以及浏览器客户端的TGC(cookie中的TGT)仍然有效。浏览器客户端再次SSO访问时就可以携带TGC(与服务器的TGT对应),向SSO服务器重新发送取得票据ST请求,取得票据ST后,携带着有效ST票据可以正常访问应用系统了。这个过程是浏览器客户端与SSO服务器的一个通讯交互,用户可能感觉不到,不会出现中断,好像能连续访问,这是为了给用户一个友好的访问体验。明白这个机制,就知道实际上是SSO机制在后台起作用了。



2.jsessionid问题

jsessionid是java客户端与应用服务器维持session的一个标识,其他语言客户端(如php)有其他标识关键字,具体是什么还不太了解。jsessionid一般存在于浏览器cookie中的(这个一般java客户端连接到应用服务器会自动执行的),一般情况下不会出现在url中,服务器会从客户端的cookie中取出来,但是如果浏览器禁用了cookie的话,就要重写url了,显式的将jsessionid重写到Url中,方便服务器来通过这个找到session的id。CAS开源单点登录SSO组件就提供了这个机制。我研究了CAS源码,基本明白了jsessionid的处理机制。大致原理如下:用户访问业务系统,SSO客户端拦截,重定向到SSO服务器认证时,就将请求路径uri中写入";jsessionid=具体的session值",SSO服务器可以分辨出这个标识值与其他客户端请求不同,进行认证处理,返回的响应给客户端cookie同时也设置了jsessionid的值,之所以在uri和cookie中都设置了jsessionid,是为了双重保障能设置jsessionid值。最后单点登录成功后,返回业务系统访问地址也带有jsessionid参数,这个在uri地址中看起来很别扭。

提供2种解决方法,如下:

1) 可以在登录页面地址的请求地址参数中加入参数”&method=POST“(记住这里要求POST大写),这样就可以在最后返回的访问uri中不显示jsessionid。

2)修改代码如下:

类org.jasig.cas.util.UrlUtils中增加方法cleanupUrl

public static final String cleanupUrl(final String url) {

if (url == null) {
return null;
}
final int jsessionPosition = url.indexOf(";jsession");
if (jsessionPosition == -1) {
return url;
}
final int questionMarkPosition = url.indexOf("?");
if (questionMarkPosition < jsessionPosition) {
return url.substring(0, url.indexOf(";jsession"));
}
return url.substring(0, jsessionPosition)
+ url.substring(questionMarkPosition);
}
类org.jasig.cas.web.flow.DynamicRedirectViewSelector的makeEntrySelection方法中修改如下行

default:

// return new ExternalRedirect(serviceResponse.getUrl());//注释源码
return new ExternalRedirect(UrlUtils.cleanupUrl(serviceResponse.getUrl()));//清除url中jsessionid
这样运行后,url路径中的jsessionid就不存在了。

3.单点退出时有时子系统未能正常退出

我们知道正常情况下,以用户A单点登录系统,正常访问各子系统,然后执行单点退出时,退出成功后一般跳转回到登录页面要求重新登录,这时各已登录的子系统session被销毁,再次以另一个用户B登录进入后,各子系统显示的应当是用户B的数据信息。可是有时却发现有些子系统仍然显示的是用户A的数据信息,这属于偶发现象。现在分析一下产生这种情况可能的原因,找出解决办法。

若想了解单点退出机制原理,我们可以先看看CAS源码的SSO单点退出实现机制。参见我的博客中 http://blog.csdn.net/yan_dk/article/details/7095091单点登录实现机制的【单点退出】部分。
我们看一下源码,看到退出时调用类AbstractWebApplicationService的方法logOutOfService(),代码如下

public synchronized boolean logOutOfService(final String sessionIdentifier) {

if (this.loggedOutAlready) {

return true;

}

LOG.debug("Sending logout request for: " + getId());

final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""

+ GENERATOR.getNewTicketId("LR")

+ "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()

+ "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"

+ sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";

HttpURLConnection connection = null;

try {

final URL logoutUrl = new URL(getOriginalUrl());

final String output = "logoutRequest=" + URLEncoder.encode(logoutRequest, "UTF-8");

connection = (HttpURLConnection) logoutUrl.openConnection();

connection.setDoInput(true);

connection.setDoOutput(true);

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();

final BufferedReader in = new BufferedReader(new InputStreamReader(connection

.getInputStream()));

while (in.readLine() != null) {

// nothing to do

}

return true;

} catch (final Exception e) {

return false;

} finally {

if (connection != null) {

connection.disconnect();

}

this.loggedOutAlready = true;

}

}

我们看到红字部分,在调用发生异常时,代码是直接返回false的,也就是说单点退出发生错误时,SSO服务器并没有做异常处理,直接返回,这样就有可能在出现异常时(比如网络瞬时闪断),虽然系统界面退出后返回登录页面,但是SSO服务器并没有退出处理,没有销毁登录会话,所以就可能出现没有真正退出,仍然显示前一用户的会话信息。这个应该是CAS源码的一个bug,解决方法是在此处积累错误日志,并抛出异常处理。这样应该能解决此问题。修改代码如下:

。。。

} catch (final Exception e) {

LOG.error("--------------Sending logout request for URL: " + getOriginalUrl()+"Network connection failed.");

throw new Exception(e);

} finally {

。。。

说明:

4.有些请求路径不需要单点登录过滤器拦截

业务系统web应用在使用单点登录组件时,有些请求路径不需要单点登录过滤器拦截,比如公共开放的路径,不需要认证都可以自由访问的路径,单点登录过滤器配置的映射路径一般以通配符匹配路径,但要把这些路径单独提取出来,让过滤器不拦截做单点登录处理,就需要对原有过滤器进行扩展改造,才能实现这个功能。

扩展实现代码如下:

public class CASFilter implements Filter {

public static enum ResponseType {

BREAK, GOON, RETURN

}

...

public void doFilter(

ServletRequest request,

ServletResponse response,

FilterChain fc){

。。。

CASReceipt receipt = (CASReceipt) session.getAttribute(CAS_FILTER_RECEIPT);

if (receipt != null && isReceiptAcceptable(receipt)) {

log.trace("CAS_FILTER_RECEIPT attribute was present and acceptable - passing request through filter..");

fc.doFilter(request, response);

return;

}else{

responeType = beforeDoSSOFilter(request, response);

if(ResponseType.RETURN==responeType){

return ;

}else if(ResponseType.BREAK==responeType) {

fc.doFilter(request, response);

return;

} //else go on

}

}

//过滤器的前置处理

public ResponseType beforeDoSSOFilter(ServletRequest request,

ServletResponse response) {

return ResponseType.GOON;

}

}

注:主要看原CASFilter 类红字部分扩展代码。

扩展实现类BMCASFilter

package com.sitechasia.sso.bmext;

public class BMCASFilter extends CASFilter {

private final Log log = LogFactory.getLog(this.getClass());

private static String ssoclient_passedPathSet;//设置不被sso过滤器拦截的请求路径,需要符合url路径通配符,多个路径可以","分割

public static final String PASSEDPATHSET_INIT_PARAM="passedPathSet";//web.xml配置文件中的参数

@Override

public void init(FilterConfig config) throws ServletException {

super.init(config);

ssoclient_passedPathSet = SSOClientPropertiesSingleton.getInstance().getProperty(ClientConstants.SSOCLIENT_PASSEDPATHSET)==null?config.getInitParameter(PASSEDPATHSET_INIT_PARAM):SSOClientPropertiesSingleton.getInstance().getProperty(ClientConstants.SSOCLIENT_PASSEDPATHSET);

}

@Override

public ResponseType beforeDoSSOFilter(ServletRequest request,

ServletResponse response) {

if (ssoclient_passedPathSet != null) {//路径过滤

HttpServletRequest httpRequest =(HttpServletRequest)request;

String requestPath = httpRequest.getRequestURI();

// String ls_requestPath = UrlUtils.buildFullRequestUrl(httpRequest.getScheme(), httpRequest.getServerName(), httpRequest.getServerPort(), requestPath, null);

PathMatcher matcher = new AntPathMatcher();

String passedPaths[]=null;

passedPaths =ssoclient_passedPathSet.split(",");

if(passedPaths!=null){

boolean flag;

for (String passedPath : passedPaths) {

flag = matcher.match(passedPath, requestPath);//ls_requestPath

if(flag){

log.info("sso client request path '"+requestPath+"'is matched,filter chain will be continued.");

return ResponseType.BREAK;

}

}

}

}

return ResponseType.GOON;

}

}

web.xml文件中配置修改如下:

<filter>

<description>单点登陆请求过滤器</description>

<filter-name>CASFilter</filter-name>

<filter-class>com.sitechasia.sso.dmext.filter.DMCASFilter</filter-class>

...

<init-param>

<description>排除路径</description>

<param-name>passedPathSet</param-name>

<param-value>

/**/restful/userLogin/findPassword,

/**/restful/userLogin/findIllegalLoginCount,

/**/restful/tenantManager/**,

/**/restful/lock/**,

/**/restful/export/**

</param-value>

</init-param>

</filter>

<filter-mapping>

<filter-name>CASFilter</filter-name>

<url-pattern>/index.jsp</url-pattern>

< /filter-mapping>

。。。

注:红字部分为相应配置内容扩展部分

经过上述这样扩展,配置的排除路径作为请求时,单点登录过滤拦截就会忽略处理,实现了目标功能要求。

5.不同应用服务实现要求SSO客户端做适应性改造

不同应用服务,对请求的处理方式不同,SSO客户端常规方法不能处理的需要进行适应性改造。我们先看一下SSO客户端常规方法处理请求,web应用中定义一个过滤器casfilter,并配置对安全资源拦截,首次登录系统、或者访问超时时,安全资源的请求被过滤器casfilter拦截,将安全资源路径包装为service参数,并重定向(http状态为302)到SSO服务器地址进行认证,输入凭证信息(用户名、密码等),SSO服务器经过认证、验证票据机制处理,认证成功后,登录通过后继续访问安全资源,再次访问安全资源时,过滤器casfilter拦截,根据用户上下文判断用户已经登录,就不再拦截安全资源,直接正常访问安全资源。而有些应用中使用其他方式处理请求,按上述常规方法不好处理,下面举实例来看看解决方案。

5.1.Ajax客户端

公司有一个应用系统使用Ajax客户端来处理请求并取得响应,在访问超时时,Ajax客户端请求由于使用异步处理技术(只局部刷新页面),不能将请求继续302重定向到SSO服务器重新认证处理,不能继续访问安全资源,界面响应处于一直延迟状态,很不友好。我们针对此应用进行适应性改造。改造要点如下:

我们约定应用系统的一个Ajax请求的标记(如ajaxRequestFlag=ajaxRequest),在安全资源ajax调用请求时携带此请求参数,还约定一个超时请求的响应状态值(如555),SSO客户端的过滤器casfilter的相应类程序对此约定进行了相应的改造。
应用系统的安全资源页面中引入文件(HttpStatusSSO.js)SSO对Axax请求的http状态的处理,其中有超时时的状态处理、判断是否登录、验证票据等的处理,都是通过ajax请求方式来处理的,其中依赖一个验证票据的资源ticketValidate.jsp。

经过上述改造后,应用系统的安全资源发送携带ajax请求标记的请求,在超时请求时,SSO客户端返回约定响应码(555),HttpStatusSSO.js文件判断处理后转发给相应的登录页面重新认证。单点登录的访问也能够正常的实现功能。这样只扩展了SSO客户端组件的实现,向下版本兼容,保证了SSO组件的统一完整性。

上述改造方式是通过实践开发出来的,可能还有更好的方法来改造扩展,今后持续改进吧。

后记:现在官方的CAS源码3.4版本已经支持Ajax请求的客户端,它的实现机制有待于进一步的研究,大家可以参考。

5.2.多域认证

有时我们遇到,不同子系统的认证实现的数据源可能来自一个用户数据库。我公司就是这种情况,采用Saas模式运营的软件,认证的用户来源于不同的租户或者域,基于这样的情况,我们对SSO进行了扩展,主要提供了认证接口,认证实现上,可以在认证时动态取得租户对应的数据源,对用户进行认证处理。举例如下:

5.3.SSO集中认证登录页面需要在业务子系统中定制

SSO集中认证的登录页面默认是放在SSO服务器端的,样式也很不好看,用户需要把登录页面放在业务子系统中自行定制,我们对SSO进行了扩展,思路也很简单,主要是将显示默认登录页面的调用,变成了远程调用业务子系统的页面地址,这个地址可以作为配置参数来配置。举例如下

=========================================================================================================

这几天为了测试人员测试,就把一个tomcat应用整个拷贝了一份,改了下端口一个8080,一个8081,上下文也一样,结果出问题了:页面登陆验证码死活验证不过去,最后跟踪了下后台发现,登陆界面请求时生成验证码并将验证码放入session里面,这个session的id和验证时从获取验证码的session的的id不一样,那验证的时候由于session不一样,获取验证码肯定为空,最终验证失败。

根据一般的流程,浏览器首次发出请求的时候服务端会在response信息里面给出:

Set-Cookie: JSESSIONID=0000yLsny8JFy4nLxDelrrq9Lx1:-1; Path=/test (这里假设服务器生成的sessionid为0000yLsny8JFy4nLxDelrrq9Lx1,应用上下文为test)要求浏览器设置浏览器会话cookie,下次请求的时候在request头信息里面附带:

Cookie: JSESSIONID=0000yLsny8JFy4nLxDelrrq9Lx1:-1 服务器以此来判断是否统一客户端发出的请求。

而跟踪我们的应用发现第一次浏览器发出请求后,服务端response信息为:

Set-Cookie
JSESSIONID=C7A2EB23B029226E6279448D1CFD6207; Path=/test


第二次发出请求的时候整个会话信息为:

响应头信息原始头信息:

Set-Cookie: JSESSIONID=
AD54301B809A7D7BBF909F10B4C838AA
; Path=/test

请求头信息原始头信息:

Cookie JSESSIONID=4DF629829C6E18953117AB66777ED2CA

浏览器虽然第二次请求带过去的JSESSIONID并不是第一次设置(
C7A2EB23B029226E6279448D1CFD6207
),并且服务端又发出了新的Set-Cookie命令和JSESSIONID。

最终才想起还有另一个tomcat应用的copy修改了本次的cookie session 里面的JSESSIONID值,

正好跟踪到信息为:

Set-Cookie JSESSIONID=4DF629829C6E18953117AB66777ED2CA; Path=/test (这一次请求影响)

Cookie JSESSIONID=C7A2EB23B029226E6279448D1CFD6207(原应用的JSESSIONID)

通过查看火狐浏览器里面两个应用在同一个主机(localhost)下面只存了一份名为JSESSIONID的cookie值,结果导致互相影响。





现在tomcat下的一个办法就是两个应用其中一个修改下上下文,例如一个是test,另一个是test1 ,这样由于cookie的路径(path)在同一个级别下名称不一样将不会互相影响。

浏览器判断cookie的应该根据主机,路径,名称来判断,通常根据目录层级不同作用域也不同,例如:

比如浏览器现有三个cookie, path分别为 "/", "/test", "/test/mgr"

则请求 "/test/other/action.do"时,第1个cookie和第2个cookie会被发给服务端,第三个不会。
此时浏览器请求头信息里面的session有可能为多个,但名称都一样如:
Cookie: JSESSIONID=8810E51861891187708C53A1805951A1;JSESSIONID=0000yLsny8JFy4nLxDelrrq9Lx1:-1
这个时候也是出现问题的时候,服务器有可能出现jsessionid不一致的情况,
一个明显的问题在websphere下由于默认的cookie session的path为/ 如图:



这个时候如果同一台主机或者ip下部署其他应用,如果都是用JSESSIONID作为key的话,由于/的作用域大,其他的应用将受到影响,首先websphere自己会受到影响,经过测试websphere的每一次请求如下:
Set-Cookie: JSESSIONID=0000yLsny8JFy4nLxDelrrq9Lx1:-1; Path=/
然后后面多次出现信息如下:
相应
Set-Cookie: JSESSIONID=
0000IsxSqg75ELW4C0Y3YCaRgab
:-1; Path=/
请求
Cookie:
JSESSIONID=8810E51861891187708C53A1805951A1(这个为tomcat的);JSESSIONID=0000yLsny8JFy4nLxDelrrq9Lx1:-1
遇到这种异常情况,请大家从cookie的domain,path,作用域的知识分析下,然后就应该能解决这些问题了,适当的修改下cookie的key名称,和path。
这里给看下百度的明显和其他的不一样用的是(baiduid作为key):



~完~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: