• SpringBoot集成SpringSecurity+CAS


    1 简介

    本文主要讲述如何通过SpringSecurity+CAS在springboot项目中实现单点登录和单点注销的功能。

    2 项目依赖

    主要依赖如下

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-cas</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

    3 项目配置

    Application配置。

    @SpringBootApplication(scanBasePackages = "com.wawscm")
    @EnableWebSecurity
    public class Application {

      public static void main(String[] args) {

        new SpringApplicationBuilder(Application.class).web(true).run(args);
      }

    }


    增加CAS参数配置

      这里分为CASServer配置和CASService配置。其中Server是CAS服务的配置,Service是我们自己服务的配置。

    @Data
    @ConfigurationProperties(prefix = "security.cas.server")
    public class CasServerConfig {
      private String host;
      private String login;
      private String logout;
    }
    
    @Data
    @ConfigurationProperties(prefix = "security.cas.service")
    public class CasServiceConfig {
      private String host;
      private String login;
      private String logout;
      private Boolean sendRenew = false;
    }

    配置内容如下

    security:
      cas:
       server:
        host: http://192.168.1.202:9082/cas
        login: ${security.cas.server.host}/login
        logout: ${security.cas.server.host}/logout
       service:
        host: http://localhost:9088
        login: /login/cas
        logout: /logout


    后面需要根据实际配置再拼接参数。

    SpringSecurity Bean配置

    @Configuration
    @EnableConfigurationProperties({CasServerConfig.class, CasServiceConfig.class})
    public class SecurityConfiguration {
    
      @Autowired
      private CasServerConfig casServerConfig;
    
      @Autowired
      private CasServiceConfig casServiceConfig;
    
      @Bean
      public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService(this.casServiceConfig.getHost() + this.casServiceConfig.getLogin());
        serviceProperties.setSendRenew(this.casServiceConfig.getSendRenew());
        return serviceProperties;
      }
    
      @Bean
      public CasAuthenticationFilter casAuthenticationFilter(AuthenticationManager authenticationManager, ServiceProperties serviceProperties) {
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        casAuthenticationFilter.setAuthenticationManager(authenticationManager);
        casAuthenticationFilter.setServiceProperties(serviceProperties);
        casAuthenticationFilter.setFilterProcessesUrl(this.casServiceConfig.getLogin());
        casAuthenticationFilter.setContinueChainBeforeSuccessfulAuthentication(false);
        casAuthenticationFilter.setAuthenticationSuccessHandler(
          new SimpleUrlAuthenticationSuccessHandler("/")
        );
        return casAuthenticationFilter;
      }
    
      @Bean
      public CasAuthenticationEntryPoint casAuthenticationEntryPoint(ServiceProperties serviceProperties) {
        CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
        entryPoint.setLoginUrl(this.casServerConfig.getLogin());
        entryPoint.setServiceProperties(serviceProperties);
        return entryPoint;
      }
    
      @Bean
      public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
        return new Cas20ServiceTicketValidator(this.casServerConfig.getHost());
      }
    
      @Bean
      public CasAuthenticationProvider casAuthenticationProvider(
        AuthenticationUserDetailsService<CasAssertionAuthenticationToken> userDetailsService,
        ServiceProperties serviceProperties, Cas20ServiceTicketValidator ticketValidator) {
        CasAuthenticationProvider provider = new CasAuthenticationProvider();
        provider.setKey("casProvider");
        provider.setServiceProperties(serviceProperties);
        provider.setTicketValidator(ticketValidator);
        provider.setAuthenticationUserDetailsService(userDetailsService);
        return provider;
      }
    
      @Bean
      public LogoutFilter logoutFilter() {
        String logoutRedirectPath = this.casServerConfig.getLogout() + "?service=" + this.casServiceConfig.getHost();
        LogoutFilter logoutFilter = new LogoutFilter(logoutRedirectPath, new SecurityContextLogoutHandler());
        logoutFilter.setFilterProcessesUrl(this.casServiceConfig.getLogout());
        return logoutFilter;
      }
    }

    ServiceProperties :服务配置,我们自己的服务。

    CasAuthenticationFilter:CAS认证过滤器,主要实现票据认证和认证成功后的跳转。

    LogoutFilter:注销功能

    Spring Security配置

    @Configuration
    @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
    public class CasWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
      @Autowired
      private CasAuthenticationEntryPoint casAuthenticationEntryPoint;
    
      @Autowired
      private CasAuthenticationProvider casAuthenticationProvider;
    
      @Autowired
      private CasAuthenticationFilter casAuthenticationFilter;
    
      @Autowired
      private LogoutFilter logoutFilter;
    
      @Autowired
      private CasServerConfig casServerConfig;
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();     http.csrf().disable();     http.authorizeRequests()     .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()     .antMatchers("/static/**").permitAll() // 不拦截静态资源     .antMatchers("/api/**").permitAll() // 不拦截对外API     .anyRequest().authenticated(); // 所有资源都需要登陆后才可以访问。     http.logout().permitAll(); // 不拦截注销     http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint);
        // 单点注销的过滤器,必须配置在SpringSecurity的过滤器链中,如果直接配置在Web容器中,貌似是不起作用的。我自己的是不起作用的。     SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();     singleSignOutFilter.setCasServerUrlPrefix(this.casServerConfig.getHost());     http.addFilter(casAuthenticationFilter)
        .addFilterBefore(logoutFilter, LogoutFilter.class)     .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class);      http.antMatcher("/**");   }   @Autowired   public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {     auth.authenticationProvider(casAuthenticationProvider);   }   @Bean   public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener(){     ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> servletListenerRegistrationBean =     new ServletListenerRegistrationBean<>();     servletListenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());     return servletListenerRegistrationBean;   } }

      到此SpringBoot、SpringSecurity、CAS集成结束。但是这样配置有一个问题,那就是如果我们登录之前的请求是带参数的,或者跳转的不是首页,那么就会出现登录成功之后直接跳转到主页,而不是我们想要访问的页面,参数也丢失了。下面我们来解决这个问题。

    4 、处理回跳地址

      处理的思路是,在登录之前记住访问地址及请求参数,在登录成功之后再取到这个地址然后回跳到对应的地址。

      首先我们需要写一个过滤器来获取我们的请求地址,并放到Session中。

    public class HttpParamsFilter implements Filter {
    
      public String REQUESTED_URL = "CasRequestedUrl";   @Override   public void init(FilterConfig filterConfig) throws ServletException {   }   @Override
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;     
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        
        String requestPath = WebUtils.getFullPath(request);
        session.setAttribute(REQUESTED_URL, requestPath);     
        chain.doFilter(request, response);   }   @Override   public void destroy() {   } }

    然后在CasWebSecurityConfiguration中增加对应的配置。

     @Bean
      public FilterRegistrationBean httpParamsFilter() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new HttpParamsFilter());
        filterRegistrationBean.setOrder(-999);
        filterRegistrationBean.addUrlPatterns("/"); 
        return filterRegistrationBean;
      }

    然后扩展SimpleUrlAuthenticationSuccessHandler来实现我们的功能。

    public class MyUrlAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    
      public NeteaseUrlAuthenticationSuccessHandler() {
        super();
      }
    
      public NeteaseUrlAuthenticationSuccessHandler(String defaultTargetUrl) {
        super(defaultTargetUrl);
      }
    
    @Override
    protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
      if (isAlwaysUseDefaultTargetUrl()) {
        return this.getDefaultTargetUrl();
      }
      // Check for the parameter and use that if available
      String targetUrl = null;
      if (this.getTargetUrlParameter() != null) {
        targetUrl = request.getParameter(this.getTargetUrlParameter());
        if (StringUtils.hasText(targetUrl)) {
          logger.debug("Found targetUrlParameter in request: " + targetUrl);
        return targetUrl;
     }
    }
    
      if (!StringUtils.hasText(targetUrl)) {
        HttpSession session = request.getSession();
        targetUrl = (String) session.getAttribute(HttpParamsFilter.REQUESTED_URL);
      }
    
      if (!StringUtils.hasText(targetUrl)) {
        targetUrl = this.getDefaultTargetUrl();
        logger.debug("Using default Url: " + targetUrl);
      }
    
      return targetUrl;
      }
    }

      最后将CasAuthenticationFilter中的SimpleUrlAuthenticationSuccessHandler替换为MyUrlAuthenticationSuccessHandler就可以了。

      这里需要注意一个问题,由于CAS回调是访问的/login/cas(这里是我的配置),所以过滤器一定不能拦截/login/cas否则HttpParamsFilter会将/login/cas放到Session中,就出现了无限循环。

    1. 访问http://host/?id=1 -- session: /?id=1

    2. CAS登录成功,然后回跳到login/cas?ticket=xxx -- session: login/cas?ticket=xxx

    3. 验证票据成功NeteaseUrlAuthenticationSuccessHandler处理跳转,从session中获取跳转地址:login/cas?ticket=xxx

    4. 跳转到`login/cas?ticket=xxx`然后重复步骤 2-4

    主要是我们保留了请求中的参数,所以一直会有票据信息。所以就出现了无限循环。如果没有保留票据信息,就直接报错了,因为第二次访问的时候票据丢了。

    由于我的是单页应用,所以我直接拦截主页就可以了。

    另一种处理方法是在HttpParamsFilter判断访问地址,如果是login/cas就不更新Session中的值。

  • 相关阅读:
    Windows环境配置HTTP服务(Windows + Apache + Mysql + PHP)
    浏览器兼容innerText nextElementSibling firstElementChild
    JavaScript倒计时
    JavaScript数组去重
    模拟javascript中的sort排序
    相对路径与绝对路径
    近阶段的总结
    随机改变颜色返回#+...样式的值
    pc端图片文件上传
    小程序开发的心得
  • 原文地址:https://www.cnblogs.com/liboware/p/12510230.html
Copyright © 2020-2023  润新知