springBoot+cas忽略拦截及session失效问题

一、写在前面

之前搭建了个springboot+cas的项目,一直想做个总结,今天刚好有时间就总结下吧。只描述自己遇到过的问题,其他的没有涉及的请见谅。cas服务端搭建在这里不做过多讲解,直接从网上下载,然后更改部分配置,放在tomcat里跑起来就可以用(不懂得百度cas服务端搭建,很多)。这里只针对cas客户端。

springboot版本:2.2.2 cas版本:1.5.0

二、搭建cas客户端

2.1 导入依赖

在springboot项目的pom文件里导入cas依赖包

  <!-- CAS依赖包 -->
        <dependency>
            <groupId>net.unicon.cas</groupId>
            <artifactId>cas-client-autoconfig-support</artifactId>
            <version>1.5.0-GA</version>
        </dependency>

2.2 配置文件修改

修改application里的配置,增加cas相关配置

#CAS 配置
#cas服务器地址
cas.server-url-prefix=http://192.168.20.69:8010/gisquest-sso
#客户端服务器地址
cas.client-host-url=http://devkpi.gisquest.com:8010/GisqPerfAssess-WebApp/a/cas-login
#默认cas登陆地址(oa)
cas.server-login-url=http://192.168.20.69:8010/gisquest-sso/login
#cas登出地址
perform.logoutUrl=http://192.168.20.69:8010/gisquest-sso/logout
cas.validation-type=CAS
#cas不拦截的url
ignore-host-url=/perf-assess/updateAssessStatus

根据自己实际情况去配,不要照搬

2.3 过滤器配置

配置关于cas的过滤器

@Configuration
public class CasFilterConfig {
    @Value("${cas.server-url-prefix}")
    private String CAS_URL;
    @Value("${cas.client-host-url}")
    private  String APP_URL;
    @Value("${ignore-host-url}")
    private  String IGNORE_URL;

    /**
     * 单点登录退出(必须放在最前面)
     * @return
     */
    @Bean
    public FilterRegistrationBean singleSignOutFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new SingleSignOutFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.addInitParameter("casServerUrlPrefix", CAS_URL );
        registrationBean.setName("CAS Single Sign Out Filter");
        registrationBean.setOrder(2);
        return registrationBean;
    }


    /**
     * @Author chenb2
     * @Description 设置监听器
     * @Date 14:24 2019/12/5
     * @Param []
     * @return org.springframework.boot.web.servlet.ServletListenerRegistrationBean
     **/
    @Bean
    public ServletListenerRegistrationBean servletListenerRegistrationBean(){
        ServletListenerRegistrationBean  listenerRegistrationBean = new ServletListenerRegistrationBean();
        listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
        listenerRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return listenerRegistrationBean;
    }



    /**
     * @Description 自定义的过滤器(session失效后跳转到登录页)
     * @Date 14:58 2020/1/6
     * @Param []
     * @return org.springframework.boot.web.servlet.FilterRegistrationBean
     **/
    @Bean
    public FilterRegistrationBean filterAuthenticationRegistration(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        // 设定匹配的路径
        registrationBean.setFilter(new MyAuthenticationFilter());
        registrationBean.addUrlPatterns("/*");
        //registrationBean.setName("CAS Filter");//加这个后无法自定义cas登录地址
        Map<String,String> initParameters = new HashMap<>();
        initParameters.put("casServerLoginUrl", CAS_URL);
        initParameters.put("serverName", APP_URL);
        //忽略的url,"|"分隔多个url
        initParameters.put("ignorePattern", IGNORE_URL);
        initParameters.put("ignoreUrlPatternType","CONTAINS");
        registrationBean.setInitParameters(initParameters);
        // 设定加载的顺序
        registrationBean.setOrder(0);
        return registrationBean;
    }

  /**
   * @Description cas的授权过滤器配置
   * @Date 15:02 2020/1/6
   * @Param []
   * @return org.springframework.boot.web.servlet.FilterRegistrationBean
   **/
    @Bean
    public FilterRegistrationBean myFilterAuthenticationRegistration(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        // 设定匹配的路径
        registrationBean.setFilter(new AuthenticationFilter());
        registrationBean.addUrlPatterns("/*");
        //registrationBean.setName("CAS Filter");//加这个后无法自定义cas登录地址
        Map<String,String> initParameters = new HashMap<>();
        initParameters.put("casServerLoginUrl", CAS_URL);
        initParameters.put("serverName", APP_URL);
        //忽略的url,"|"分隔多个url
        initParameters.put("ignorePattern", IGNORE_URL);
        initParameters.put("ignoreUrlPatternType","CONTAINS");
        registrationBean.setInitParameters(initParameters);
        // 设定加载的顺序
        registrationBean.setOrder(1);
        return registrationBean;
    }

    /**
     * 单点登录校验
     * @return
     */
    @Bean
    public FilterRegistrationBean cas20ProxyReceivingTicketValidationFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
        registrationBean.addUrlPatterns("/*");
        Map<String,String>  initParameters = new HashMap<String, String>();
        initParameters.put("casServerUrlPrefix", CAS_URL);
        initParameters.put("serverName", APP_URL);
        initParameters.put("useSession", "true");
        registrationBean.setName("CAS Validation Filter");
        registrationBean.setInitParameters(initParameters);
        registrationBean.setOrder(3);
        return registrationBean;
    }

    /**
     * 单点登录请求包装
     * @return
     */
    @Bean
    public FilterRegistrationBean httpServletRequestWrapperFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new HttpServletRequestWrapperFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("CAS HttpServletRequest Wrapper Filter");
        registrationBean.setOrder(4);
        return registrationBean;
    }

    /**
     * 单点登录本地用户信息
     * @return
     */
    @Bean
    public FilterRegistrationBean localUserInfoFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new LocalUserInfoFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("localUserInfoFilter");
        registrationBean.setOrder(5);
        return registrationBean;
    }

}

