现在的位置: 首页 > 综合 > 正文

spring security 整合 CAS

2018年05月01日 ⁄ 综合 ⁄ 共 19348字 ⁄ 字号 评论关闭

Spring security+CAS单点登录

Spring security 版本 3.2.4 

CAS Server版本  3.4.10

CAS client 版本 3.2.1

JDK 1.7

Tomcat 8.0

 

原理:

从结构上看,CAS 包含两个部分: CAS Server 和 CAS ClientCAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。图是 CAS 最基本的协议过程:

图 1. CAS 基础协议

 

CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。对于访问受保护资源的每个 Web 请求,CAS Client 会分析该请求的 Http 请求中是否包含 Service Ticket,如果没有,则说明当前用户尚未登录,于是将请求重定向到指定好的 CAS Server 登录地址,并传递 Service (也就是要访问的目的资源地址),以便登录成功过后转回该地址。用户在第 步中输入认证信息,如果登录成功,CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket,并缓存以待将来验证,之后系统自动重定向到 Service 所在地址,并为客户端浏览器设置一个 Ticket Granted CookieTGC),CAS Client 在拿到 Service 和新产生的 Ticket 过后,在第 5步中与 CAS Server 进行身份合适,以确保 Service Ticket 的合法性。

在该协议中,所有与 CAS 的交互均采用 SSL 协议,确保ST 和 TGC 的安全性。协议工作过程中会有 次重定向的过程,但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的。

另外,CAS 协议中还提供了 Proxy (代理)模式,以适应更加高级、复杂的应用场景,具体介绍可以参考 CAS 官方网站上的相关文档。

 

配置tomcat https 

本例是将CAS server 和 CAS client部署在不同的tomcat 上,首先需要为客户端和服务端的tomcat配置https协议:

SSL文件准备:
server.keystore——服务器端库文件
client.keystore——客户端库文件
server.cer——服务器端证书(自制)
client.cer——客户端证书(自制)
cacerts——证书链

1、生成服务器端库文件
keytool -genkey -v -alias server -keyalg RSA -keystore F:\server.keystore -validity 36500 

(输入密码,并输入你的名字和姓氏,组织名称,地区名称等,注意在填入 你的名字和姓氏CN的时候输入localhost)

 

2、导出服务器端证书
keytool -export -alias server -storepass 密码 -file F:\server.cer -keystore F:\server.keystore

3、生成客户端库文件
keytool -genkey -v -alias client -keyalg RSA -keystore F:\client.keystore -validity 36500 

(输入密码,并输入你的名字和姓氏,组织名称,地区名称等,注意在填入 你的名字和姓氏CN的时候输入localhost)

4、导出客户端证书
keytool -export -alias client -storepass 密码 -file F:\client.cer -keystore F:\client.keystore

 

5、导入服务器端证书到cacerts(cacerts是JDK下的jre的可信任证书仓库)
keytool -import -trustcacerts -alias server -file F:\server.cer -keystore ”%JAVA_HOME%\jre\lib\security\cacerts” -storepass changeit

6、
6、导入客户端证书到cacerts(cacerts是JDK下的jre的可信任证书仓库)
keytool -import -trustcacerts -alias server -file F:\server.cer -keystore ”%JAVA_HOME%\jre\lib\security\cacerts” -storepass changeit

将server.keystore、client.keystore、server.cer、client.cer、cacerts文件复制到cas服务器、cas客户机、cas客户机1的TOMCAT_HOME主目录及JAVA_HOEM\jre\lib\security目录下。

 

 

分别修改CAS server 和 CAS client的tomcat中的 conf/server.xml

修改CAS server的tomcat以下端口:

 <Connector useBodyEncodingForURI="true" URIEncoding="UTF-8" connectionTimeout="20000" port="8089" protocol="HTTP/1.1" redirectPort="8443"/>

   

   <Connector useBodyEncodingForURI="true" URIEncoding="UTF-8" SSLEnabled="true" acceptCount="100" clientAuth="false" 

   disableUploadTimeout="true" enableLookups="true" 

   keystoreFile="/server.keystore" keystorePass="wentao211()" 

   maxSpareThreads="75" maxThreads="200" minSpareThreads="5" port="8443" 

   protocol="org.apache.coyote.http11.Http11Protocol" scheme="https" secure="true" 

   sslProtocol="TLS"/>

    <!-- Define an AJP 1.3 Connector on port 8009 -->

    <Connector port="8019" protocol="AJP/1.3" redirectPort="8443"/>

 

为了防止和CAS clienttomcat端口冲突,我将端口修改

 

 

