定義:在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。它包括可以將這次主要的登錄映射到其他應用中用於同一個用戶的登錄的機制,這就是單點登錄。
技術實現機制:當用戶第一次訪問應用系統的時候,因為還沒有登錄,會被引導到認證系統中進行登錄;根據用戶提供的登錄信息,認證系統進行身份校驗,如果通過校驗,應該返回給用戶一個認證的憑據--ticket;用戶再訪問別的應用的時候,就會將這個ticket帶上,作為自己認證的憑據,應用系統接受到請求之后會把ticket送到認證系統進行校驗,檢查ticket的合法性。如果通過校驗,用戶就可以在不用再次登錄的情況下訪問應用系統2和應用系統3了。
-----百度百科
有這么一個需求,比如你做的產品只是一個大的系統的子系統,比如學校的管理系統中的一部分(比如教務,學工等),這些都是學校使用系統中的N個系統中的一個,為了方便統一管理,學校會有一個門戶系統,教師和學生只需要登錄門戶系統,就可以進入教務系統查課表,學工系統分配班級等等,這些系統或許來自不同的公司,但是它們都被集成在門戶系統中,做為一個子系統的形式存在,你現在的公司就是其中的一個模塊(子系統)的供應商,那么你在把你做的產品放在學校服務器上的同時,也需要把自己公司的系統集成到學校的門戶系統中,那么就需要集成單點登錄。
那么前面這個需求怎么做呢,好吧這就是我的一個任務,我現在就是一個子系統公司的開發人員,我們公司的產品賣給了N個學校,這些學校的門戶系統各異,每賣出一個系統就為那個學校配置一遍單點登錄實在是麻煩,大家也知道單點登錄就是在web.xml配置幾個過濾器,監聽器,然后改造自己的登錄口,把登錄口的地址反饋給集成方就可以,這是一個簡單的過程,但是我深信不疑的墨菲定律在無時無刻的起着作用,如果事情有變壞的可能,不管這種可能性有多小,它總會發生。那么我需要做的就是提供接口和盡可能多的不同對接廠商的實現,讓實施工程師去通過配置直接完成集成單點登錄的功能。
思路:前面說了,配置單點登錄很簡單,就是把廠商提供的過濾器和監聽器放到web.xml中,再提供一個cas登錄的action(請求),這個工作就算完成了,問題就在於不同的集成廠商他們的過濾器不一樣,當然參數是大同小異.那么提起不一樣和一樣,我首先想到的是一樣的接口,不一樣的實現類。然后就是過濾器,不同的過濾器怎么使用呢,這就引入了我寫這篇博客的目的,過濾器鏈。
注意:這里引出了過濾器鏈,這是一種設計模式,叫做責任鏈設計模式,我看到有一篇博客對他具有很好的解釋 http://www.flyne.org/article/693,可自行參閱
實現:首先你需要有自己的過濾器和監聽器,監聽自然不多說,那過濾器呢,你只需要把廠商的過濾器作為一個過濾器鏈,使用你的過濾器去執行這條鏈,以達到代碼可控的地步。
代碼實現:
常量類,因為很多相關類都要用到,這里就寫到接口里面.

