第 13 章 单点登录

所谓单点登录,SSO(Single Sign On),就是把N个应用的登录系统整合在一起,这样一来无论用户登录了任何一个应用,都可以直接以登录过的身份访问其他应用,不用每次访问其他系统再去登陆一遍了。

Spring Security没有实现自己的SSO,而是整合了耶鲁大学单点登陆(JA-SIG),这是当前使用很广泛的一种SSO实现,它是基于中央认证服务CAS(Center Authentication Service)的结构实现的,可以访问它们的官方网站获得更详细的信息http://www.jasig.org/cas

在了解过这些基础知识之后,我们可以开始研究如何使用Spring Security实现单点登录了。

13.1. 配置JA-SIG

从JA-SIG的官方网站下载cas-server,本文写作时的最新稳定版为3.5.1。http://www.ja-sig.org/downloads/cas/cas-server-3.5.1-release.zip

将下载得到的cas-server-3.5.1-release.zip文件解压后,可以得到一大堆的目录和文件,我们这里需要的是modules目录下的cas-server-webapp-3.5.1.war。

把cas-server-webapp-3.5.1.war放到ch103\server目录下,然后执行run.bat就可启动CAS中央认证服务器。

我们已在pom.xml中配置好了启用SSL所需的配置,包括使用的server.jks和对应密码,之后我们可以通过https://localhost:9443/cas/login访问CAS中央认证服务器。

登陆页面

图 13.1. 登陆页面


默认情况下,只要输入相同的用户名和密码就可以登陆系统,比如我们使用user/user进行登陆。

登陆成功

图 13.2. 登陆成功


这就证明中央认证服务器已经跑起来了。下一步我们来配置Spring Security,让它通过中央认证服务器进行登录。

13.2. 配置Spring Security

13.2.1. 添加依赖

首先要添加对cas的插件和cas客户端的依赖库。因为我们使用了maven2,所以只需要在pom.xml中添加一个依赖即可。

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-cas</artifactId>
  <version>3.1.3.RELEASE</version>
</dependency>
            

如果有人很不幸的没有使用maven,那么就需要手工把去下面这些依赖库了。

spring-security-cas-3.1.3.RELEASE.jar
aopalliance-1.0.jar
cas-client-core-3.1.12.jar
            

大家可以去spring和ja-sig的网站去寻找这些依赖库。

13.2.2. 修改applicationContext.xml

首先修改http部分。

<http auto-config='true' entry-point-ref="casProcessingFilterEntryPoint">1
    <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
    <intercept-url pattern="/index.jsp" access="ROLE_USER" />
    <intercept-url pattern="/" access="ROLE_USER" />
    <logout logout-success-url="/cas-logout.jsp"/>2
</http>
            

1

添加一个entry-point-ref引用cas提供的casProcessingFilterEntryPoint,这样在验证用户登录时就用上cas提供的机制了。

2

修改注销页面,将注销请求转发给cas处理。

<a href="https://localhost:9443/cas/logout">Logout of CAS</a>

然后要提供userService和authenticationManager,二者会被注入到cas的类中用来进行登录之后的用户授权。

<user-service id="userService">1
    <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
    <user name="user" password="user" authorities="ROLE_USER" />
</user-service>

<authentication-manager alias="authenticationManager">2
	<authentication-provider ref="casAuthenticationProvider" />
</authentication-manager>
            

1

为了演示方便,我们将用户信息直接写在了配置文件中,之后cas的类就可以通过id获得userService,以此获得其中定义的用户信息和对应的权限。

2

对于authenticationManager来说,我们没有创建一个新实例,而是使用了“别名”(alias),这是因为在之前的namespace配置时已经自动生成了authenticationManager的实例,cas只需要知道这个实例的别名就可以直接调用。

创建cas的filter, entryPoint, serviceProperties和authenticationProvider。

<http auto-config='true' entry-point-ref="casProcessingFilterEntryPoint">
    <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
    <intercept-url pattern="/index.jsp" access="ROLE_USER" />
    <intercept-url pattern="/" access="ROLE_USER" />
    <logout logout-success-url="/cas-logout.jsp"/>
    <custom-filter position="CAS_FILTER" ref="casProcessingFilter" />1
</http>

<authentication-manager alias="authenticationManager>
	<authentication-provider ref="casAuthenticationProvider"2 />
</authentication-manager>

<beans:bean id="casProcessingFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
    <beans:property name="authenticationManager" ref="authenticationManager"/>
</beans:bean>

<beans:bean id="casProcessingFilterEntryPoint"
            class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
    <beans:property name="loginUrl" value="https://localhost:9443/cas/login" />3
    <beans:property name="serviceProperties" ref="casServiceProperties" />
</beans:bean>

<beans:bean id="casServiceProperties" class="org.springframework.security.cas.ServiceProperties">
    <beans:property name="service" value="https://localhost:8443/ch103/j_spring_cas_security_check"/>4
    <beans:property name="sendRenew" value="false"/>
</beans:bean>

