单点登录解决方案-CAS

/ java / 没有评论 / 463浏览

本文目标

单点登录

CAS

什么是CAS

server和client

sso流程

  1. 访问服务:SSO客户端发送请求访问应用系统提供的服务资源。
  2. 定向认证:SSO客户端会重定向用户请求到SSO服务器。
  3. 用户认证:用户身份认证。
  4. 发放票据:SSO服务器会产生一个随机的Service Ticket。
  5. 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。
  6. 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。

CAS服务端部署

我们使用CAS时是无需编码的,一切需要根据我们项目作出调整的需求都可更改相应的配置文件来实现。

登录认证

<bean id="primaryAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
  <property name="users">
    <map>
      <entry key="casuser" value="Mellon"/>
      <entry key="tom" value="123"/>
    </map>
  </property>
</bean>

​你可以增加一个entry来增加一个通行证(如我增加了tom)。修改配置文件后别忘了重启tomcat以使其生效。

登出url

端口修改

去除https协议

CAS默认使用的是HTTPS协议,如果使用HTTPS协议需要SSL安全证书(需向特定的机构申请和购买) 。如果对安全要求不高或是在开发测试阶段,可使用HTTP协议。我们这里讲解通过修改配置,让CAS使用HTTP协议。

<bean id="proxyAuthenticationHandler" 
class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" 
p:httpClient-ref="httpClient" 
p:requireSecure="false"/>

​这里需要增加参数p:requireSecure="false",requireSecure属性意思为是否需要安全验证,即HTTPS,false为不采用

<bean id="ticketGrantingTicketCookieGenerator" 
class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" 
p:cookieSecure="false" 
p:cookieMaxAge="3600" 
p:cookieName="CASTGC" 
p:cookiePath="/cas" />

​ 参数p:cookieSecure="true",同理为HTTPS验证相关,TRUE为采用HTTPS验证,FALSE为不采用https验证。参数p:cookieMaxAge="-1",是cookie的最大生命周期,-1为无生命周期,即只在当前打开的窗口有效,关闭或重新打开其它窗口,仍会要求验证。可以根据需要修改为大于0的数字,比如3600等,意思是在3600秒内,打开任意窗口,都不需要验证。我们这里将cookieSecure改为false , cookieMaxAge 改为3600

<bean id="warnCookieGenerator" 
class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" 
p:cookieSecure="false" 
p:cookieMaxAge="3600" 
p:cookieName="CASPRIVACY" 
p:cookiePath="/cas" />

​我们这里将cookieSecure改为false , cookieMaxAge 改为3600

CAS客户端入门小Demo

客户端工程1搭建

  1. 搭建工程引入依赖

    创建Maven工程 (war)casclient_demo1 引入cas客户端依赖并指定tomcat运行端口为9001

<dependencies>
    <!-- cas -->
    <dependency>
        <groupId>org.jasig.cas.client</groupId>
        <artifactId>cas-client-core</artifactId>
        <version>3.3.3</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>2.3.2</version>
            <configuration>
                <source>1.7</source>
                <target>1.7</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <configuration>
                <!-- 指定端口 -->
                <port>9001</port>
                <!-- 请求路径 -->
                <path>/</path>
            </configuration>
        </plugin>
    </plugins>
</build>

  1. 添加WEB-INF/web.xml,添加配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
    <!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->
    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
    </listener>
    <!-- 该过滤器用于实现单点登出功能,可选配置。 -->
    <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>
    <!-- 该过滤器负责用户的认证工作,必须启用它 -->
    <filter>
        <filter-name>CASFilter</filter-name>
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
        <init-param>
            <param-name>casServerLoginUrl</param-name>
            <param-value>http://localhost:9100/cas/login</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <!-- cas客户端IP -->
            <param-value>http://localhost:9001</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CASFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- 该过滤器负责对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:9100/cas</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <!-- cas客户端IP -->
            <param-value>http://localhost:9001</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CAS Validation Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
    <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>
    <!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 -->
    <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>
