不知道有没有童鞋像我那样需要Tomcat的siteminder sso agent,有的话这篇文章应该能给大家一点启示。
CA官方是没有Tomcat的sso agent的,替代办法是使用apache拦在tomcat前面,然后用apache专用的agent达到使用sso的目的。但如果之前一直使用tomcat JAAS控制权限的用户就会很不爽,验证方面需要改很多地方,apache方案基本没用。创维TTG也搞了个tomcat agent,但不是开源的,反编译发现居然还加了代码混淆,不用也罢。
经过一周的研究,要实现Tomcat的sso agent,基本就以下几种方案:
1、不用jaas的话,使用filter方案即可,在所有请求前面都拦一个filter,通过filter连policy server验证smsession,判断用户是否有权限访问受保护资源。
2、使用jaas的话,filter方案就不太好用了,因为filter是在触发FormAuthenticator之后的事情,也就是说还没等你的filter去验证smsession,就会被扔到form-login-page让你登陆。这里我想了一个比较绕的办法,但只是简单测了一下,基本可以用,但没有上生产跑过,大家慎用,也不太鼓励大家用。方案如下:
继续使用jaas,但form-login-page不是指向默认的登陆页面,而是指向一个servlet,暂定名为:autoLogin
autoLogin的doGet方法,可以写验证smsession的逻辑,具体方法是当smsession验证通过后,使用 httpclient请求一个受保护资源,得到一个JSESSIONID。然后再次使用httpclient用smsession decode出来的用户名去登陆,登陆方法就是post到j_security_check,当然大家要写一个自定义realm去做这个登陆,通过查DB 也好还是查Ldap也好,反正最后返回一个Principal。到这里,刚才得到的JSESSIONID就在服务器里认证通过了。然后redirect一个静态页面,将JSESSIONID当参数一起返回。
为什么要redirect到一个静态页面?因为当你访问一个受保护资源,服务器会自动给你产生一个JSESSIONID,这个 JSESSIONID和httpclient认证过的那个JSESSIONID不是同一个,所以即使在上一步通过setCookie把认证通过的 JSESSIONID加上,你仍然访问不了受保护资源,因为你将会有两个JSESSIONID!一个是认证通过的,一个是没有的。所以这个静态页面的作用就是通过js,把原来的JSESSIONID给干掉,再加上认证过的JSESSIONID,最后再转到需要访问的资源。
这个方法非常绕,玩玩还可以,不适合用在生产系统。
3、这是我最终选择的方案,也是最直接最完美的方案,就是修改tomcat源码。下面讲一下详细过程。
这次我选择的tomcat版本是7.0.8,最开始想用5.0.28,因为可以避免升级,但发现5.0和7.0版本在authenticate的实现上有点区别,5.0版本的authenticate方法里没有传入Request对象,这将导致无法在认证的时候获取smsession cookie进行decode。最后只能选择tomcat 7.0进行改造。
第一步:获取tomcat7.0.8源代码。
建议大家使用eclipse svn新建项目,通过以下地址获取源码:http://svn.apache.org/repos/asf/tomcat/archive/tc5.0.x/tags/TOMCAT_5_0_28
第二步:修改tomcat源代码。
因为我们使用的是form验证,因此需要修改 org.apache.catalina.authenticator.FormAuthenticator文件的public boolean authenticate(Request request,HttpServletResponse response,LoginConfig config)方法
在以下代码之后
// Have we authenticated this user before but have caching disabled? if (!cache) { session = request.getSessionInternal(true); if (log.isDebugEnabled()) log.debug("Checking for reauthenticate in session " + session); String username = (String) session.getNote(Constants.SESS_USERNAME_NOTE); String password = (String) session.getNote(Constants.SESS_PASSWORD_NOTE); if ((username != null) && (password != null)) { if (log.isDebugEnabled()) log.debug("Reauthenticating username '" + username + "'"); principal = context.getRealm().authenticate(username, password); if (principal != null) { session.setNote(Constants.FORM_PRINCIPAL_NOTE, principal); if (!matchRequest(request)) { register(request, response, principal, Constants.FORM_METHOD, username, password); return (true); } } if (log.isDebugEnabled()) log.debug("Reauthentication failed, proceed normally"); } }
加上自己的代码:
if(principal == null){ Cookie[] cookies = request.getCookies(); if(cookies != null){ principal = context.getRealm().authenticate(cookies); } if (principal != null) { // Bind the authorization credentials to the request request.setAuthType("FORM"); request.setUserPrincipal(principal); session = request.getSessionInternal(true); session.setPrincipal(principal); return (true); } }
大家细心的话会发现,Realm接口里是没有authenticate(cookies)这个方法的,因此我们还需修改 org.apache.catalina.Realm接口,加上方法public Principal authenticate(Cookie[] cookie);同时还需要在Realm的实现类RealmBase里,加上以下代码,让它默认返回null就可以了,将来我们可以自己写个realm类 继承ReamlBase,再重写这个方法,这样能减少对tomcat的改动。
public Principal authenticate(Cookie[] cookie) { return null; }
OK,对tomcat的改动就完成了,以下是重新编译这几个类,编译方法不建议用ant,会很麻烦,需要很多依赖包。建议下一个已经编译好的tomcat7.0.8,把lib下和bin下的所有jar包都引进classpath,直接通过javac去编译这三个类文件,然后替换catalina.jar里对应的class文件,最后用新的catalina.jar替换老的catalina.jar即可。建议使用jdk1.6以上版本,或者大家可以查看原来catalina.jar里的class文件的版本号选择对应的jdk也行。
第三步:编写自定义realm,处理smsession cookie。
自定义realm需要继承RealmBase,引入sso的javaagent,目前只测通了使用JNI的agent(smjavaagentapi.jar),pure java的agent测不过,用JNI最不好的地方就是会crash,看来CA还留了一手。
private static ResourceBundle bundle = null; private static final String BUNDLE_NAME = "ssoconfig"; private static Logger log = Logger.getLogger(LDAPJDBCRealm.class); /** SiteMinder AgentAPI objects */ private AgentAPI agentapi; private boolean isAgentInit = false; public LDAPJDBCRealm(){ bundle = ResourceBundle.getBundle(BUNDLE_NAME); log.info("bundle init success!"); isAgentInit=smInitAPI(); } public Principal authenticate(Cookie[] cookies) { String ssoToken = null; boolean flag = false; log.info("start decode smsession!"); if (cookies != null) { for (int i = 0; i < cookies.length; i++) { log.info("COOKIE:" +cookies[i].getName() + ":" + cookies[i].getValue()); if (cookies[i].getName().equalsIgnoreCase("SMSESSION")) { ssoToken = cookies[i].getValue(); flag = true; break; } } } if(flag){ //这个就是从sso里decode出来的用户名了,自己写方法验证吧!验证完记得return一个Principal。 String userName = smDecodeSSOtoken(ssoToken); ..... }else{ log.info("没有找到SMSESSION!"); return null; } } private String smDecodeSSOtoken(String ssoToken) { int retcode; // create attribute list to receive attributes from the SSO token AttributeList attrList = new AttributeList(); TokenDescriptor tokendesc = new TokenDescriptor(0, false); SessionDef sessionDef = new SessionDef(); // request that an updated token be produced boolean updateToken = true; // this object will receive the updated token StringBuffer updatedSSOToken = new StringBuffer(); retcode = agentapi.decodeSSOToken(ssoToken.toString(), tokendesc, attrList, updateToken, updatedSSOToken); boolean isFirstElem = true; Enumeration attributeListEnum = attrList.attributes(); if (!attributeListEnum.hasMoreElements()) { log.info(bundle.getString("AGENTAPI_NONE")); } String userName = ""; while (attributeListEnum.hasMoreElements()) { Attribute attr = (Attribute) attributeListEnum.nextElement(); log.info(attr.id + "\t" + new String(attr.value)); isFirstElem = false; if(attr.id == 210){//smsession cookie里包含了很多信息,我需要的是attr210里的用户名,大家各取所需。 userName = new String(attr.value); } } // this.setHeaderAttributes(attrList, ht); //return updatedSSOToken.toString(); return userName; } boolean smInitAPI(){ String agentName = bundle.getString("AGENT_NAME"); String agentSecret = bundle.getString("AGENT_SECRET"); log.info("Loading configuration for agent_name:" + agentName); agentapi = new AgentAPI(); ServerDef serverdef = new ServerDef(); serverdef.serverIpAddress = bundle.getString("PS_IP"); try { serverdef.connectionMin = Integer.parseInt(bundle.getString("PS_CONMIN")); serverdef.connectionMax = Integer.parseInt(bundle.getString("PS_CONMAX")); serverdef.connectionStep = Integer.parseInt(bundle.getString("PS_CONSTEP")); serverdef.timeout = Integer.parseInt(bundle.getString("PS_TIMEOUT")); serverdef.authenticationPort = Integer.parseInt(bundle.getString("PS_AUPORT")); serverdef.authorizationPort = Integer.parseInt(bundle.getString("PS_AZPORT")); serverdef.accountingPort = Integer.parseInt(bundle.getString("PS_ACPORT")); } catch (Exception e) { log.info("Invalid agent configuration parameter - non numeric"); return false; } try{ InitDef initdef = new InitDef(agentName, agentSecret, false, serverdef); int retcode = agentapi.init(initdef); log.info("SSO agent初始化成功!"); if (retcode != AgentAPI.SUCCESS) { log.info("Failed to connect to Siteminder policy server"); return false; } }catch(Throwable ex){ log.error("SSO AGENT初始化失败!"); log.error(ex.getMessage()); return false; } return true; } }
sso agent信息配置文件:
PS_IP = policy server地址 PS_CONMIN = 1 PS_CONMAX = 3 PS_CONSTEP = 1 PS_TIMEOUT = 75 PS_AUPORT = 44442 PS_AZPORT = 44443 PS_ACPORT = 44441 AGENT_NAME = agent名称 AGENT_SECRET = agent密码 AGENT_IP = agent IP ADMIN_NAME = admin name ADMIN_PWD = admpwd USER_NAME = user name USER_PWD = userpwd LOGFILE_NAME = smjsdksample.log LOGGING_DETAIL = false
第四步:配置tomcat。
增加如下配置,这里的配置只是个参考,具体根据个人设置而定,反正就是需要增加自己的realm。
Realm className="MyRealm"
第五步:配置policy server
根据上面的配置文件配就好了,注意要勾上“Support 4.x agents”