前面用了不少的篇幅介绍了怎么自定义一些常用的CAS服务端的功能,本文将集成CAS客户端,继续验证CAS服务。
1、规划与实现
本文的需求是创建两个APP接入cas服务器,验证cas单点登录的功能。
首先,规划一下app与CAS服务器的域名和端口。我一直有个梦想,就是把jd和taobao整合成一个庞大的超级集团,嗯,那么为了两个网站可以统一登录,现在需要把它们的应用接入到我们提供的单点登录服务中。下面给除了规划的一些域名端口。
序号 | 服务器名称 | 域名 | 访问端口 |
1 | app-jd-edu | http://jd.edu | 9443 |
2 | app-taobao-edu | http://taobao.edu | 9444 |
3 | cas-server-webapp | http://passport.edu | 18080 |
项目中建立两个web app工程,分别对应于jd和taobao
web app添加cas client的依赖
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
</dependency>
jd和taobao的web.xml中分别加入过滤器,用于cas server与cas client之间的协议通信。具体每个过滤器的作用将会在第二节中介绍。
jd.edu的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_3_0.xsd"
id="WebApp_ID" version="3.0">
<context-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://passport.edu:18080</param-value>
</context-param>
<!-- 用于实现单点登出功能 可选 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>
<!-- 登出功能,单点退出配置,一定要放在其他filter之前可选 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://passport.edu:18080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--登录认证-->
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://passport.edu:18080/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://jd.edu:9443</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--该过滤器负责对Ticket的校验工作 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter
</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://passport.edu:18080</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://jd.edu:9443</param-value>
</init-param>
<!--表示是否验证通过后重新跳转到该URL,但是不带参数ticket,默认为true-->
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
<!--
在验证ticket成功后会生成一个Assertion对象,如果useSession为true,则会将该对象存放到Session中。
如果为false,则要求每次请求都需要携带ticket进行验证,显然useSession为false跟redirectAfterValidation为true是冲突的。
默认为true。
-->
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
<!--
<init-param>
<param-name>acceptAnyProxy</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>proxyReceptorUrl</param-name>
<param-value>/sample/proxyUrl</param-value>
</init-param>
<init-param>
<param-name>proxyCallbackUrl</param-name>
<param-value>http://jd.edu:9443/sample/proxyUrl</param-value>
</init-param>
-->
<!--
<init-param>
<param-name>authn_method</param-name>
<param-value>mfa-duo</param-value>
</init-param>
-->
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<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>
<welcome-file-list>
<welcome-file>
index.jsp
</welcome-file>
</welcome-file-list>
</web-app>
taobao.edu的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_3_0.xsd"
id="WebApp_ID" version="3.0">
<context-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://passport.edu:18080</param-value>
</context-param>
<!-- 用于实现单点登出功能 可选 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 登出功能,单点退出配置,一定要放在其他filter之前可选 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://passport.edu:18080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--登录认证-->
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://passport.edu:18080/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://taobao.edu:9444</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--该过滤器负责对Ticket的校验工作 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://passport.edu:18080</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://taobao.edu:9444</param-value>
</init-param>
<!--表示是否验证通过后重新跳转到该URL,但是不带参数ticket,默认为true-->
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
<!--
在验证ticket成功后会生成一个Assertion对象,如果useSession为true,则会将该对象存放到Session中。
如果为false,则要求每次请求都需要携带ticket进行验证,显然useSession为false跟redirectAfterValidation为true是冲突的。
默认为true。
-->
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
<!--
<init-param>
<param-name>acceptAnyProxy</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>proxyReceptorUrl</param-name>
<param-value>/sample/proxyUrl</param-value>
</init-param>
<init-param>
<param-name>proxyCallbackUrl</param-name>
<param-value>http://taobao.edu:9444/sample/proxyUrl</param-value>
</init-param>
-->
<!--
<init-param>
<param-name>authn_method</param-name>
<param-value>mfa-duo</param-value>
</init-param>
-->
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<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>
<welcome-file-list>
<welcome-file>
index.jsp
</welcome-file>
</welcome-file-list>
</web-app>
上面的cas登录认证过滤器拦截了所有的请求。当浏览器访问jd/taobao应用的时候,不过URL是什么,都会拦截进行验证,如果没有登录将会重定向到cas server。我们在两个web app中定义了一个index.jsp文件,该文件将显示用户登录后的一些信息。由于两个app的index.jsp文件大同小异,所以下面只列出其中一个文件的内容。
清单:index.jsp
<%@page contentType="text/html" %>
<%@page pageEncoding="UTF-8" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="java.util.List" %>
<%@ page import="org.jasig.cas.client.authentication.AttributePrincipal" %>
<%@ page import="org.jasig.cas.client.configuration.ConfigurationKeys" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JD的应用首页</title>
</head>
<body>
<h1>JD的应用</h1>
<hr>
<%--输出登录后的用户名--%>
<p>
<span style="font-weight: bold;">Authenticated User Id:</span><%= request.getRemoteUser() %>
</p>
<p>
<span style="font-weight: bold;">
<a href="<%= request.getServletContext().getInitParameter(ConfigurationKeys.CAS_SERVER_URL_PREFIX.getName())%>/logout">退出登录</a>
</span>
</p>
<%
if (request.getUserPrincipal() != null) {
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
final Map attributes = principal.getAttributes();
/*遍历输出principal中所有的属性*/
if (attributes != null) {
Iterator attributeNames = attributes.keySet().iterator();
out.println("<b>Attributes:</b>");
if (attributeNames.hasNext()) {
out.println("<hr><table border='3pt' width='100%'>");
out.println("<th colspan='2'>Attributes</th>");
out.println("<tr><td><b>Key</b></td><td><b>Value</b></td></tr>");
for (; attributeNames.hasNext(); ) {
out.println("<tr><td>");
String attributeName = (String) attributeNames.next();
out.println(attributeName);
out.println("</td><td>");
final Object attributeValue = attributes.get(attributeName);
if (attributeValue instanceof List) {
final List values = (List) attributeValue;
out.println("<strong>Multi-valued attribute: " + values.size() + "</strong>");
out.println("<ul>");
for (Object value : values) {
out.println("<li>" + value + "</li>");
}
out.println("</ul>");
} else {
out.println(attributeValue);
}
out.println("</td></tr>");
}
out.println("</table>");
} else {
out.print("No attributes are supplied by the CAS server.</p>");
}
} else {
out.println("<pre>The attribute map is empty. Review your CAS filter configurations.</pre>");
}
} else {
out.println("<pre>The user principal is empty from the request object. Review the wrapper filter configuration.</pre>");
}
%>
</body>
</html>
下面根据上面的配置,一起来验证以下两个不同 域名的app是否可以统一登录。当然,因为上述分配域名是不存在的,所以我们还需要修改以下host
# cas
# port:18080
127.0.0.1 passport.edu
# port:9443
127.0.0.1 jd.edu
# port:9444
127.0.0.1 taobao.edu
2、验证
首先,在没有登录过的情况下,不管输入 http://jd.edu:9443 ,还是 http://taobao.edu:9444,页面都会重定向到 http://passport.edu/login。
其中一个app登录成功后,当我们再输入 http://jd.edu:9443和http://taobao.edu:9444时,两者都顺利进入了index.jsp
当其中一个应用的index.jsp点击“退出登录”时,再次输入 http://jd.edu:9443 或者 http://taobao.edu:9444 时,都被要求重新登录了。
这里遗留了一个问题,就是等我们点击退出登录时,其实是调用cas server的URL: http://passport.edu:18080/logout ,等成功退出登录后,跳转的页面是cas server内置的logout页面,这其实不是我们想要的,我们一般在登出后,会跳转到登录页,或者是某个应用的主页,这个问题我们在下一篇文章解决。
3、CAS客户端的参数说明
为了无侵入地完成cas的一些功能,cas client定义了一些过滤器,它帮助我们不知不觉地完成cas协议。下面将介绍一些主要的过滤器的作用。
3.1 SingleSignOutFilter
SingleSignOutFilter 需要配置在所有Filter之前,当 Cas Client 通过 Cas Server 登录成功后,Cas Server会携带生成的 Service Ticket 回调 Cas Client,此时 SingleSignOutFilter 会将Service Ticket与当前的Session 绑定在一起。当Cas Server在进行logout后回调Cas Client应用时也会携带该Service Ticket,此时Cas Client配置的SingleSignOutFilter将会使对应的Session失效,进而达到登出的目的。
3.2 SingleSignOutHttpSessionListener
SingleSignOutHttpSessionListener用于在Cas Client应用中的Session过期时将其从对应的映射关系中移除。
3.3 AuthenticationFilter
AuthenticationFilter用来拦截所有的请求,用以判断用户是否需要通过Cas Server进行认证,如果需要登录则将跳转到Cas Server的登录页面。如果不需要进行登录认证,则请求会继续往下执行。
AuthenticationFilter有两个必须指定的参数,一个是用来指定Cas Server登录地址的casServerLoginUrl,另一个是用来指定认证成功后需要跳转地址的 serverName 或 service。service和serverName只需要指定一个就可以了。当两者都指定了,参数service将具有更高的优先级,即将以service指定的参数值为准。service和serverName的区别在于service指定的是一个确定的URL,认证成功后就会确切的跳转到service指定的URL;而serverName则是用来指定主机名,其格式为{protocol}:{hostName}:{port},如:https://localhost:8443,当指定的是serverName时,AuthenticationFilter将会把它附加上当前请求的URI,以及对应的查询参数来构造一个确定的URL,如指定serverName为“http://localhost”,而当前请求的URI为“/app”,查询参数为“a=b&b=c”,则对应认证成功后的跳转地址将为“http://localhost/app?a=b&b=c”。
3.4 TicketValidationFilter
在请求通过AuthenticationFilter的认证之后,如果请求中携带了参数ticket则将会由TicketValidationFilter来对携带的ticket进行校验。TicketValidationFilter只是对验证ticket的这一类Filter的统称,其并不对应Cas Client中的一个具体类型。Cas Client中有多种验证ticket的Filter,都继承自AbstractTicketValidationFilter,它们的验证逻辑都是一致的,都有AbstractTicketValidationFilter实现,所不同的是使用的TicketValidator不一样。
<!--该过滤器负责对Ticket的校验工作 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter
</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://passport.edu:18080</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://jd.edu:9443</param-value>
</init-param>
<!--表示是否验证通过后重新跳转到该URL,但是不带参数ticket,默认为true-->
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
<!--
在验证ticket成功后会生成一个Assertion对象,如果useSession为true,则会将该对象存放到Session中。
如果为false,则要求每次请求都需要携带ticket进行验证,显然useSession为false跟redirectAfterValidation为true是冲突的。
默认为true。
-->
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
<!--
<init-param>
<param-name>acceptAnyProxy</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>proxyReceptorUrl</param-name>
<param-value>/sample/proxyUrl</param-value>
</init-param>
<init-param>
<param-name>proxyCallbackUrl</param-name>
<param-value>http://jd.edu:9443/sample/proxyUrl</param-value>
</init-param>
-->
<!--
<init-param>
<param-name>authn_method</param-name>
<param-value>mfa-duo</param-value>
</init-param>
-->
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
casServerUrlPrefix:用来指定Cas Server对应URL地址的前缀,如上面示例的“https://elim:8443/cas”。
serverName或service:语义跟前面介绍的一致。
redirectAfterValidation :表示是否验证通过后重新跳转到该URL,但是不带参数ticket,默认为true。
useSession :在验证ticket成功后会生成一个Assertion对象,如果useSession为true,则会将该对象存放到Session中。如果为false,则要求每次请求都需要携带ticket进行验证,显然useSession为false跟redirectAfterValidation为true是冲突的。默认为true。
exceptionOnValidationFailure :表示ticket验证失败后是否需要抛出异常,默认为true。
3.5 HttpServletRequestWrapperFilter
HttpServletRequestWrapperFilter用于将每一个请求对应的HttpServletRequest封装为其内部定义的CasHttpServletRequestWrapper,该封装类将利用之前保存在Session或request中的Assertion对象重写HttpServletRequest的getUserPrincipal()、getRemoteUser()和isUserInRole()方法。这样在我们的应用中就可以非常方便的从HttpServletRequest中获取到用户的相关信息。