注意: CAS_URL是配置文件中cas服务端的地址
APP_URL是配置文件中cas客户端地址
IGNORE_URL是配置文件中配置的关于忽略拦截请求的地址(不需要登陆认证就可以访问该接口)

MyAuthenticationFilter和LocalUserInfoFilter是我自己定义的过滤器,这里暂时会报错,后面做讲解,其他的过滤器都是默认的,导入相关的包后不会报错。

过滤器的执行顺序:除了自己定义的过滤器之外其他的顺序建议按照上面代码中的来,避免出错。

2.4 搭建完成

过滤器配置完成之后,cas客户端其实已经已经搭建完成了,至于网上说的配置cas客户端的重定向策略,我并没有配置,也没发现什么问题。怎么样,是不是很简单。好了我们接着往下看。

三、核心过滤器

AuthenticationFilter和MyAuthenticationFilter是我今天着重想提一嘴的。

AuthenticationFilter里面定义了关于请求的处理,忽略请求的判断,session的是否存在的判断等等,有兴趣的朋友可以去看看源码。主要看doFileter方法和isRequestUrlExcluded方法

 public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        if (this.isRequestUrlExcluded(request)) {
            this.logger.debug("Request is ignored.");
            filterChain.doFilter(request, response);
        } else {
            HttpSession session = request.getSession(false);
            Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
            if (assertion != null) {
                filterChain.doFilter(request, response);
            } else {
                String serviceUrl = this.constructServiceUrl(request, response);
                String ticket = this.retrieveTicketFromRequest(request);
                boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
                if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
                    this.logger.debug("no ticket and no assertion found");
                    String modifiedServiceUrl;
                    if (this.gateway) {
                        this.logger.debug("setting gateway attribute in session");
                        modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
                    } else {
                        modifiedServiceUrl = serviceUrl;
                    }

                    this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
                    String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
                    this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
                    this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
                } else {
                    filterChain.doFilter(request, response);
                }
            }
        }
    }
   private boolean isRequestUrlExcluded(HttpServletRequest request) {
        if (this.ignoreUrlPatternMatcherStrategyClass == null) {
            return false;
        } else {
            StringBuffer urlBuffer = request.getRequestURL();
            if (request.getQueryString() != null) {
                urlBuffer.append("?").append(request.getQueryString());
            }

            String requestUri = urlBuffer.toString();
            return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
        }
    }

