CAS单点登录实战和简介

首先看一个我在实际项目中的客户端xml配置的应用,实际上可以用更好的javaconfig方式配置,这样免于web.xml里参数只能在项目打包的时候才会注入的缺陷:

工程需要引入的jar包:

在工程pom中添加文件:

<dependency>
   <groupId>org.jasig.cas.client</groupId>
   <artifactId>cas-client-core</artifactId>
   <version>3.4.0</version>
</dependency>

web.xml里面配置:
<!-- cas 配置开始 -->
<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>
    <init-param>
        <param-name>casServerUrlPrefix</param-name>
        <param-value>http://ssosit.trace.com/cas</param-value>
        <!--<param-value>${casServerUrlPrefix}</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>CASFilter</filter-name>
    <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
    <init-param>
        <param-name>casServerLoginUrl</param-name>
        <param-value>http://ssosit.trace.com/cas/login</param-value>
        <!--<param-value>${casServerLoginUrl}</param-value>-->
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://localhost:8080</param-value>
        <!--<param-value>${serverName}</param-value>-->
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CASFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<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://ssosit.trace.com/cas</param-value>
        <!--<param-value>${casServerUrlPrefix}</param-value>-->
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://localhost:8080</param-value>
        <!--<param-value>${serverName}</param-value>-->
    </init-param>
    <init-param>
        <param-name>proxyGrantingTicketStorageClass</param-name>
        <param-value>org.jasig.cas.client.proxy.EhcacheBackedProxyGrantingTicketStorageImpl</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>
<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>
<!-- cas 配置结束 -->

<!-- cas 配置退出 -->
<context-param>
    <param-name>casServerLogoutUrl</param-name>
    <param-value>http://ssosit.trace.com/cas/logout?service=http://localhost:8080/ctxxx-web
    </param-value>
    <!--<param-value>${casServerLogoutUrl}</param-value>-->
</context-param>
<!--登录拦截器 开始-->
<filter>
    <filter-name>sessionFilter</filter-name>
    <filter-class>com.suning.cxxx.web.filter.SessionFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>sessionFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!--登录拦截器 结束-->

 

SessionFilter类配置:
@Slf4j
public class SessionFilter implements Filter {

    public void destroy() {

    }

    public void doFilter(ServletRequest arg0, ServletResponse arg1,
                         FilterChain arg2) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) arg0;
        HttpSession session = request.getSession(true);
        UserEntity user = (UserEntity) session.getAttribute("user");
        UserEntity casUser = getUser(request);
        if (casUser == null) {
            if (user != null) {
                user.setRealName(URLDecoder.decode(user.getRealName(), "utf-8"));
                String ctssoIgnore = (String) session.getAttribute("ctsso-ignore");
                String demoIgnore = (String) session.getAttribute("demo-ignore");
                if (ctssoIgnore == null && demoIgnore == null) {
                    session.setAttribute("user", null);
                }
                session.setAttribute("casuser", casUser);
                request.getSession(true).setAttribute("user", casUser);
            }
        } else {
            session.setAttribute("casuser", casUser);
            request.getSession(true).setAttribute("user", casUser);
        }
        arg2.doFilter(arg0, arg1);
    }

    public void init(FilterConfig config) {

    }

    private UserEntity getUser(HttpServletRequest request) {
        AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
        if (principal == null) { // 没登录
            return null;
        }
        Map<String, Object> attributes = principal.getAttributes();
        UserEntity user = new UserEntity();
        try {
            String email = (String) attributes.get("email");
            if (StringUtils.isNotBlank(email)) {
                user.setEmail(java.net.URLDecoder.decode(email, "utf-8"));
            }
            String realName = (String) attributes.get("realName");
            if (StringUtils.isNotBlank(realName)) {
                user.setRealName(java.net.URLDecoder.decode(realName, "utf-8"));
            }
            String roleNum = (String) attributes.get("roleNum");
            if (StringUtils.isNotBlank(roleNum)) {
                user.setRoleNum(java.net.URLDecoder.decode(roleNum, "utf-8"));
            }
            String userName = (String) attributes.get("userName");
            if (StringUtils.isNotBlank(userName)) {
                user.setUserName(java.net.URLDecoder.decode(userName, "utf-8"));
            }
            String deptName = (String) attributes.get("deptName");
            if (StringUtils.isNotBlank(deptName)) {
                user.setDeptName(java.net.URLDecoder.decode(deptName, "utf-8"));
            }
            String role_en = (String) attributes.get("role_en");
            if (StringUtils.isNotBlank(role_en)) {
                user.setRole_en(java.net.URLDecoder.decode(role_en, "utf-8"));
            }
            String firstCenter = (String) attributes.get("firstCenter");
            if (StringUtils.isNotBlank(firstCenter)) {
                user.setFirstCenter(java.net.URLDecoder.decode(firstCenter, "utf-8"));
            }
            String secondCenter = (String) attributes.get("secondCenter");
            if (StringUtils.isNotBlank(secondCenter)) {
                user.setSecondCenter(java.net.URLDecoder.decode(secondCenter, "utf-8"));
            }
            user.setInnerUser((Boolean) attributes.get("innerUser"));
        } catch (Exception e) {
            log.error("解析user异常!");
        }
        return user;
    }
}
UserEntity 类:
@Data
@Accessors(chain = true)
public class UserEntity implements Serializable {