修改CAS clienttomcat的端口:

  <Connector URIEncoding="UTF-8" connectionTimeout="20000" port="8088" protocol="HTTP/1.1" redirectPort="8445" useBodyEncodingForURI="true"/>

   

   <Connector SSLEnabled="true" URIEncoding="UTF-8" acceptCount="100" clientAuth="false" disableUploadTimeout="true" enableLookups="true" keystoreFile="/client.keystore" keystorePass="wentao211()" maxSpareThreads="75" maxThreads="200" minSpareThreads="5" port="8445" protocol="org.apache.coyote.http11.Http11Protocol" scheme="https" secure="true" sslProtocol="TLS" useBodyEncodingForURI="true"/>

    <!-- Define an AJP 1.3 Connector on port 8009 -->

    <Connector port="8009" protocol="AJP/1.3" redirectPort="8445"/>

 

 

配置CAS Server

将下载的cas-server-3.4.10-release.zip 解压,把cas-server-3.4.10/modules下面的

cas-server-webapp-3.4.10.war拷贝到tomcat下的webapps下,然后启动tomcat;或者直接从myeclipse中导入war包,将其导入到myeclipse中,然后再发布到tomcat中,推荐使用后缀,因为我们需要继续对CAS server进行一些修改才能满足我们的需求:

 

1,我们需要使用数据源来对用户进行登陆验证,本例使用mysql作为数据库,所以需要拷贝一个mysql-connector-java-5.1.33-bin.jar和一个cas-server-support-jdbc-3.4.10.jarlib下面,

并且需要在数据库中建立一个表,用来存放用户数据,当然表中的字段可以根据需要新增,其中的字段名也是可以修改,只是需要在配置

deployerConfigContext.xml的数据源中字段查询的时候跟其对应上

 

然后修改/WEB-INF/deployerConfigContext.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"

xmlns:sec="http://www.springframework.org/schema/security"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl">

<property name="credentialsToPrincipalResolvers">

<list>

<bean

class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver">

<property name="attributeRepository" ref="attributeRepository" />

</bean>

<bean

class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" />

</list>

</property>

<property name="authenticationHandlers">

<list>

<bean

class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"

p:httpClient-ref="httpClient" />

<bean

class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">

<property name="dataSource" ref="dataSource" />

<property name="sql" value="select password from user where username=? and enabled=1" />

<property name="passwordEncoder" ref="passwordEncoder"></property>

</bean>

</list>

</property>

</bean>

<!-- 自定义的加密类 -->

<bean id="passwordEncoder" class="org.jasig.cas.util.MD5Encoder"></bean>

<!-- 数据源 -->

<bean id="dataSource"

class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName">

<value>com.mysql.jdbc.Driver</value>

</property>

<property name="url">

<value>jdbc:mysql://127.0.0.1:3306/test</value>

</property>

<property name="username">

<value>root</value>

</property>

<property name="password">

<value>mysql</value>

</property>

</bean>

 

<bean id="attributeRepository"

class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao">

<constructor-arg index="0" ref="dataSource" />

<constructor-arg index="1"

value="select * from user where username = ?" />

<property name="queryAttributeMapping">

<map>

<entry key="username" value="username" />

</map>

</property>

<!-- key为数据库中的字段名,value为客户端需要通过得到的名字 -->

<property name="resultAttributeMapping">

<map>

<entry key="username" value="username" />

<entry key="password" value="password" />

<entry key="enabled" value="enabled" />

<entry key="authority" value="authorities" />

</map>

</property>

</bean>

 

<!-- 这个userDetailsService名字不能变,因为在spring-configuration/securityContext.xml中有引用 -->

<sec:jdbc-user-service data-source-ref="dataSource" id="userDetailsService"

 users-by-username-query="select username,password,enabled from user where username = ?" 

 authorities-by-username-query="select username,authority from user where username = ?"/>     

 

<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">

</bean>

 

<bean id="auditTrailManager" class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" />

</beans>

 

 

2.修改view/jsp/protocol/2.0/casServiceValidationSuccess.jsp

<%@ page session="false" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

 

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>

<cas:authenticationSuccess>

<cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>

<c:if test="${not empty pgtIou}">

<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>

</c:if>

<c:if test="${fn:length(assertion.chainedAuthentications) > 1}">

<cas:proxies>

<c:forEach var="proxy" items="${assertion.chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(assertion.chainedAuthentications)-2}" step="1">

<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>

</c:forEach>

</cas:proxies>

</c:if>

 

<%-- 加入以下这段,否则客户端验证后会出现没有权限的错误 --%>

