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中的值。
本文来自博客园,作者:洛神灬殇,转载请注明原文链接:https://www.cnblogs.com/liboware/p/12510230.html,任何足够先进的科技,都与魔法无异。