    private static final long serialVersionUID = 8137740902289144089L;
    //工号
    private String userName;
    //姓名
    private String realName;
    //邮箱
    private String email;
    //部门
    private String deptName;
    //角色
    private String roleNum;
    //角色的英文名称
    private String role_en;
    //一级中心
    private String firstCenter;
    //二级中心
    private String secondCenter;
    //是否内部员工
    private boolean innerUser;
}

退出的control:
/**
 * 登陆页面
 *
 * @author 17074536
 */
@Controller
@Slf4j
public class LoginControllor extends BaseController {

    /**
     * 登出页
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/sso/logout.do")
    @Action(type = ViewType.API, name = "logout")
    public Object logout(HttpServletRequest request, HttpServletResponse response) {
        try {
            request.getSession().invalidate();
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                int len = cookies.length;
                for (int i = 0; i < len; i++) {
                    Cookie cookie = new Cookie(cookies[i].getName(), null);
                    cookie.setMaxAge(0);
                    cookie.setPath("/cas");
                    response.addCookie(cookie);
                }
            }
            String casServerLogoutUrl = request.getSession().getServletContext().getInitParameter("casServerLogoutUrl");
            response.sendRedirect(casServerLogoutUrl);
        } catch (Exception e) {
            log.error("Exception occurs when logout,{}", e.getMessage(), e);
        }
        return null;
    }
}

简单介绍单点登录原理,以下来自:https://www.cnblogs.com/lihuidu/p/6495247.html 以及 https://blog.csdn.net/guanzhongye007/article/details/78796996

1、基于Cookie的单点登录的回顾

      

基于Cookie的单点登录核心原理:

      将用户名密码加密之后存于Cookie中,之后访问网站时在过滤器(filter)中校验用户权限,如果没有权限则从Cookie中取出用户名密码进行登录,让用户从某种意义上觉得只登录了一次。

      该方式缺点就是多次传送用户名密码,增加被盗风险,以及不能跨域。同时www.qiandu.com与mail.qiandu.com同时拥有登录逻辑的代码,如果涉及到修改操作,则需要修改两处。

2、统一认证中心方案原理

      在生活中我们也有类似的相关生活经验,例如你去食堂吃饭,食堂打饭的阿姨(www.qiandu.com)告诉你,不收现金。并且告诉你,你去门口找换票的(passport.com)换小票。于是你换完票之后,再去找食堂阿姨,食堂阿姨拿着你的票,问门口换票的,这个票是真的吗?换票的说,是真的,于是给你打饭了。
      基于上述生活中的场景,我们将基于Cookie的单点登录改良以后的方案如下:
      

      经过分析,Cookie单点登录认证太过于分散,每个网站都持有一份登陆认证代码。于是我们将认证统一化,形成一个独立的服务。当我们需要登录操作时,则重定向到统一认证中心http://passport.com。于是乎整个流程就如上图所示:
      第一步:用户访问www.qiandu.com。过滤器判断用户是否登录,没有登录,则重定向(302)到网站http://passport.com。
      第二步:重定向到passport.com,输入用户名密码。passport.com将用户登录的信息记录到服务器的session中。
      第三步:passport.com给浏览器发送一个特殊的凭证,浏览器将凭证交给www.qiandu.com,www.qiandu.com则拿着浏览器交给他的凭证去passport.com验证凭证是否有效,从而判断用户是否登录成功。
      第四步:登录成功,浏览器与网站之间进行正常的访问。

3、Yelu大学研发的CAS(Central Authentication Server)

下面就以耶鲁大学研发的CAS为分析依据,分析其工作原理。首先看一下最上层的项目部署图:

      

部署项目时需要部署一个独立的认证中心(cas.qiandu.com),以及其他N个用户自己的web服务。

认证中心:也就是cas.qiandu.com,即cas-server。用来提供认证服务,由CAS框架提供,用户只需要根据业务实现认证的逻辑即可。

用户web项目:只需要在web.xml中配置几个过滤器,用来保护资源,过滤器也是CAS框架提供了,即cas-client,基本不需要改动可以直接使用。

4、CAS的详细登录流程

上图是3个登录场景,分别为:第一次访问www.qiandu.com、第二次访问、以及登录状态下第一次访问mail.qiandu.com。

下面就详细说明上图中每个数字标号做了什么,以及相关的请求内容,响应内容。

4.1、第一次访问www.qiandu.com

标号1:用户访问http://www.qiandu.com,经过他的第一个过滤器(cas提供,在web.xml中配置)AuthenticationFilter。

      过滤器全称:org.jasig.cas.client.authentication.AuthenticationFilter

      主要作用:判断是否登录,如果没有登录则重定向到认证中心。

标号2:www.qiandu.com发现用户没有登录,则返回浏览器重定向地址。

      

      首先可以看到我们请求www.qiandu.com,之后浏览器返回状态码302,然后让浏览器重定向到cas.qiandu.com并且通过get的方式添加参数service,该参数目的是登录成功之后会要重定向回来,因此需要该参数。并且你会发现,其实server的值就是编码之后的我们请求www.qiandu.com的地址。

标号3:浏览器接收到重定向之后发起重定向,请求cas.qiandu.com。

标号4:认证中心cas.qiandu.com接收到登录请求,返回登陆页面。

      

      上图就是标号3的请求,以及标号4的响应。请求的URL是标号2返回的URL。之后认证中心就展示登录的页面,等待用户输入用户名密码。

标号5:用户在cas.qiandu.com的login页面输入用户名密码,提交。

标号6:服务器接收到用户名密码,则验证是否有效,验证逻辑可以使用cas-server提供现成的,也可以自己实现。

      

      上图就是标号5的请求,以及标号6的响应了。当cas.qiandu.com即csa-server认证通过之后,会返回给浏览器302,重定向的地址就是Referer中的service参数对应的值。后边并通过get的方式挟带了一个ticket令牌,这个ticket就是ST(数字3处)。同时会在Cookie中设置一个CASTGC,该cookie是网站cas.qiandu.com的cookie,只有访问这个网站才会携带这个cookie过去。

      Cookie中的CASTGC:向cookie中添加该值的目的是当下次访问cas.qiandu.com时,浏览器将Cookie中的TGC携带到服务器,服务器根据这个TGC,查找与之对应的TGT。从而判断用户是否登录过了,是否需要展示登录页面。TGT与TGC的关系就像SESSION与Cookie中SESSIONID的关系。

      TGT:Ticket Granted Ticket(俗称大令牌,或者说票根,他可以签发ST)

      TGC:Ticket Granted Cookie(cookie中的value),存在Cookie中,根据他可以找到TGT。

      ST:Service Ticket (小令牌),是TGT生成的,默认是用一次就生效了。也就是上面数字3处的ticket值。

标号7:浏览器从cas.qiandu.com哪里拿到ticket之后,就根据指示重定向到www.qiandu.com,请求的url就是上面返回的url。

      

标号8:www.qiandu.com在过滤器中会取到ticket的值,然后通过http方式调用cas.qiandu.com验证该ticket是否是有效的。

标号9:cas.qiandu.com接收到ticket之后,验证,验证通过返回结果告诉www.qiandu.com该ticket有效。

标号10:www.qiandu.com接收到cas-server的返回,知道了用户合法,展示相关资源到用户浏览器上。

      

      至此,第一次访问的整个流程结束,其中标号8与标号9的环节是通过代码调用的,并不是浏览器发起,所以没有截取到报文。

4.2、第二次访问www.qiandu.com

上面以及访问过一次了,当第二次访问的时候发生了什么呢?

标号11:用户发起请求,访问www.qiandu.com。会经过cas-client,也就是过滤器,因为第一次访问成功之后www.qiandu.com中会在session中记录用户信息,因此这里直接就通过了,不用验证了。

标号12:用户通过权限验证,浏览器返回正常资源。

4.3、访问mail.qiandu.com

标号13:用户在www.qiandu.com正常上网,突然想访问mail.qiandu.com,于是发起访问mail.qiandu.com的请求。

标号14:mail.qiandu.com接收到请求,发现第一次访问,于是给他一个重定向的地址,让他去找认证中心登录。

      

      上图可以看到,用户请求mail.qiandu.com,然后返回给他一个网址,状态302重定向,service参数就是回来的地址。

标号15:浏览器根据14返回的地址,发起重定向,因为之前访问过一次了,因此这次会携带上次返回的Cookie:TGC到认证中心。

标号16:认证中心收到请求,发现TGC对应了一个TGT,于是用TGT签发一个ST,并且返回给浏览器,让他重定向到mail.qiandu.com

      

      可以发现请求的时候是携带Cookie:CASTGC的,响应的就是一个地址加上TGT签发的ST也就是ticket。

标号17:浏览器根据16返回的网址发起重定向。

标号18:mail.qiandu.com获取ticket去认证中心验证是否有效。

标号19:认证成功,返回在mail.qiandu.com的session中设置登录状态,下次就直接登录。

标号20:认证成功之后就反正用想要访问的资源了。

      



posted @ 2018-04-13 20:20  舞羊  阅读(283)  评论(0编辑  收藏  举报