<c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes)> 0}">

 <cas:attributes>

   <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}" varStatus="loopStatus" begin="0"  end="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes)-1}" step="1">

   <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>

   </c:forEach>

 </cas:attributes>

</c:if>

 

</cas:authenticationSuccess>

</cas:serviceResponse>

 

3.在src下的org.jasig.cas.util.MD5Encoder.jar

public class MD5Encoder implements PasswordEncoder{

private static final String SALTStR = "testsalt"; //盐值

@Override

public String encode(String password) {

return new Md5PasswordEncoder().encodePassword(passwordSALTStR);

}

}

 

4.修改spring-configuration/securityContext.xml,修改一下一段,主要是加入了密码加密类

<sec:authentication-manager alias="casAuthenticationManager">

        <sec:authentication-provider ref="casAuthenticationProvider">

         <sec:password-encoder ref="passwordEncoder"></sec:password-encoder>

        </sec:authentication-provider>

   </sec:authentication-manager>

 

 

至此,服务器端已经搭建完成,在浏览器中输入:

https://localhost:8443/cas-server-webapp-3.4.10/login就会出现以下页面,根据数据库中你插入的用户数据登陆即可

 

 

CAS client配置

客户端采用SpringMVC进行简单的配置,客户端的流程是:

点击前往登陆页面 --->  CAS client拦截跳转到 CAS server的登陆页面 ---> 登陆成功后返回到主操作页面

客户端结构:

 

 

1.先配置好web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 

http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

<display-name></display-name>

<welcome-file-list>

<welcome-file>index.jsp</welcome-file>

</welcome-file-list>

 

<!-- 上下文配置文件路径 -->

<context-param>

<param-name>contextConfigLocation</param-name>

<param-value>

classpath:properties/applicationContext.xml,

    classpath:properties/springmvc-security.xml

       </param-value>

</context-param>

 

<!-- 上下文加载监听器 -->

<listener>

<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>

<!-- 防止session溢出 -->

<listener>

<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>

</listener>

 

<!-- 中文过滤器 -->

<filter>

<filter-name>characterFilter</filter-name>

<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>

<init-param>

<param-name>encoding</param-name>

<param-value>UTF-8</param-value>

</init-param>

<init-param>

<param-name>forceEncoding</param-name>

<param-value>true</param-value><!-- 强制进行转码 -->

</init-param>

</filter>

<filter-mapping>

<filter-name>characterFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

 

<!-- 然后接着是SpringSecurity必须的filter 优先配置,让SpringSecurity先加载,防止SpringSecurity拦截失效 -->

<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>

 

<!-- springmvcservlet -->

<servlet>

<servlet-name>springmvc</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<init-param>

<param-name>contextConfigLocation</param-name>

<param-value>classpath:properties/springmvc-servlet.xml</param-value>

</init-param>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>springmvc</servlet-name>

<url-pattern>*.action</url-pattern>

</servlet-mapping>

</web-app>

 

2.配置好applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans

xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:p="http://www.springframework.org/schema/p"

xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/beans

 http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

 http://www.springframework.org/schema/context 

     http://www.springframework.org/schema/context/spring-context-3.2.xsd">

<context:property-placeholder location="classpath:properties/jdbc.properties"/>

<!-- 扫描包,controller不扫描 -->

<context:component-scan base-package="com.springsecurity">

<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>

</context:component-scan>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"

init-method="init" destroy-method="close">

<property name="url" value="${druid.url}" />

<property name="username" value="${druid.username}" />

<property name="password" value="${druid.password}" />

<property name="maxActive" value="${druid.maxActive}" />

<property name="minIdle" value="${druid.minIdle}" />

<property name="maxWait" value="${druid.maxWait}" />

<property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />

<property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />

<property name="connectionProperties" value="config.decrypt=true" />

<property name="filters" value="config,stat,wall,log4j" />

</bean>

</beans>

 

3.配置spring-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:mvc="http://www.springframework.org/schema/mvc"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

http://www.springframework.org/schema/mvc 

http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd

http://www.springframework.org/schema/context 

http://www.springframework.org/schema/context/spring-context-3.2.xsd">

<!-- 扫描包 -->

<context:component-scan base-package="com.springsecurity.action" />

 

<!-- 加载资源路径 -->

<mvc:resources location="/resources/" mapping="/resources/**" />

 

<mvc:annotation-driven/>

<!-- jsp页面视图处理 @author黄文韬 -->

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">

<property name="order" value="1"></property>

<property name="defaultContentTypevalue="text/html" />

<property name="ignoreAcceptHeadervalue="true"></property>

<property name="mediaTypes">

<map>

<entry key="json" value="application/json" />

