Tomcat Siteminder SSO Agent

不知道有沒有童鞋像我那樣需要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”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章