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