单点登录Cas项目实践

jopen 10年前


Cas 实现单点登录的实际应用


根据实际项目开发过程对Cas单点登录做了梳理,在吸收了前辈们的宝贵经验的基础上记录全套开发过程,如下


一、CAS版本
     Cas Server 3.5.2
     Cas Client   3.2.1


二、Cas Server 部署
     下载完整War包,可以直接部署到tomcat的webapps目录下,不做任何修改即刻成功,我部署的路径是 localhost:8080/casweb3.5.2,浏览器访问出现登录页面,默认输入任意同样的登录名和密码即刻登录成功


1、修改登录页面:
         一般来说系统实际使用时都不会直接已这个登录页面提供个用户使用,多少都会以实际情况作修改:
          cas统一认证的登陆页面位于:cas目录/WEB-INF/view/jsp/default 文件夹里,其中ui/casLoginView.jsp为登陆页面
          首先我们复制一份default文件夹 重命名为myview
          然后复制一份 /WEB-INF/classes/default_views.properties  命名为default_views_my.properties 打开default_views_my.properties 修改登陆页面的路径为我们复制的myview 文件夹(casLoginView.url=/WEB-INF/view/jsp/myview/ui/casLoginView.jsp)
          修改/WEB-INF/cas.properties 中 cas.viewResolver.basename =default_views_my
         到这一步我们只是将登陆页面拷贝了一份然后指向这份拷贝,接下来我们就可以随意修改我们拷贝的页面,这样做的目的是如果以后想还原回来比较方便,只需要修改引用就行,  页面修改可以搜索前辈的样例;
2、修改验证方式:
          简单的相同用户名密码的验证在实际应用中肯定是要不换掉的,下面就修改成基本的数据库验证
          修改WEN-INF/deployerConfigContext.xml
          搜索<bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />,将其注释掉
          这段是CAS默认的登录方式,即用户名和密码相同即可通过认证。我们现在需要改造它使之可以通过数据库查询的用户名和密码进行验证。

          其他是有这样一个数据库(casserver)一张数据表(tsys_user)

</div>
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">           <property name="sql" value="select password from tsys_user where username=? " />           <property name="dataSource" ref="dataSource" />     </bean>
</div>
dataSource配置,位置不要配置错了,不要直接配置bean id="authenticationManager" 这个里面,不然会加载出错,调试时就遇到这样情况
</div>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">                 <property name="driverClassName">                      <value>com.microsoft.sqlserver.jdbc.SQLServerDriver</value>                 </property>                 <property name="url">                      <value>jdbc:sqlserver://localhost:1435; DatabaseName=casserver</value>                 </property>                 <property name="username">                    <value>sa</value>               </property>               <property name="password">                    <value>sa</value>               </property>              </bean> 

 

