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中的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM