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