单点登录 - 添加CAS客户端

原创
2017/07/17 16:28
阅读数 7.7K

前面用了不少的篇幅介绍了怎么自定义一些常用的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中获取到用户的相关信息。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部
返回顶部
顶部