</web-app>
  1. 创建index.jsp
<%@ page="" language="java" contenttype="text/html; charset=utf-8" pageencoding="utf-8" %="">欢迎来到一品优购,<%=request.getremoteuser()%>

request.getRemoteUser()为获取远程登录名

客户端工程2搭建

单点登录测试

单点登出

欢迎来到一品优购,<%=request.getremoteuser()%>。
点击退出

登出到指定url

CAS服务端数据源设置

需求分析

配置数据源

<!-- c3p0数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" p:driverClass="com.mysql.jdbc.Driver" p:jdbcUrl="jdbc:mysql://127.0.0.1:3306/pinyougoudb?characterEncoding=utf8" p:user="root" p:password="123456" />
<!-- md5加密 -->
<bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" c:encodingAlgorithm="MD5" p:characterEncoding="UTF-8" />
<!-- 数据库认证处理器 -->
<bean id="dbAuthHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler" p:dataSource-ref="dataSource" p:sql="select password from tb_user where username = ?" p:passwordEncoder-ref="passwordEncoder"/> 

<bean id="authenticationManager" class="org.jasig.cas.authentication.PolicyBasedAuthenticationManager">
    <constructor-arg>
        <map>
            <entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
            <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
        </map>
    </constructor-arg>
</bean>
<bean id="primaryAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
    <property name="users">
        <map>
            <entry key="casuser" value="Mellon"/>
            <entry key="tom" value="123"/>
        </map>
    </property>
</bean>
<entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />

<entry key-ref="dbAuthHandler" value-ref="primaryPrincipalResolver"/>

需要注意的是,我们添加的c3p0、md5加密、根据数据源进行认证的dbAuthHandler需要引入3个依赖到WEB-INF/lib

  • c3p0-0.9.1.2.jar
  • cas-server-support-jdbc-4.0.0.jar
  • mysql-connector-java-5.1.32.jar

CAS服务端界面改造

需求分析

改头换面

修改页面

