在這個前提之下,我們會面臨不同web application的session其實是被隔離的問題,也就是說在一個web application登入之後,另外一個web application並不會知道使用者已經登入過,或許會有人用cross servlet context的方式讓不同的web application交換資料,但這牽涉到所使用application server的特性,不見得是好的方法。
我們使用Spring Security的Pre-Authentication機制來處理SSO of Cross Web Applications問題,在某個master web application中實作登入機制,其它的slave web application只需要套用Pre-Authentication機制取得使用者曾經登入的資訊,那麼就不會發生登入不同步的問題了。
以下是我們簡單利用cookie記錄使用者曾經登入過的帳號密碼來做Pre-Authentication,當然帳號、密碼是經過加密的。
Spring XML for Master Web Application
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<security:http pattern="/login.action" security="none" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" authentication-manager-ref="authenticationManager" use-expressions="true">
<security:form-login authentication-success-handler-ref="cookieTokenAwareAuthenticationSuccessHandler"
login-page="/login.action" authentication-failure-url="/login.action?login_error=1" />
<security:logout invalidate-session="true" success-handler-ref="cookieTokenClearingLogoutSuccessHandler"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
<security:authentication-provider ref="casAuthenticationProvider" />
<security:authentication-provider ref="ldapAuthenticationProvider" />
<security:authentication-provider user-service-ref="userDetailService">
<!-- <security:password-encoder ref="passwordEncoder"/> -->
</security:authentication-provider>
</security:authentication-manager>
<!-- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"/> -->
<bean id="cookieTokenAwareAuthenticationSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.CookieTokenAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/init/menu.jsp" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenClearingLogoutSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.logout.CookieTokenClearingLogoutSuccessHandler">
<property name="defaultTargetUrl" value="/login.action" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenAuthenticationFilter" class="com.gss.gmo.cao.spring.security.web.authentication.preauth.CookieTokenAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="continueFilterChainOnUnsuccessfulAuthentication" value="true" />
<property name="checkForPrincipalChanges" value="true" />
</bean>
<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
<bean id="authenticationUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailService" />
</bean>
<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
<property name="service" value="http://localhost:8080/portal-web/gss/j_spring_cas_security_check" />
</bean>
<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="filterProcessesUrl" value="/gss/j_spring_cas_security_check" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationSuccessHandler" ref="cookieTokenAwareAuthenticationSuccessHandler" />
<property name="authenticationFailureHandler">
<bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/login.action?login_error=1" />
</bean>
</property>
</bean>
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg value="http://teamkube.gss.com.tw/cas" />
</bean>
</property>
<property name="key" value="teamkube-cas" />
<property name="authenticationUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<security:http pattern="/login.action" security="none" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" authentication-manager-ref="authenticationManager" use-expressions="true">
<security:form-login authentication-success-handler-ref="cookieTokenAwareAuthenticationSuccessHandler"
login-page="/login.action" authentication-failure-url="/login.action?login_error=1" />
<security:logout invalidate-session="true" success-handler-ref="cookieTokenClearingLogoutSuccessHandler"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
<security:authentication-provider ref="casAuthenticationProvider" />
<security:authentication-provider ref="ldapAuthenticationProvider" />
<security:authentication-provider user-service-ref="userDetailService">
<!-- <security:password-encoder ref="passwordEncoder"/> -->
</security:authentication-provider>
</security:authentication-manager>
<!-- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"/> -->
<bean id="cookieTokenAwareAuthenticationSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.CookieTokenAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/init/menu.jsp" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenClearingLogoutSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.logout.CookieTokenClearingLogoutSuccessHandler">
<property name="defaultTargetUrl" value="/login.action" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenAuthenticationFilter" class="com.gss.gmo.cao.spring.security.web.authentication.preauth.CookieTokenAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="continueFilterChainOnUnsuccessfulAuthentication" value="true" />
<property name="checkForPrincipalChanges" value="true" />
</bean>
<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
<bean id="authenticationUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailService" />
</bean>
<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
<property name="service" value="http://localhost:8080/portal-web/gss/j_spring_cas_security_check" />
</bean>
<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="filterProcessesUrl" value="/gss/j_spring_cas_security_check" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationSuccessHandler" ref="cookieTokenAwareAuthenticationSuccessHandler" />
<property name="authenticationFailureHandler">
<bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/login.action?login_error=1" />
</bean>
</property>
</bean>
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg value="http://teamkube.gss.com.tw/cas" />
</bean>
</property>
<property name="key" value="teamkube-cas" />
<property name="authenticationUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
</beans>
Spring XML for Slave Web Application
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="classpath:spring-acm-core.xml" />
<import resource="classpath:gmo-cao-struts2-spring-context.xml" />
<context:annotation-config />
<context:component-scan base-package="com.gss.acm.web" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" use-expressions="true">
<!-- Additional http configuration omitted -->
<security:form-login login-page="/../portal-web/login.action" />
<security:logout invalidate-session="true" logout-success-url="/../portal-web/spring_security_logout"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="classpath:spring-acm-core.xml" />
<import resource="classpath:gmo-cao-struts2-spring-context.xml" />
<context:annotation-config />
<context:component-scan base-package="com.gss.acm.web" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" use-expressions="true">
<!-- Additional http configuration omitted -->
<security:form-login login-page="/../portal-web/login.action" />
<security:logout invalidate-session="true" logout-success-url="/../portal-web/spring_security_logout"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>
</beans>
CookieTokenAuthenticationFilter
package com.gss.gmo.cao.spring.security.web.authentication.preauth;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.util.WebUtils;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Get username and password from cookie for SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (getPreAuthenticatedPrincipal((HttpServletRequest) request) == null) {
SecurityContextHolder.clearContext();
}
super.doFilter(request, response, chain);
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
Cookie passwordCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY);
if (passwordCookie != null) {
return encryptor.decrypt(passwordCookie.getValue());
}
return null;
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
Cookie usernameCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY);
if (usernameCookie != null) {
return encryptor.decrypt(usernameCookie.getValue());
}
return null;
}
}
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.util.WebUtils;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Get username and password from cookie for SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (getPreAuthenticatedPrincipal((HttpServletRequest) request) == null) {
SecurityContextHolder.clearContext();
}
super.doFilter(request, response, chain);
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
Cookie passwordCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY);
if (passwordCookie != null) {
return encryptor.decrypt(passwordCookie.getValue());
}
return null;
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
Cookie usernameCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY);
if (usernameCookie != null) {
return encryptor.decrypt(usernameCookie.getValue());
}
return null;
}
}
CookieTokenAwareAuthenticationSuccessHandler
package com.gss.gmo.cao.spring.security.web.authentication;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Set username/password to cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException,
IOException {
UserDetails user = (UserDetails) authentication.getPrincipal();
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, encryptor.encrypt(user.getUsername()));
username.setPath("/");
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, encryptor.encrypt(user.getPassword()));
password.setPath("/");
response.addCookie(username);
response.addCookie(password);
super.onAuthenticationSuccess(request, response, authentication);
}
}
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Set username/password to cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException,
IOException {
UserDetails user = (UserDetails) authentication.getPrincipal();
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, encryptor.encrypt(user.getUsername()));
username.setPath("/");
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, encryptor.encrypt(user.getPassword()));
password.setPath("/");
response.addCookie(username);
response.addCookie(password);
super.onAuthenticationSuccess(request, response, authentication);
}
}
CookieTokenClearingLogoutSuccessHandler
package com.gss.gmo.cao.spring.security.web.authentication.logout;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
/**
* Clear username/password from cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenClearingLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, null);
username.setPath("/");
username.setMaxAge(0);
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, null);
password.setPath("/");
password.setMaxAge(0);
response.addCookie(username);
response.addCookie(password);
super.onLogoutSuccess(request, response, authentication);
}
}
最後需要強調的是AbstractPreAuthenticatedProcessingFilter和AbstractAuthenticationProcessingFilter是不相關的filter類別,如果是內部系統彼此間的SSO可以使用AbstractPreAuthenticatedProcessingFilter,但是外部系統的SSO最好還是使用AbstractAuthenticationProcessingFilter的子類別來處理,但無論使用哪種方式,我們都會reuse UserDetailsService instance。
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
/**
* Clear username/password from cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenClearingLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, null);
username.setPath("/");
username.setMaxAge(0);
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, null);
password.setPath("/");
password.setMaxAge(0);
response.addCookie(username);
response.addCookie(password);
super.onLogoutSuccess(request, response, authentication);
}
}