<entry key="html" value="text/html" />

</map>

</property>

<property name="viewResolvers">

<list>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/pages/" />

<property name="suffix" value=".jsp" />

</bean>

</list>

</property> 

</bean> 

</beans>

 

4.配置spring-security.xml(*****重点*****)

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  

    xmlns:security="http://www.springframework.org/schema/security"  

    xsi:schemaLocation="http://www.springframework.org/schema/beans   

            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd  

            http://www.springframework.org/schema/security   

            http://www.springframework.org/schema/security/spring-security-3.2.xsd">  

<security:debug/>

<!--  Spring-Security 的配置 -->

<!-- filters="none"  不过滤这些资源-->

<security:http pattern="/images/**" security="none"/>

<security:http pattern="/js/**" security="none" />

<security:http pattern="/index.jsp" security="none" />

<!-- 单点登录拦截 -->

<security:http use-expressions="true" auto-config="false" 

entry-point-ref="casProcessingFilterEntryPoint" 

access-denied-page="/WEB-INF/pages/common/noAuth.jsp">

<security:intercept-url pattern="/main/*.action" access="hasRole('ROLE_USER')"/>

<!-- 对权限角色进行处理,见下面配置的权限层级控制 -->

<security:expression-handler ref="expressHandler"/>

<!-- 在https登陆的时候需要配置端口 -->

<security:port-mappings>

<security:port-mapping http="8088" https="8443"/>

</security:port-mappings>

   <security:custom-filter ref="casAuthenticationFilter" position="CAS_FILTER"/>

   <!-- 退出登陆,将需要退出后返回的url通过service传入到服务器即可还需要在cas server/WEB-INF/cas-servlet.xml中修改 bean id="logoutController" 新增p:followServiceRedirects="true"/>

 -->

   <security:logout logout-url="/securitylogout" invalidate-session="true" delete-cookies="JSESSIONID" 

logout-success-url="https://localhost:8443/cas-server-webapp-3.4.10/logout?service=http://localhost:8088/springsecuritydemo_CAS" />

</security:http>

<!-- 指定一个自定义的authentication-manager :customUserDetailsService -->

<security:authentication-manager alias="theAuthenticationManager">

<security:authentication-provider ref="casAuthenticationProvider" />

</security:authentication-manager>

 

    <!-- 单点登录 -->

     <bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">

        <property name="authenticationManager" ref="theAuthenticationManager"/>

     </bean> 

    <bean id="casProcessingFilterEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">

        <property name="loginUrl" value="https://localhost:8443/cas-server-webapp-3.4.10/login"/>

        <property name="serviceProperties" ref="serviceProperties"/>

    </bean> 

    

<!-- loginUrl定义CAS server登陆页面,A的地址-->

    <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">

<property name="authenticationUserDetailsService" ref="authenticationUserDetailsService"/>

<property name="serviceProperties" ref="serviceProperties"></property>

<property name="ticketValidator">

<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">

<!-- <property name="proxyCallbackUrl" value="http://localhost:8088/springsecuritydemo_CAS/main/main.action"></property> -->

<constructor-arg index="0" value="https://localhost:8443/cas-server-webapp-3.4.10" /> <!-- SSO验证地址-->

</bean>

</property>

<property name="key" value="cas"></property>

</bean>

    <!-- authorities对应 CAS server的 登录属性, 在此设置到spirng security中,用于spring security的验证 -->

<bean id="authenticationUserDetailsService" class="org.springframework.security.cas.userdetails.GrantedAuthorityFromAssertionAttributesUserDetailsService">

<constructor-arg>

<array>

<!-- 这里是根据CAS server上的配置得到的authorities的值 -->

<value>userId</value>

<value>username</value>

<value>password</value>

<value>enabled</value>

<value>authorities</value>

</array>

</constructor-arg>

</bean> 

<!-- http://localhost:8088/SpringSecurity 具体应用 -->

    <!-- j_spring_cas_security_check spring的虚拟URL,此标志标识使用 CAS authentication upon return from CAS SSO login. -->

<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">

<property name="service" value="http://localhost:8088/springsecuritydemo_CAS/j_spring_cas_security_check"></property>

<property name="sendRenew" value="false"></property>

</bean>

    

    <!-- 权限层级控制 -->

    <bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">

     <property name="hierarchy" value="ROLE_ADMIN > ROLE_USER"/>

    </bean>

    <bean id="expressHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">

     <property name="roleHierarchy" ref="roleHierarchy"/>

    </bean>

</beans>

 

 

前往登陆的页面(client):

 

 

登陆页面(server)

 

返回主页面:

 

 

抱歉!评论已关闭.