修改casLoginView.jsp (参照casLoginView-origin.jsp

<%@ page="" pageencoding="UTF-8" %=""> <%@ page="" contenttype="text/html; charset=UTF-8" %=""> 
<%@ taglib="" prefix="c" uri="http://java.sun.com/jsp/jstl/core" %=""> 
<%@ taglib="" prefix="spring" uri="http://www.springframework.org/tags" %=""> 
<%@ taglib="" prefix="form" uri="http://www.springframework.org/tags/form" %=""> 
<%@ taglib="" prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %="">
<form:form method="post" id="fm1" commandName="${commandName}" 
htmlEscape="true" class="sui-form">
</form:form>
<form:input id="username" tabindex="1" accesskey="${userNameAccessKey}" 
path="username" autocomplete="off" htmlEscape="true" 
placeholder="邮箱/用户名/手机号" class="span2 input-xfat" />
<form:password id="password" tabindex="2" path="password" 
accesskey="${passwordAccessKey}" htmlEscape="true" autocomplete="off" 
placeholder="请输入密码" class="span2 input-xfat" />
<input type="hidden" name="lt" value="${loginTicket}" /> 
<input type="hidden" name="execution" value="${flowExecutionKey}" /> 
<input type="hidden" name="_eventId" value="submit" /> 
<input class="sui-btn btn-block btn-xlarge btn-danger" accesskey="l" value="登陆" type="submit" />

错误提示

<form:errors path="*" id="msg" cssClass="errors" element="div" htmlEscape="false" />
authenticationFailure.AccountNotFoundException=Invalid credentials. 
authenticationFailure.FailedLoginException=Invalid credentials.
<bean id="localeResolver" 
class="org.springframework.web.servlet.i18n.CookieLocaleResolver" 
p:defaultLocale="zh_CN" />
authenticationFailure.AccountNotFoundException=\u7528\u6237\u4E0D\u5B58\u5728. 
authenticationFailure.FailedLoginException=\u5BC6\u7801\u9519\u8BEF.

第一个是用户名不存在时的错误提示 第二个是密码错误的提示

CAS客户端与SpringSecurity集成

Spring Security测试工程搭建

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>4.1.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>4.1.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
    <!-- cas -->
    <dependency>
        <groupId>org.jasig.cas.client</groupId>
        <artifactId>cas-client-core</artifactId>
        <version>3.3.3</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <configuration>
                <!-- 指定端口 -->
                <port>9003</port>
                <!-- 请求路径 -->
                <path>/</path>
            </configuration>
        </plugin>
    </plugins>
</build>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/spring-security.xml</param-value>
</context-param>
<listener>
    <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class>
</listener>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
  xmlns="http://www.springframework.org/schema/security"
  xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
  <!-- 拦截规则 拦截哪些路径 该用户需要什么角色才能访问该路径 use-expressions="false"表示不启用SPEL表达式,如hasRole('ROLE_USER') -->
  <http use-expressions="false">
      <!--被授予ROLE_USER角色的用户才能访问根目录及所有其子目录下的资源-->
      <intercept-url pattern="/**" access="ROLE_ADMIN"></intercept-url>
      <form-login always-use-default-target="true" default-target-url="/index.html" login-page="/login.html" authentication-failure-url="/login.html" />
      <csrf disabled="true"/>
      <headers>
          <frame-options policy="SAMEORIGIN"/>
      </headers>
      <logout/>
  </http>
  <!-- 认证管理 -->
  <authentication-manager>
      <authentication-provider>
          <user-service>
              <user name="admin" authorities="ROLE_ADMIN" password="admin"/>
          </user-service>
      </authentication-provider>
  </authentication-manager>
</beans:beans>
  ```  

*   添加`/login.html`和`/index.html`测试`SpringSecurity`搭建是否成功

  *   login.html

```html
欢迎光临
*   访问 [http://localhost:9003/index.html](http://localhost:9003/index.html) ,自动跳转到登录页,输入admin、admin,登录成功,SpringSecurity搭建完成。

Spring Security与 CAS集成

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-cas</artifactId>
  <version>4.1.0.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.jasig.cas.client</groupId>
  <artifactId>cas-client-core</artifactId>
  <version>3.3.3</version>
  <exclusions>
      <exclusion>
          <groupId>org.slf4j</groupId>
          <artifactId>log4j-over-slf4j</artifactId>
      </exclusion>
  </exclusions>
</dependency> 
<?xml version=”1.0” encoding=”UTF-8”?>
<beans:beans
    xmlns=”http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
    <!-- entry-point-ref 入口点引用 -->
    <http use-expressions="false" entry-point-ref="casProcessingFilterEntryPoint">
        <intercept-url pattern="/**" access="ROLE_USER"/>
        <csrf disabled="true"/>
        <!-- custom-filter为过滤器, position 表示将过滤器放在指定的位置上,before表示放在指定位置之前 ,after表示放在指定的位置之后 CAS_FILTER为SpringSecurity内置过滤器的别名,详情参加本文末尾附录A -->
        <custom-filter ref="casAuthenticationFilter" position="CAS_FILTER" />
        <custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
        <custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
    </http>
    <beans:bean id="casProcessingFilterEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
        <!-- 单点登录服务器登录URL -->
        <beans:property name="loginUrl" value="http://localhost:9100/cas/login"/>
        <beans:property name="serviceProperties" ref="serviceProperties"/>
    </beans:bean>
    <beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
        <!--service 配置自身工程的根地址+/login/cas,写法固定:根地址+/login/cas -->
        <beans:property name="service" value="http://localhost:9003/login/cas"/>
    </beans:bean>
    <beans:bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
        <beans:property name="authenticationManager" ref="authenticationManager"/>
    </beans:bean>
    <!-- 认证管理器 -->
    <authentication-manager alias="authenticationManager">
        <authentication-provider ref="casAuthenticationProvider"></authentication-provider>
    </authentication-manager>
    <!-- 认证提供者 -->
    <beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
        <!-- security认证类,这里只用来保存用户明细 -->
        <beans:property name="authenticationUserDetailsService">
            <beans:bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
                <beans:constructor-arg ref="userDetailsService" />
            </beans:bean>
        </beans:property>
        <!-- 由cas来做认证 -->
        <beans:property name="serviceProperties" ref="serviceProperties"/>
        <!-- ticketValidator 为票据验证器 -->
        <beans:property name="ticketValidator">
            <beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
                <beans:constructor-arg index="0" value="http://localhost:9100/cas"/>
            </beans:bean>
        </beans:property>
        <beans:property name="key" value="an_id_for_this_auth_provider_only"/>
    </beans:bean>
    <!-- 认证类,只是保存用户名,认证还是交由cas来做 -->
    <beans:bean id="userDetailsService" class="top.zhenganwen.demo.service.UserDetailServiceImpl"/>
    <beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
    <beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
        <beans:constructor-arg value="http://localhost:9100/cas/logout"/>
        <beans:constructor-arg>
            <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
        </beans:constructor-arg>
        <beans:property name="filterProcessesUrl" value="/logout/cas"/>
    </beans:bean>
</beans:beans>
/** * 认证类 */
public class UserDetailServiceImpl implements UserDetailsService { 
  @Override 
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
    //拿用户名到数据库查询用户的事CAS系统帮你做了 
    //构建角色集合 
    List<GrantedAuthority> authorities=new ArrayList(); //TODO 应该从数据库查询该用户权限的,这里作为demo简化,直接赋值 
    authorities.add(new SimpleGrantedAuthority("ROLE_USER")); 
    return new User(username, "" , authorities); 
  }
}

这个类的主要作用是在登陆后得到用户名,可以根据用户名查询角色或执行一些逻辑。 如通过 SecurityContextHolder获取用户名。

获取登录名

退出登录

<beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
    <beans:constructor-arg value="http://localhost:9100/cas/logout?service=http://localhost:9003/index2.html"/>
    <beans:constructor-arg>
        <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
    </beans:constructor-arg>
    <!-- 访问本工程的/logout/cas将单点退出cas系统 -->
    <beans:property name="filterProcessesUrl" value="/logout/cas"/>
</beans:bean>
<a href="/logout/cas">退出登录</a>
<http pattern="/index2.html" security="none"></http>

附录A. Spring Security 内置过滤器表

别名Filter 类
CHANNEL_FILTERChannelProcessingFilter
SECURITY_CONTEXT_FILTERSecurityContextPersistenceFilter
CONCURRENT_SESSION_FILTERConcurrentSessionFilter
LOGOUT_FILTERLogoutFilter
X509_FILTERX509AuthenticationFilter
PRE_AUTH_FILTERAstractPreAuthenticatedProcessingFilter 的子类
CAS_FILTERCasAuthenticationFilter
FORM_LOGIN_FILTERUsernamePasswordAuthenticationFilter
BASIC_AUTH_FILTERBasicAuthenticationFilter
SERVLET_API_SUPPORT_FILTERSecurityContextHolderAwareRequestFilter
JAAS_API_SUPPORT_FILTERJaasApiIntegrationFilter
REMEMBER_ME_FILTERRememberMeAuthenticationFilter
ANONYMOUS_FILTERAnonymousAuthenticationFilter
SESSION_MANAGEMENT_FILTERSessionManagementFilter
EXCEPTION_TRANSLATION_FILTERExceptionTranslationFilter
FILTER_SECURITY_INTERCEPTORFilterSecurityInterceptor
SWITCH_USER_FILTERSwitchUserFilter