MyAuthenticationFilter是自定义的过滤器,目的是为了解决session过期或失效点击页面上的接口报接口异常的问题(如果是location.href的页面跳转则没有问题,如果是ajax请求则会报接口异常,百度了一通说是会存在跨域的问题,也没找到很好的办法,只能自己写过滤器处理请求)
MyAuthenticationFilter的代码如下

public class MyAuthenticationFilter  extends AbstractCasFilter {
    private static final String loginUrl = "http://deliver.gisquest.com/GisqPerfAssess-WebApp/a/cas-login";
    private String casServerLoginUrl;
    private boolean renew;
    private boolean gateway;
    private GatewayResolver gatewayStorage;
    private AuthenticationRedirectStrategy authenticationRedirectStrategy;
    private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass;
    private static final Map<String, Class<? extends UrlPatternMatcherStrategy>> PATTERN_MATCHER_TYPES = new HashMap();

    /**
     * 构造方法
     */
    public MyAuthenticationFilter() {
        this(Protocol.CAS2);
    }

    protected MyAuthenticationFilter(Protocol protocol) {
        super(protocol);
        this.renew = false;
        this.gateway = false;
        this.gatewayStorage = new DefaultGatewayResolverImpl();
        this.authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();
        this.ignoreUrlPatternMatcherStrategyClass = null;
    }

    protected void initInternal(FilterConfig filterConfig) throws ServletException {
        if (!this.isIgnoreInitConfiguration()) {
            super.initInternal(filterConfig);
            this.setCasServerLoginUrl(this.getString(ConfigurationKeys.CAS_SERVER_LOGIN_URL));
            this.setRenew(this.getBoolean(ConfigurationKeys.RENEW));
            this.setGateway(this.getBoolean(ConfigurationKeys.GATEWAY));
            String ignorePattern = this.getString(ConfigurationKeys.IGNORE_PATTERN);
            String ignoreUrlPatternType = this.getString(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE);
            Class gatewayStorageClass;
            if (ignorePattern != null) {
                gatewayStorageClass = (Class)PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType);
                if (gatewayStorageClass != null) {
                    this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy) ReflectUtils.newInstance(gatewayStorageClass.getName(), new Object[0]);
                } else {
                    try {
                        this.logger.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType);
                        this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy)ReflectUtils.newInstance(ignoreUrlPatternType, new Object[0]);
                    } catch (IllegalArgumentException var6) {
                        this.logger.error("Could not instantiate class [{}]", ignoreUrlPatternType, var6);
                    }
                }

                if (this.ignoreUrlPatternMatcherStrategyClass != null) {
                    this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern);
                }
            }

            gatewayStorageClass = this.getClass(ConfigurationKeys.GATEWAY_STORAGE_CLASS);
            if (gatewayStorageClass != null) {
                this.setGatewayStorage((GatewayResolver)ReflectUtils.newInstance(gatewayStorageClass, new Object[0]));
            }

            Class<? extends AuthenticationRedirectStrategy> authenticationRedirectStrategyClass = this.getClass(ConfigurationKeys.AUTHENTICATION_REDIRECT_STRATEGY_CLASS);
            if (authenticationRedirectStrategyClass != null) {
                this.authenticationRedirectStrategy = (AuthenticationRedirectStrategy)ReflectUtils.newInstance(authenticationRedirectStrategyClass, new Object[0]);
            }
        }

    }

    /**
     * 初始化
     */
    public void init() {
        super.init();
        CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
    }

    /**
     * 核心方法
     * @param servletRequest
     * @param servletResponse
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        //判断是否是不必要拦截的请求地址
        if (this.isRequestUrlExcluded(request)) {
            this.logger.debug("Request is ignored.");
            filterChain.doFilter(request, response);
        } else {
            //获取session,判断session是否失效
            HttpSession session = request.getSession(false);
            Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
            if (assertion != null) {
                filterChain.doFilter(request, response);
            } else {
                //session失效判断票据和断言
                String serviceUrl = this.constructServiceUrl(request, response);
                String ticket = this.retrieveTicketFromRequest(request);
                boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
                if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
                    this.logger.debug("no ticket and no assertion found");
                    String modifiedServiceUrl;
                    if (this.gateway) {
                        this.logger.debug("setting gateway attribute in session");
                        modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
                    } else {
                        modifiedServiceUrl = serviceUrl;
                    }
                    //获取请求路径
                    this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
                    String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
                    //判断请求方式是否为ajax请求
                    String header = request.getHeader("X-Requested-With");
                    if (header != null && "XMLHttpRequest".equals(header)){
                        //给这个请求打上标记(登录已经超时或者认证未通过)
                        ajaxHttpToLogin(request,response,loginUrl);
                        return;
                    }else{
                        this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
                        this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
                    }
                } else {
                    filterChain.doFilter(request, response);
                }
            }
        }
    }

    /**
     * ajax请求标记
     * @param request
     * @param response
     * @param loginUrl
     */
    private void ajaxHttpToLogin(HttpServletRequest request, HttpServletResponse response, String loginUrl) {
        try {
            response.setHeader("SESSIONSTATUS", "TIMEOUT");
            response.setHeader("CONTEXTPATH", loginUrl);
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);//403 禁止
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    public final void setRenew(boolean renew) {
        this.renew = renew;
    }

    public final void setGateway(boolean gateway) {
        this.gateway = gateway;
    }

    public final void setCasServerLoginUrl(String casServerLoginUrl) {
        this.casServerLoginUrl = casServerLoginUrl;
    }

    public final void setGatewayStorage(GatewayResolver gatewayStorage) {
        this.gatewayStorage = gatewayStorage;
    }

    private boolean isRequestUrlExcluded(HttpServletRequest request) {
        if (this.ignoreUrlPatternMatcherStrategyClass == null) {
            return false;
        } else {
            StringBuffer urlBuffer = request.getRequestURL();
            if (request.getQueryString() != null) {
                urlBuffer.append("?").append(request.getQueryString());
            }

            String requestUri = urlBuffer.toString();
            return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
        }
    }

    static {
        PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class);
        PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class);
        PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class);
    }
}