这样做已基本满足大部分业务的需求,但如果遇到一些特殊的需求,如要求登录支持用户名、邮箱地址 或者需要自定义的加密验证等
自定义一个验证Handler,继承AbstractJdbcUsernamePasswordAuthenticationHandler.java.
</div>
    public class AlQueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler  {            //@NotNull        private String sql;            protected final boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials credentials) throws AuthenticationException {                   final String id = getPrincipalNameTransformer().transform(credentials.getUsername());            final String password = credentials.getPassword();                       try {                               final String dbPassword = getJdbcTemplate().queryForObject(this.sql, String.class, id);                               System.out.print("Username:" + id);                System.out.print("dbPassword:" + dbPassword);                               return MD5.verifyPassword(MD5.getMD5ofStr(password)+id, dbPassword);                           } catch (final IncorrectResultSizeDataAccessException e) {                return false;            }        }            /**        * @param sql The sql to set.        */        public void setSql(final String sql) {            this.sql = sql;        }    }  

 

修改WEN-INF/deployerConfigContext.xml 配置</span>
<bean class="com.aolong.cas.authentication.handler.AlQueryDatabaseAuthenticationHandler">               <property name="sql" value="select password from tsys_user where loginName=? " />                 <property name="dataSource" ref="dataSource" />      </bean> 

二、Cas Client 配置
客户端的jar,如cas-client-core-3.2.1.jar引入到web应用程序的classpath中
     配置web.xml文件, 主要是添加过滤器拦截通信, 下面的实例代码, 假设web应用程序的端口是9988
1. web.xml 配置
<!-- 与CAS Single Sign Out Filter配合,注销登录信息  -->            <listener>                <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>           </listener>                    <!-- CAS Server 通知 CAS Client,删除session,注销登录信息  -->          <filter>               <filter-name>CAS Single Sign Out Filter</filter-name>              <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>           </filter>            <filter-mapping>              <filter-name>CAS Single Sign Out Filter</filter-name>              <url-pattern>/*</url-pattern>           </filter-mapping>                   <!-- 登录认证,未登录用户导向CAS Server进行认证 -->          <filter>                <filter-name>CAS Filter</filter-name>               <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>             <!--<filter-class>com.aolong.eip.portal.sys.servlet.RemoteAuthenticationFilter</filter-class>             <init-param>                    <param-name>localLoginUrl</param-name>                  <param-value>http://localhost:9988/login.jsp</param-value>              </init-param>-->            <init-param>                  <param-name>casServerLoginUrl</param-name>                  <param-value>http://localhost:8080/casweb3.5.2/login</param-value>              </init-param>              <init-param>                  <param-name>serverName</param-name>                  <param-value>http://localhost:9988</param-value>              </init-param>           </filter>            <filter-mapping>              <filter-name>CAS Filter</filter-name>              <url-pattern>/*</url-pattern>           </filter-mapping>                  <!-- CAS Client向CAS Server进行ticket验证 -->          <filter>                <filter-name>CAS Validation Filter</filter-name>              <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>              <init-param>                  <param-name>casServerUrlPrefix</param-name>                  <param-value>http://localhost:8080/casweb3.5.2</param-value>              </init-param>              <init-param>                  <param-name>serverName</param-name>                  <param-value>http://localhost:9988</param-value>               </init-param>          </filter>          <filter-mapping>               <filter-name>CAS Validation Filter</filter-name>               <url-pattern>/*</url-pattern>          </filter-mapping>                    <!-- 封装request, 支持getUserPrincipal等方法-->          <filter>               <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>              <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>          </filter>          <filter-mapping>              <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>              <url-pattern>/*</url-pattern>          </filter-mapping>                    <!-- 存放Assertion到ThreadLocal中   -->          <filter>               <filter-name>CAS Assertion Thread Local Filter</filter-name>              <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>           </filter>           <filter-mapping>              <filter-name>CAS Assertion Thread Local Filter</filter-name>              <url-pattern>/*</url-pattern>           </filter-mapping>                   <!-- 自定义过滤器,单点登录成功后,写入自己的Session   -->           <filter>              <display-name>casSetUserAdapterFilter</display-name>              <filter-name>casSetUserAdapterFilter</filter-name>              <filter-class>com.aolong.eip.portal.sys.servlet.CasSetUserAdapterFilter</filter-class>          </filter>          <filter-mapping>              <filter-name>casSetUserAdapterFilter</filter-name>              <url-pattern>/*</url-pattern>          </filter-mapping>
注意这些配置应该尽量放在其他过滤器前面,比如Struts的配置, 如果顺序搞错,它的过滤器想不会起作用,这一点是容易被忽略的
</div> </div>

自定义的过滤器代码:

 
public class CasSetUserAdapterFilter implements Filter {            @Override        public void init(FilterConfig paramFilterConfig) throws ServletException {            }            @Override        public void doFilter(ServletRequest paramServletRequest,                ServletResponse paramServletResponse, FilterChain paramFilterChain)                throws IOException, ServletException {                        HttpServletRequest httpRequest = (HttpServletRequest) paramServletRequest;              HttpServletResponse httpResponse = (HttpServletResponse) paramServletResponse;              HttpSession session = httpRequest.getSession();                        //_const_cas_assertion_是CAS中存放登录用户名的session标志              Object object = session.getAttribute("_const_cas_assertion_");              Object userId  = session.getAttribute(SessionUtils.SKEY_USERID);                if (object != null && ((userId == null) || ("".equals(userId.toString())))) {                                  Assertion assertion = (Assertion) object;                  String loginName = assertion.getPrincipal().getName();                                  ILoginService loginService = (ILoginService) SpringContextUtil.getBean("loginService");                                loginService.ssoLogin(loginName, httpRequest, httpResponse);                                if (loginPage >= 0)                {                    session.setAttribute(SessionUtils.SKEY_LOGINPAGE, this.loginPage);                }            }              paramFilterChain.doFilter(paramServletRequest, paramServletResponse);          }            protected void setLoginPage(int iLoginPage)        {            this.loginPage = iLoginPage;        }                @Override        public void destroy() {            }            private int loginPage = -1;            }  
</div> </div>
三、安全配置

通过以上的配置基本实现Cas的功能, 但是至此会发现,登录成功后再访问其客户端时又要需要再重新登录
两个CAS Client 应用本身是没有问题的,都可以单独认证。问题还是出现在CAS Server上,因为即使不附带service参数进行Login,在CAS Server登录后,再次输入CAS Server Login的地址也是出现输入密码界面,而不是应该的直接显示已登录的界面 ,应该是登录成功后的TGC根本没有被加到浏览器上,问题在于:由于客户没有SSL的证书,暴露的是CAS Server的HTTP Login页面。而CAS Server默认会给TGC的Cookie加上secure选项,就是只有在SSL下CAS Server下一次才能获得这个Cookie,所以用户登录后再次访问CAS Server的HTTP页面,服务器获取不到TGC,认为这个用户还没有登录,就跳转到登录界面了
1、解决的办法当然是启用HTTPS的Login页面 - 当然,我们的客户没有证书,自签名的证书在浏览器默认设置还会弹出警告,所以不能用这个办法;或者修改CAS Server的配置
WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml
文件中修改p:cookieSecure属性为false就好了。虽然这个会降低SSO的安全性

2、对于Cas取消Https协议的所有配置如下(其中其他的配置修改我尚未理解其真正的作用):
修改 deployerConfigContext.xml 配置文件,添加 p:requireSecure="false"  属性。
</div>
 
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"          p:httpClient-ref="httpClient" p:requireSecure="false"/>  
</div> </div>
修改 ticketGrantingTicketCookieGenerator.xml 配置文件,p:cookieSecure="true"  改为 p:cookieSecure="false"
 
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"      p:cookieSecure="true"      p:cookieMaxAge="-1"      p:cookieName="CASTGC"      p:cookiePath="/cas" />
</div> </div>
修改 warnCookieGenerator.xml 配置文件,p:cookieSecure="true"  改为 p:cookieSecure="false"
 
<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"      p:cookieSecure="true"      p:cookieMaxAge="-1"      p:cookieName="CASPRIVACY"      p:cookiePath="/cas" />
</div> </div>
四、 单点登出

CAS3.4版本已经很好的支持了单点注销功能

假设环境如下:
 
两个业务系统APP1和APP2
在没有配置单点退出时,效果是这样子的
1:登录APP1,然后经过CAS认证后进入APP1
再访问APP2无需要认证
2:在APP1中连接到cas的logout地址,现象注销成功界面,然后再访问APP1,还是可以进去的,因为APP1将用户的登录票据存入了session。
 
那么实现了单点退出后的效果应该是这样子的:
1:登录APP1,然后经过CAS认证后进入APP1
再访问APP2无需要认证
2:用户在APP1或者APP2点击注销,显示CAS的注销成功页面,然后再访问APP1或者APP2都需要再次认证。

在APP1和APP2的web.xml文件中
CAS Single Sign Out Filter 相关配置就是实现此功能的作用
客户端访问 http://localhost:8080/casweb3.5.2/logout 退出

如果直接访问CAS的logout话,会出现注销成功页面,其实大部分情况下这个页面是没有必要的,更多的需求可能是退出后显示登录页面,并且登录成功后还是会进入到之前的业务系统,那么可以修改cas-servlet.xml文件,在"logoutController"的bean配置中增加属性 “followServiceRedirects”,设置为“true”
然后在业务系统的注销连接中加入"service参数",值为业务系统的绝对URL,这样就OK了,如你的业务系统URL 为:http://localhost:8080/casClient,那么注销URL就为:http://localhost:8080/cas /logout?service=http://localhost:8080/casClient
</div>