1 public interface CasConst { 2 3 public static final String CAS_STATE_ON = "on"; // 開啟統一身份驗證 4 public static final String CAS_STATE_OFF = "off"; // 關閉統一身份驗證 5 6 public static final String CAS_STATE = "cas.state";// 開啟 關閉 7 public static final String CAS_MANUFACTURER = "cas.manufacturer";// 對接廠商 8 public static final String CAS_SERVERURLPREFIX = "cas.serverUrlPrefix";// CAS服務URL 9 public static final String CAS_SERVERLOGINURL = "cas.serverLoginUrl";// CAS服務認證頁URL 10 public static final String CAS_SERVERNAME = "cas.serverName";// 系統訪問地址 11 public static final String CAD_SERVERLOGOUTURL = "cas.serverLogoutUrl"; 12 public static final String CAS_ENCODING = "cas.encoding";// 認證編碼格式 13 public static final String CAS_PARAM = "cas.param";// 反饋賬號參數名 14 15 }
1.接口,定義一個廠商的接口類,提供了,獲得過濾器集合,獲得監聽器(監聽器只有一個),獲得用戶信息的方法。

1 public interface Authentication { 2 3 /** 4 * 獲得統一認證過濾器鏈 5 * @param filterConfig filterConfig對象 6 * @return 7 * @throws ServletException 8 */ 9 List<Filter> getFilters(FilterConfig filterConfig) throws ServletException; 10 11 /** 12 * 根據廠商認證信息獲得user對象 13 * @param request request對象 14 * @return 15 * @throws LoginException 16 */ 17 public User authentication(HttpServletRequest request) throws LoginException; 18 19 /** 20 * 獲得廠商提供監聽器 21 * @return 22 */ 23 public HttpSessionListener getListener(); 24 }
2.實現接口

1 public class AuthenticationForWiscom implements Authentication, CasConst { 2 3 private static final Logger LOG = LoggerFactory.getLogger(AuthenticationForWiscom.class); 4 5 private SingleSignOutFilter ssoOutFilter; 6 private AuthenticationFilter ssoAuthFilter; 7 private Cas20ProxyReceivingTicketValidationFilter ssoTicketFilter; 8 private HttpServletRequestWrapperFilter ssoQeqWraFilter; 9 List<Filter> filters = null; 10 11 @Override 12 public List<Filter> getFilters(FilterConfig filterConfig) throws ServletException { 13 filters = new ArrayList<>(); 14 String serverName = SysUtils.getSysParam(CAS_SERVERNAME); 15 String casServerLoginUrl = SysUtils.getSysParam(CAS_SERVERLOGINURL); 16 String casServerUrlPrefix = SysUtils.getSysParam(CAS_SERVERURLPREFIX); 17 LOG.error("serverName:[{}],casServerLoginUrl:[{}],casServerUrlPrefix:[{}]", serverName, casServerLoginUrl, casServerUrlPrefix); 18 19 ssoOutFilter = new SingleSignOutFilter(); 20 ssoOutFilter.init(new CasFilterConfig("CAS Single Sign Out Filter", filterConfig.getServletContext())); 21 filters.add(ssoOutFilter); 22 23 ssoAuthFilter = new AuthenticationFilter(); 24 ssoAuthFilter.init(new CasFilterConfig("CASFilter", filterConfig.getServletContext()).setInitParameter("serverName", serverName) 25 .setInitParameter("casServerLoginUrl", casServerLoginUrl)); 26 filters.add(ssoAuthFilter); 27 28 ssoTicketFilter = new Cas20ProxyReceivingTicketValidationFilter(); 29 ssoTicketFilter.init(new CasFilterConfig("CAS Validation Filter", filterConfig.getServletContext()).setInitParameter("serverName", serverName) 30 .setInitParameter("casServerUrlPrefix", casServerUrlPrefix)); 31 filters.add(ssoTicketFilter); 32 33 ssoQeqWraFilter = new HttpServletRequestWrapperFilter(); 34 ssoQeqWraFilter.init(new CasFilterConfig("CAS HttpServletRequest Wrapper Filter", filterConfig.getServletContext())); 35 filters.add(ssoQeqWraFilter); 36 37 return filters; 38 } 39 40 @Override 41 public User authentication(HttpServletRequest request) { 42 String account = request.getRemoteUser(); 43 // 如果沒有通過SSO認證, 44 if (StringUtils.isBlank(account)) { 45 LOG.error("單點登錄無法獲得用戶登錄信息!"); 46 } else { 47 return new UserBO().getUserByAccount(account); 48 } 49 return null; 50 } 51 52 @Override 53 public HttpSessionListener getListener() { 54 return new SingleSignOutHttpSessionListener(); 55 } 56 57 }
3.廠商的工廠類,通過spring注入的方式,注入廠商實現類

1 public class AuthenticationFactory implements CasConst { 2 3 private static AuthenticationFactory factory; 4 5 private Map<String, Authentication> map = new HashMap<String, Authentication>(); 6 7 /** 8 * 實例化一個工廠對象 9 * 10 * @return 11 */ 12 public static AuthenticationFactory getInstance() { 13 if (factory == null) { 14 factory = SpringContextUtil.getBean("authenticationFactory"); 15 } 16 return factory; 17 } 18 19 /** 20 * 根據廠商類型返回具體認證類 21 * 22 * @param key 廠商名稱 23 * @return 24 */ 25 public Authentication getAuthentication(String key) { 26 if (map.containsKey(key)) { 27 return map.get(key); 28 } 29 return null; 30 } 31 32 public Map<String, Authentication> getMap() { 33 return map; 34 } 35 36 public void setMap(Map<String, Authentication> map) { 37 this.map = map; 38 } 39 }
4.spring.xml文件

<bean id="authenticationForWiscom" class="com.eplugger.cas.module.AuthenticationForWiscom" scope="singleton"></bean> <bean id="authenticationFactory" class="com.eplugger.abilities.cas.module.AuthenticationFactory" scope="singleton"> <property name="map"> <map> <entry key="wiscom" value-ref="authenticationForWiscom"/> </map> </property> </bean>
5.監聽器類,配置監聽器類去啟動cas需要的監聽器類

1 public class CasListener implements HttpSessionListener,CasConst{ 2 private static final Logger LOG = LoggerFactory.getLogger(CasListener.class); 3 private HttpSessionListener listener; 4 5 @Override 6 public void sessionCreated(HttpSessionEvent event) { 7 // 初始化單點登錄公司 8 String casName = SysUtils.getSysParam(CAS_STATE); 9 if (StringUtils.equals(CAS_STATE_ON, casName)) { 10 String casManufacturer = SysUtils.getSysParam(CAS_MANUFACTURER); 11 // 獲得公司filter 12 Authentication authentication = AuthenticationFactory.getInstance().getAuthentication(casManufacturer); 13 listener = authentication.getListener(); 14 if (listener != null) { 15 LOG.info("單點登錄監聽類創建"); 16 listener.sessionCreated(event); 17 } 18 } 19 } 20 21 @Override 22 public void sessionDestroyed(HttpSessionEvent event) { 23 if (listener != null) { 24 listener.sessionDestroyed(event); 25 } 26 } 27 28 public HttpSessionListener getListener() { 29 return listener; 30 } 31 32 public void setListener(HttpSessionListener listener) { 33 this.listener = listener; 34 } 35 }
6.過濾器類(Filter,FilterChain,FilterConfig)

1 public class CasClentFilter implements Filter, CasConst { 2 private Authentication authen; 3 private CasFilterChain casChain = new CasFilterChain(); 4 private static final Logger LOG = LoggerFactory.getLogger(CasClentFilter.class); 5 6 @Override 7 public void destroy() { 8 casChain.dostroy(); 9 } 10 11 @Override 12 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 13 HttpServletRequest hsreq = (HttpServletRequest) request; 14 //判斷是否啟用,沒有啟用的直接跳到下一個過濾器 15 String state = SysUtils.getSysParam(CAS_STATE); 16 if (StringUtils.equals(CAS_STATE_OFF, state)) { 17 chain.doFilter(request, response); 18 return; 19 } 20 //獲得用戶緩存 21 User user = null;//從session中獲得用戶信息 22 String str[] = hsreq.getRequestURI().trim().split("/"); 23 String actionName = null; 24 if (str.length > 0) { 25 actionName = str[str.length - 1]; 26 } 27 //判斷發出的請求是否需要過濾,這里這么做是為了留一個口,保證在單點登錄不能使用的情況下系統還能正常登錄 28 //比如說你把加入系統和登錄系統的請求作為例外,那么你可以正常登錄 29 if(CasClentValidateUtils.isValidateActionName(actionName) || user != null){ 30 chain.doFilter(request, response); 31 }else{ 32 if (casChain != null) { 33 casChain.initChain(); 34 //執行過濾器鏈 35 casChain.doFilter(request, response); 36 //過濾器鏈並不知道什么時候停止,所以這里做記錄,最后利用過濾器鏈獲得request,response轉發到下一個過濾器 37 if (casChain.isFinishSSO()) { 38 chain.doFilter(casChain.getRequest(), casChain.getResponse()); 39 } 40 } 41 } 42 } 43 44 @Override 45 public void init(FilterConfig config) throws ServletException { 46 // 初始化單點登錄公司 47 String state = SysUtils.getSysParam(CAS_STATE); 48 if (StringUtils.isNotBlank(state) || !StringUtils.equals(CAS_STATE_OFF, state)) { 49 String casManufacturer = SysUtils.getSysParam(CAS_MANUFACTURER); 50 // 獲得公司filter 51 authen = AuthenticationFactory.getInstance().getAuthentication(casManufacturer); 52 if (authen != null ) { 53 // 初始化casChain 54 casChain.list.addAll(authen.getFilters(config)); 55 } 56 } 57 } 58 59 public Authentication getAuthen() { 60 return authen; 61 } 62 63 public void setAuthen(Authentication authen) { 64 this.authen = authen; 65 } 66 }

1 public class CasFilterChain implements FilterChain { 2 private ServletRequest request; 3 private ServletResponse response; 4 List<Filter> list = new ArrayList<Filter>(); 5 int index = 0; 6 7 public CasFilterChain addFilter(Filter filter) { 8 this.list.add(filter); 9 return this; 10 } 11 12 public void initChain() { 13 index = 0; 14 } 15 16 public boolean isFinishSSO() { 17 return index == list.size(); 18 } 19 20 public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { 21 if (this.request != request) 22 this.request = request; 23 if (this.response != response) 24 this.response = response; 25 if (index >= list.size()) 26 return; 27 Filter f = list.get(index); 28 index++; 29 f.doFilter(request, response, this); 30 } 31 32 public ServletRequest getRequest() { 33 return request; 34 } 35 36 public ServletResponse getResponse() { 37 return response; 38 } 39 40 public void dostroy() { 41 for (Filter filter : list) { 42 filter.destroy(); 43 } 44 } 45 }

1 public class CasFilterConfig implements FilterConfig { 2 String filterName = ""; 3 Map<String, String> initParameters = new HashMap<String, String>(); 4 ServletContext servletContext; 5 6 public CasFilterConfig() {} 7 8 public CasFilterConfig(String filterName, ServletContext servletContext) { 9 this.filterName = filterName; 10 this.servletContext = servletContext; 11 } 12 13 public String getFilterName() { 14 return filterName; 15 } 16 17 public CasFilterConfig setInitParameter(String key, String value) { 18 initParameters.put(key, value); 19 return this; 20 } 21 22 public String getInitParameter(String key) { 23 return initParameters.get(key); 24 } 25 26 public Enumeration getInitParameterNames() { 27 return Collections.enumeration(initParameters.keySet()); 28 } 29 30 public ServletContext getServletContext() { 31 return servletContext; 32 } 33 34 }
7.登錄的時候獲得用戶信息
由於你提供給集成方的請求就是你的登錄入口,所以在入口處的請求發出后,他會在走第一個過濾器的時候跳轉到門戶的登錄頁面,如果用戶登錄成功,會走獲得用戶信息的過濾器,這時request對象里就已經用了用戶的信息

1 public User authentication(HttpServletRequest request) { 2 User user = getCurUserInfo().getUser(); 3 // 如果已經有用戶登錄了科研系統,直接進入系統 4 if (user != null) { 5 return user; 6 } else { 7 // 初始化單點登錄公司 8 String casName = SysUtils.getSysParam(CasConst.CAS_MANUFACTURER); 9 // 獲得公司filter 10 Authentication authen = AuthenticationFactory.getInstance().getAuthentication(casName); 11 if (authen == null) { 12 return null; 13 } 14 return authen.authentication(request); 15 } 16 }
8.差點忘了web.xml配置,由於這里我使用的ssh框架開發,我需要過濾的請求就是*.action,注意這里filter應當放在請求信息編碼轉換器過濾器后面,也就是字符過濾器后面

1 <!-- 該過濾器用於實現單點登錄功能 --> 2 <filter> 3 <filter-name>CasClentFilter</filter-name> 4 <filter-class>CasClentFilter全路徑</filter-class> 5 </filter> 6 <filter-mapping> 7 <filter-name>CasClentFilter</filter-name> 8 <url-pattern>*.action</url-pattern> 9 </filter-mapping> 10 <!-- 統一身份認證監聽類 --> 11 <listener> 12 <listener-class>CasListener全路徑</listener-class> 13 </listener>
9.結束語
設計模式的用處無處不在,還是需要深入體會啊!對於單點登錄我的理解不深,這篇博客主要是講解的我在實際應用中對於過濾器鏈的使用,如有不對還請指出!