可以看到这个类是对AuthenticationFilter的部分源码进行了加工处理,对请求进行处理:session失效后判断是否是ajax请求,是的话对请求进行标记,返回状态为403,返回登陆页的地址,在前端进行处理。

在前端的页面里引入一个common.js,js里统一对403状态的请求进行处理(跳转到登陆页)

<script src="${pageContext.request.contextPath}/static/js/common.js"></script>

common.js的内容

```java
//超时异常处理
$.ajaxSetup({
    complete : function(xhr) {
        //拦截器实现超时跳转到登录页面
        // 通过xhr取得响应头
        var SESSIONSTATUS = xhr.getResponseHeader("SESSIONSTATUS");
        //如果响应头中包含 TIMEOUT 则说明是登录过期
        if (SESSIONSTATUS == "TIMEOUT"){
            var win = window;
            while (win != win.top){
                win = win.top;
            }
            //重新跳转到首页
            win.location.href = xhr.getResponseHeader("CONTEXTPATH");
        }
    }
});

LocalUserInfoFilter是一个简单的输出登陆人信息的,没啥用,我测试用的

public class LocalUserInfoFilter implements Filter {

   private static Logger logger =  LoggerFactory.getLogger(LocalUserInfoFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request_ = (HttpServletRequest)request;
        String username = CASUtil.getAccountNameFromCas(request_);
        if(StringUtils.isNotEmpty(username)){
            logger.info("访问者:{}",username);
            request_.getSession().setAttribute("username", username);
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }
}
``

四、客户端退出

退出功能直接重定向到cas服务端的退出地址(cas自己会清除票据的),想要跳转到登陆页,需要更改cas服务端的一个配置(跳转地址的service后面的参数,不会就百度吧,很好找的),直接看代码吧

@RequestMapping(value = "/logout")
public void logout(HttpSession session, HttpServletResponse response){
	session.invalidate();//清除session
	try {
		response.sendRedirect(casProperties.getLogoutUrl());
	} catch (IOException e) {
		logger.error("登出系统失败");
	}
}

五、总结

至此,关于cas客户端搭建以及忽略请求配置和session失效处理就已经讲完了,好多东西也是在慢慢摸索中解决的,如果大家有更好的方案可以在下方留言互相交流。

你可能感兴趣的:(java进阶)