1、 简介
本文主要讲述如何通过SpringSecurity+CAS在springboot项目中实现单点登录和单点注销的功能。
参考内容有Spring Security官方文档中的
本文为Jaune161的原创文章。 原文地址http://blog.csdn.net/jaune161/article/details/78060780,转载请注明出处。
2、 项目依赖
主要依赖如下
1<dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-security</artifactId> 4</dependency> 5<dependency> 6 <groupId>org.springframework.security</groupId> 7 <artifactId>spring-security-cas</artifactId> 8</dependency> 9<dependency> 10 <groupId>org.projectlombok</groupId> 11 <artifactId>lombok</artifactId> 12 <optional>true</optional> 13</dependency>
3、 项目配置
Application配置。
1@SpringBootApplication(scanBasePackages = "com.wawscm") 2@EnableWebSecurity 3public class Application { 4 5 public static void main(String[] args) { 6 new SpringApplicationBuilder(Application.class).web(true).run(args); 7 } 8 9}
增加CAS参数配置
这里分为CASServer配置和CASService配置。其中Server是CAS服务的配置,Service是我们自己服务的配置。
1@Data 2@ConfigurationProperties(prefix = "security.cas.server") 3public class CasServerConfig { 4 private String host; 5 private String login; 6 private String logout; 7 8} 9 10@Data 11@ConfigurationProperties(prefix = "security.cas.service") 12public class CasServiceConfig { 13 private String host; 14 private String login; 15 private String logout; 16 private Boolean sendRenew = false; 17}
配置内容如下
1security: 2 cas: 3 server: 4 host: http://192.168.1.202:9082/cas 5 login: ${security.cas.server.host}/login 6 logout: ${security.cas.server.host}/logout 7 service: 8 host: http://localhost:9088 9 login: /login/cas 10 logout: /logout
后面需要根据实际配置再拼接参数。
SpringSecurity Bean配置
1@Configuration 2@EnableConfigurationProperties({CasServerConfig.class, CasServiceConfig.class}) 3public class SecurityConfiguration { 4 5 @Autowired 6 private CasServerConfig casServerConfig; 7 8 @Autowired 9 private CasServiceConfig casServiceConfig; 10 11 @Bean 12 public ServiceProperties serviceProperties() { 13 ServiceProperties serviceProperties = new ServiceProperties(); 14 serviceProperties.setService(this.casServiceConfig.getHost() + this.casServiceConfig.getLogin()); 15 serviceProperties.setSendRenew(this.casServiceConfig.getSendRenew()); 16 return serviceProperties; 17 } 18 19 @Bean 20 public CasAuthenticationFilter casAuthenticationFilter(AuthenticationManager authenticationManager, ServiceProperties serviceProperties) { 21 CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter(); 22 casAuthenticationFilter.setAuthenticationManager(authenticationManager); 23 casAuthenticationFilter.setServiceProperties(serviceProperties); 24 casAuthenticationFilter.setFilterProcessesUrl(this.casServiceConfig.getLogin()); 25 casAuthenticationFilter.setContinueChainBeforeSuccessfulAuthentication(false); 26 casAuthenticationFilter.setAuthenticationSuccessHandler( 27 new SimpleUrlAuthenticationSuccessHandler("/") 28 ); 29 return casAuthenticationFilter; 30 } 31 32 @Bean 33 public CasAuthenticationEntryPoint casAuthenticationEntryPoint(ServiceProperties serviceProperties) { 34 CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint(); 35 entryPoint.setLoginUrl(this.casServerConfig.getLogin()); 36 entryPoint.setServiceProperties(serviceProperties); 37 return entryPoint; 38 } 39 40 @Bean 41 public Cas20ServiceTicketValidator cas20ServiceTicketValidator() { 42 return new Cas20ServiceTicketValidator(this.casServerConfig.getHost()); 43 } 44 45 @Bean 46 public CasAuthenticationProvider casAuthenticationProvider( 47 AuthenticationUserDetailsService<CasAssertionAuthenticationToken> userDetailsService, 48 ServiceProperties serviceProperties, Cas20ServiceTicketValidator ticketValidator) { 49 CasAuthenticationProvider provider = new CasAuthenticationProvider(); 50 provider.setKey("casProvider"); 51 provider.setServiceProperties(serviceProperties); 52 provider.setTicketValidator(ticketValidator); 53 provider.setAuthenticationUserDetailsService(userDetailsService); 54 55 return provider; 56 } 57 58 @Bean 59 public LogoutFilter logoutFilter() { 60 String logoutRedirectPath = this.casServerConfig.getLogout() + "?service=" + 61 this.casServiceConfig.getHost(); 62 LogoutFilter logoutFilter = new LogoutFilter(logoutRedirectPath, new SecurityContextLogoutHandler()); 63 logoutFilter.setFilterProcessesUrl(this.casServiceConfig.getLogout()); 64 return logoutFilter; 65 } 66}
ServiceProperties :服务配置,我们自己的服务。 CasAuthenticationFilter:CAS认证过滤器,主要实现票据认证和认证成功后的跳转。 LogoutFilter:注销功能
Spring Security配置
1@Configuration 2@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) 3public class CasWebSecurityConfiguration extends WebSecurityConfigurerAdapter { 4 5 @Autowired 6 private CasAuthenticationEntryPoint casAuthenticationEntryPoint; 7 8 @Autowired 9 private CasAuthenticationProvider casAuthenticationProvider; 10 11 @Autowired 12 private CasAuthenticationFilter casAuthenticationFilter; 13 14 @Autowired 15 private LogoutFilter logoutFilter; 16 17 @Autowired 18 private CasServerConfig casServerConfig; 19 20 @Override 21 protected void configure(HttpSecurity http) throws Exception { 22 http.headers().frameOptions().disable(); 23 24 http.csrf().disable(); 25 26 http.authorizeRequests() 27 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() 28 .antMatchers("/static/**").permitAll() // 不拦截静态资源 29 .antMatchers("/api/**").permitAll() // 不拦截对外API 30 .anyRequest().authenticated(); // 所有资源都需要登陆后才可以访问。 31 32 http.logout().permitAll(); // 不拦截注销 33 34 http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint); 35 // 单点注销的过滤器,必须配置在SpringSecurity的过滤器链中,如果直接配置在Web容器中,貌似是不起作用的。我自己的是不起作用的。 36 SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter(); 37 singleSignOutFilter.setCasServerUrlPrefix(this.casServerConfig.getHost()); 38 39 http.addFilter(casAuthenticationFilter) 40 .addFilterBefore(logoutFilter, LogoutFilter.class) 41 .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class); 42 43 http.antMatcher("/**"); 44 } 45 46 @Autowired 47 public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { 48 auth.authenticationProvider(casAuthenticationProvider); 49 } 50 51 @Bean 52 public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener(){ 53 ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> servletListenerRegistrationBean = 54 new ServletListenerRegistrationBean<>(); 55 servletListenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener()); 56 return servletListenerRegistrationBean; 57 } 58 59}
到此SpringBoot、SpringSecurity、CAS集成结束。但是这样配置有一个问题,那就是如果我们登录之前的请求是带参数的,或者跳转的不是首页,那么就会出现登录成功之后直接跳转到主页,而不是我们想要访问的页面,参数也丢失了。下面我们来解决这个问题。
4 、处理回跳地址
处理的思路是,在登录之前记住访问地址及请求参数,在登录成功之后再取到这个地址然后回跳到对应的地址。
首先我们需要写一个过滤器来获取我们的请求地址,并放到Session中。
1public class HttpParamsFilter implements Filter { 2 public String REQUESTED_URL = "CasRequestedUrl"; 3 4 @Override 5 public void init(FilterConfig filterConfig) throws ServletException { 6 7 } 8 9 @Override 10 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) 11 throws IOException, ServletException { 12 final HttpServletRequest request = (HttpServletRequest) servletRequest; 13 final HttpServletResponse response = (HttpServletResponse) servletResponse; 14 HttpSession session = request.getSession(); 15 16 String requestPath = WebUtils.getFullPath(request); 17 18 session.setAttribute(REQUESTED_URL, requestPath); 19 20 chain.doFilter(request, response); 21 } 22 23 @Override 24 public void destroy() { 25 26 } 27}
然后在CasWebSecurityConfiguration中增加对应的配置。
1@Bean 2public FilterRegistrationBean httpParamsFilter() { 3 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); 4 filterRegistrationBean.setFilter(new HttpParamsFilter()); 5 filterRegistrationBean.setOrder(-999); 6 filterRegistrationBean.addUrlPatterns("/"); 7 return filterRegistrationBean; 8}
然后扩展SimpleUrlAuthenticationSuccessHandler来实现我们的功能。
1public class MyUrlAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 2 3 public NeteaseUrlAuthenticationSuccessHandler() { 4 super(); 5 } 6 7 public NeteaseUrlAuthenticationSuccessHandler(String defaultTargetUrl) { 8 super(defaultTargetUrl); 9 } 10 11 @Override 12 protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { 13 if (isAlwaysUseDefaultTargetUrl()) { 14 return this.getDefaultTargetUrl(); 15 } 16 17 // Check for the parameter and use that if available 18 String targetUrl = null; 19 20 if (this.getTargetUrlParameter() != null) { 21 targetUrl = request.getParameter(this.getTargetUrlParameter()); 22 23 if (StringUtils.hasText(targetUrl)) { 24 logger.debug("Found targetUrlParameter in request: " + targetUrl); 25 26 return targetUrl; 27 } 28 } 29 30 if (!StringUtils.hasText(targetUrl)) { 31 HttpSession session = request.getSession(); 32 targetUrl = (String) session.getAttribute(HttpParamsFilter.REQUESTED_URL); 33 } 34 35 if (!StringUtils.hasText(targetUrl)) { 36 targetUrl = this.getDefaultTargetUrl(); 37 logger.debug("Using default Url: " + targetUrl); 38 } 39 40 return targetUrl; 41 } 42}
最后将CasAuthenticationFilter中的SimpleUrlAuthenticationSuccessHandler替换为MyUrlAuthenticationSuccessHandler就可以了。
这里需要注意一个问题,由于CAS回调是访问的/login/cas(这里是我的配置),所以过滤器一定不能拦截/login/cas否则HttpParamsFilter会将/login/cas放到Session中,就出现了无限循环。
11. 访问http://host/?id=1 -- session: /?id=1 22. CAS登录成功,然后回跳到login/cas?ticket=xxx -- session: login/cas?ticket=xxx 33. 验证票据成功NeteaseUrlAuthenticationSuccessHandler处理跳转,从session中获取跳转地址:login/cas?ticket=xxx 44. 跳转到`login/cas?ticket=xxx`然后重复步骤 2-4
主要是我们保留了请求中的参数,所以一直会有票据信息。所以就出现了无限循环。如果没有保留票据信息,就直接报错了,因为第二次访问的时候票据丢了。
由于我的是单页应用,所以我直接拦截主页就可以了。 另一种处理方法是在HttpParamsFilter判断访问地址,如果是login/cas就不更新Session中的值。
文章编写的有点仓促,如内容有误,或有纰漏欢迎各位读者指正。 原文地址http://blog.csdn.net/jaune161/article/details/78060780,转载请注明出处。