<beans:bean id="casAuthenticationProvider"
            class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
    <beans:property name="userDetailsService" ref="userService" />
    <beans:property name="serviceProperties" ref="casServiceProperties" />
    <beans:property name="ticketValidator">
        <beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
            <beans:constructor-arg index="0" value="https://localhost:9443/cas" />5
        </beans:bean>
    </beans:property>
    <beans:property name="key" value="ch103" />
</beans:bean>
            

1

casProcessingFilter最终是要放到Spring security的安全过滤器链中才能发挥作用的。这里使用的customer-filter就会把它放到CAS_PROCESSING_FILTER位置的后面。

这个位置具体是在LogoutFilter和AuthenticationProcessingFilter之间,这样既不会影响注销操作,又可以在用户进行表单登陆之前拦截用户请求进行cas认证了。

2

使用custom-authentication-provider之后,Spring Security其他的权限模块会从这个bean中获得权限验证信息。

3

当用户尚未登录时,会跳转到这个cas的登录页面进行登录。

4

用户在cas登录成功后,再次跳转回原系统时请求的页面。

CasProcessingFilter会处理这个请求,从cas获得已登录的用户信息,并对用户进行授权。

5

系统需要验证当前用户的tickets是否有效。

经过了这么多的配置,我们终于把cas功能添加到spring security中了,看着一堆堆一串串的配置文件,好似又回到了acegi的时代,可怕啊。

下面运行系统,尝试使用了cas的权限控制之后有什么不同。

13.3. 运行配置了cas的子系统

首先要保证cas中央认证服务器已经启动了。子系统的pom.xml中也已经配置好了SSL,所以可以进入ch103执行run.bat启动子系统。

现在直接访问http://localhost:8080/ch103/不再会弹出登陆页面,而是会跳转到cas中央认证服务器上进行登录。

登陆页面

图 13.3. 登陆页面


输入user/user后进行登录,系统不会做丝毫的停留,直接跳转回我们的子系统,这时我们已经登录到系统中了。

登陆成功

图 13.4. 登陆成功


我们再来试试注销,点击logout会进入cas-logout.jsp。

cas-logout.jsp

图 13.5. cas-logout.jsp


在此点击Logout of CAS会跳转至cas进行注销。

注销成功

图 13.6. 注销成功


现在我们完成了Spring Security中cas的配置,enjoy it。

13.4. 为cas配置SSL

在使用cas的时候,我们要为cas中央认证服务器和子系统都配置上SSL,以此来对他们之间交互的数据进行加密。这里我们将使用JDK中包含的keytool工具生成配置SSL所需的密钥。

13.4.1. 生成密钥

首先生成一个key store。

keytool -genkey -keyalg RSA -dname "cn=localhost,ou=family168,o=www.family168.com,l=china,st=beijing,c=cn" -alias server -keypass password -keystore server.jks -storepass password
            

我们会得到一个名为server.jks的文件,它的密码是password,注意cn=localhost部分,这里必须与cas服务器的域名一致,而且不能使用ip,因为我们是在本地localhost测试cas,所以这里设置的就是cn=localhost,在实际生产环境中使用时,要将这里配置为cas服务器的实际域名。

导出密钥

keytool -export -trustcacerts -alias server -file server.cer -keystore  server.jks -storepass password
            

将密钥导入JDK的cacerts

keytool -import -trustcacerts -alias server -file server.cer -keystore  D:/apps/jdk1.5.0_15/jre/lib/security/cacerts -storepass password
            

这里需要把使用实际JDK的安装路径,我们要把密钥导入到JDK的cacerts中。

我们在ch103/certificates/下放了一个genkey.bat,这个批处理文件中已经包含了上述的所有命令,运行它就可以生成我们所需的密钥。

13.4.2. 为jetty配置SSL

jetty的配置可参考ch103中的pom.xml文件。

  <connectors>
    <connector implementation="org.mortbay.jetty.security.SslSocketConnector">
      <port>9443</port>
      <keystore>../certificates/server.jks</keystore>
      <password>password</password>
      <keyPassword>password</keyPassword>
      <truststore>../certificates/server.jks</truststore>
      <trustPassword>password</trustPassword>
      <wantClientAuth>true</wantClientAuth>
      <needClientAuth>false</needClientAuth>
    </connector>
  </connectors>
  <systemProperties>
    <systemProperty>
      <name>javax.net.ssl.trustStore</name>
      <value>../certificates/server.jks</value>
    </systemProperty>
    <systemProperty>
      <name>javax.net.ssl.trustStorePassword</name>
      <value>password</value>
    </systemProperty>
  </systemProperties>
            

13.4.3. 为tomcat配置SSL

要运行支持SSL的tomcat,把server.jks文件放到tomcat的conf目录下,然后把下面的连接器添加到server.xml文件中。

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"
            clientAuth="true" sslProtocol="TLS"
            keystoreFile="${catalina.home}/conf/server.jks"
            keystoreType="JKS" keystorePass="password"
            truststoreFile="${catalina.home}/conf/server.jks"
            truststoreType="JKS" truststorePass="password"
/>
            

如果你希望客户端没有提供证书的时候SSL链接也能成功,也可以把clientAuth设置成want。

实例在ch103。