• CAS方式实现单点登录SSO


    1. CAS介绍

    CAS(Central Authentication Service)中心认证服务
    下面这张图来自官网,清晰简单的介绍了CAS的继续交互过程
    在这里插入图片描述

    2. CAS具体实现

    首先需要分别搭建CAS-server和CAS-client服务,
    这两个服务分别在2台机器上,官方地址如下:

    https://github.com/apereo/java-cas-client

    2.1 搭建CAS-server

    这一步就不详细阐述了,许多公司内部都已经搭建好了CAS server,我们只需要把我们的域名注册到CAS server即可。

    2. 搭建CAS-client

    在我们自己的项目中,我们首先需要导入依赖

            <dependency>
                <groupId>org.jasig.cas.clientgroupId>
                <artifactId>cas-client-coreartifactId>
                <version>3.6.4version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    根据这个库的实现,我们还需要写2个Filter,分别为Filter1_CasAuthenticationFilterFilter2_CasTicketValidationFilter
    具体实现如下:
    Filter1_CasAuthenticationFilter

    package com.vip.data.unific.server.config;
    
    import lombok.extern.slf4j.Slf4j;
    import org.jasig.cas.client.authentication.AuthenticationFilter;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.util.Arrays;
    
    
    @WebFilter(urlPatterns = "/*")
    @Slf4j
    public class Filter1_CasAuthenticationFilter implements Filter {
    
        private AuthenticationFilter authentication;
        @Autowired
        private CasProperties casProperties;
        public Filter1_CasAuthenticationFilter() {
            super();
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
            this.authentication = new AuthenticationFilter();
            this.authentication.setIgnoreInitConfiguration(true);
            this.authentication.setServerName(casProperties.getServerName());
            this.authentication.setCasServerLoginUrl(casProperties.getCasUrlPrefix() + "/login");
            authentication.init(filterConfig);
        }
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            if (Boolean.TRUE.equals(casProperties.getSkipFilter())) {
                chain.doFilter(request, response);
                return ;
            }
            // 不需要 CAS 单点登录的页面直接跳过
            if (Arrays.stream(casProperties.getIgnorePaths()).filter(p -> request.getRequestURI().matches(p)).count() > 0) {
    //            log.info(LogMsgKit.of("doFilter").p("uri", request.getRequestURI()).end("跳过cas认证"));
                chain.doFilter(request, response);
                return;
            }
            this.authentication.doFilter(request, response, chain);
        }
        @Override
        public void destroy() {
            this.authentication.destroy();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    Filter2_CasTicketValidationFilter如下

    package com.vip.data.unific.server.config;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.*;
    
    @WebFilter(urlPatterns = "/*")
    @Slf4j
    public class Filter2_CasTicketValidationFilter implements Filter  {
    
        private static String casServerUrlPrefix = "casServerUrlPrefix";
    
        private static String serverName = "serverName";
    
        private static String encoding = "encoding";
    
        private final TicketValidationWrapper ticketValidation;
    
        @Autowired
        private CasProperties casProperties;
    
        public Filter2_CasTicketValidationFilter() {
            super();
            this.ticketValidation = new TicketValidationWrapper();
        }
    
        @Override
        public void init(final FilterConfig filterConfig) throws ServletException {
            ticketValidation.init(new FilterConfig() {
                @Override
                public String getFilterName() {
                    return filterConfig.getFilterName();
                }
    
                @Override
                public ServletContext getServletContext() {
                    return filterConfig.getServletContext();
                }
    
                @Override
                public String getInitParameter(String name) {
                    String value = null;
                    if (casServerUrlPrefix.equals(name)) {
                        value = casProperties.getCasUrlPrefix();
                    } else if (serverName.equals(name)) {
                        value = casProperties.getServerName();
                    } else if(encoding.equals(name)){
                        value = casProperties.getEncoding();
                    }
                    if (value == null) {
                        value = filterConfig.getInitParameter(name);
                    }
                    return value;
                }
    
                @Override
                public Enumeration<String> getInitParameterNames() {
                    Enumeration<String> name = filterConfig.getInitParameterNames();
                    Set<String> set = new HashSet<>();
                    while (name.hasMoreElements()) {
                        set.add(name.nextElement());
                    }
                    set.add(casServerUrlPrefix);
                    set.add(serverName);
                    set.add(encoding);
                    final Iterator<String> iterator = set.iterator();
                    set = null;
                    return new Enumeration<String>() {
                        @Override
                        public boolean hasMoreElements() {
                            return iterator.hasNext();
                        }
                        @Override
                        public String nextElement() {
                            return iterator.next();
                        }
                    };
                }
            });
            ticketValidation.setRedirectAfterValidation(false);//取消重定向,自定义重定向
        }
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) resp;
            if (Boolean.TRUE.equals(casProperties.getSkipFilter())) {
                chain.doFilter(request, response);
                return ;
            }
            // 不需要 CAS 单点登录的页面直接跳过
            if (Arrays.stream(casProperties.getIgnorePaths()).filter(p -> request.getRequestURI().matches(p)).count() > 0) {
    //            log.info(LogMsgKit.of("doFilter").p("uri", request.getRequestURI()).end("跳过cas认证"));
                chain.doFilter(request, response);
                return;
            }
            ticketValidation.doFilter(request, response, chain);
        }
    
        @Override
        public void destroy() {
            ticketValidation.destroy();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110

    这2个Filter基本就是固定写法,顺序最好不要交换(虽然交换了也能执行成功),

    3、常见问题讨论

    常见问题

    如果我有多台服务器,如何实现分布式session共享?

    单点登录其本质就是分布式session共享的一种解决方案,也就是集中管理session,所以单点登录已经解决了session共享问题

    用户访问/api/xxx路径,登录成功后跳转到/api/aaa路径,如何修改重定向路径?

    修改重定向有2个方法,一个是修改response如下,但是cas-client中是直接response.sendRedirect();所以这种方法不管用

    response.setHeader(“Location”,“/index”);
    response.setStatus(302);
    第二种方法,继承Cas20ProxyReceivingTicketValidationFilter,然后重写onSuccessfulValidation方法,自己定义重定向地址

    public class TicketValidationWrapper extends Cas20ProxyReceivingTicketValidationFilter {
     
    //    @Value("${cas.redirectURL}") //filter启动先于spring bean初始化
        public static final String redirectURL="/";
        @Override
        protected void onSuccessfulValidation(final HttpServletRequest request, final HttpServletResponse response,
                                              final Assertion assertion) {
            try {
                response.sendRedirect(redirectURL);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    为什么需要2个Filter,能写在一起吗?

    2个Filter负责的职责不同,理论上可以写在一起,但是分开写逻辑更加清楚

    这两个Filter的顺序能交换吗?

    通过实验测试,发现2个Filter交换顺序并不会影响登录,用户体验结果是一样,但是F2放在F1前面的话会多走1此Filter,建议Filter1 auth,Filter2 ticket

    Filter1_CasAuthenticationFilter
    这个主要用来判断用户是否登录,没有登录则重定向到登录界面进行登录,登录完成继续重定向到原始路径

    Filter2_CasTicketValidationFilter
    这个主要用来验证是否有ST-ticket(ticket是一次性使用的)

    session到底存储在哪?

    登录成功后,CAS-server有一个session,当用户请求的cookie中携带TGT-xxx访问CAS-server时,可以得到一个ST-ticket

    自己的app中也会存储一个session,当用户请求的cookie中携带jsessionid时,则判断为登录成功

    也就是CAS-server和自己的app都存储一份session,这两个session是不一样的

    编码时可能出现的问题?

    注册Filter时,使用2个注解即可,否则会多次注册Filer,导致一个请求执行多次Filter

    @ServletComponentScan 加在springboot启动main函数上
    @WebFilter(urlPatterns = “/*”) 加在Filter上

    Filter上不用加@Component,加了可能会报错或者filter多次注册
    Filer执行多次原因还有可能是浏览器默认请求了favicon.ico这个文件,可能检查网络请求中是否有

    参考链接:https://blog.csdn.net/chaijunkun/article/details/7646338

    在Filter进行注入时要注意,无法使用@Value注入,因为Filter启动时,springbean还没初始化?(可能)

    如何控制Filter执行顺序

    @order注解不管用,默认是按照类名,所以建议以Filer1xxx,Filter2xxx命名

    参考链接:https://www.cnblogs.com/tfgzs/p/4571137.html

    下面是一些调研

    分布式session解决方案
    方案1:Tomcat集群Session全局复制(集群内每个tomcat的session完全同步)【会影响集群的性能】

    方案2:根据请求的IP进行Hash映射到对应的机器上【如果服务器宕机了,会丢失Session的数据,实现最简单】

    方案3:引入中间件Redis,把Session数据放在Redis中,已经实现的框架有Spring session 使用Spring Session和Redis解决分布式Session共享【有一定的侵入性,实现难度中等,】

    方案4:JWT方式,user信息保存在token中,每次请求都携带token【可能存在安全问题,网络开销大一点】

    (单点登录其实就属于方案1和3的结合,CAS-server保存登录状态,每个app中也保存的单独的session)

    共同点(单点登录的核心)
    问题:单点登录的问题是session是各个系统所独自拥有的,各个系统不知道用户是否登录,无法共享用户的登录状态,
    目标/切入点:目标/切入点是 “一定要让所有的系统就都可以知道现在用户登录没有”,只要能够实现这个目标/切入点,就可以作为方案,所以 Tomcat集群Session全局复制、请求的IP一直会访问同一个服务器、引入中间件Redis 都是方案。

  • 相关阅读:
    一位Java程序猿的“炫技“:从高级特性到深奥的代码实践
    深度学习优化算法之动量法[公式推导](MXNet)
    EN 13859-2防水用柔性薄板—CE认证
    将自定义RPM包加入YUM
    深度学习与CV教程(4) | 神经网络与反向传播
    马拉车算法
    【操作系统】总结(二)linux指令
    JS对象操作(in、instanceof、delete)运算符
    Workfine新手入门:筛选条件—文本判断
    element-ui一个奇葩的故障,非常奇葩,无解
  • 原文地址:https://blog.csdn.net/qq_40733911/article/details/134205750