定义:在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制,这就是单点登录。
技术实现机制:当用户第一次访问应用系统的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--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.结束语
设计模式的用处无处不在,还是需要深入体会啊!对于单点登录我的理解不深,这篇博客主要是讲解的我在实际应用中对于过滤器链的使用,如有不对还请指出!