使用具有两种不同Spring Security身份验证流的CAS服务器

时间:2016-09-13 11:18:46

标签: java spring spring-security single-sign-on cas

我有一个spring-boot后端应用程序,它授权用户使用我们的JASIG-CAS服务器并将它们重定向到前端,后端现在可以从后端访问受保护的资源。现在我需要添加一个移动客户端。到目前为止,我的配置SimpleUrlAuthenticationSuccessHandler已在CasAuthenticationFilter中使用我的前端的硬编码网址,如此:

public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
    CasAuthenticationFilter authenticationFilter = new CasAuthenticationFilter();        authenticationFilter.setAuthenticationManager(authenticationManager());
    authenticationFilter.setServiceProperties(serviceProperties());
    authenticationFilter.setFilterProcessesUrl("/auth/cas");
    SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler =
        new SimpleUrlAuthenticationSuccessHandler(env.getRequiredProperty(CAS_REDIRECT_TARGET));
    authenticationFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
    return authenticationFilter;
}//CasAuthenticationFilter

但是现在我的移动客户端应该打开浏览器,显示熟悉的CAS登录页面,验证用户,重定向到后端,这将发送到移动应用程序的深层链接。问题是硬编码的重定向目标指向前端。 CAS的请求看起来是相同的,如果它是从前端或移动设备触发的,因为它们都使用浏览器,所以我无法使用我自己的AuthenticationSuccessHandler来区分它们。在一个绝望的行为中,我尝试使用相同的CAS服务器但不同的回调端点构建两个不同的身份验证流程。这是这个怪物: 包com.my.company.config;

import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.authentication.NullStatelessTicketCache;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);

    private static final String CAS_URL_SERVER = "cas.url.server";
    private static final String CAS_URL_LOGIN = "cas.url.login";
    private static final String CAS_URL_LOGOUT = "cas.url.logout";
    private static final String CAS_URL_SERVICE = "cas.url.service";
    private static final String CAS_URL_CALLBACK = "cas.url.callback";
    private static final String CAS_REDIRECT_TARGET = "cas.redirect.target";

    @Inject
    private Environment env;

    @Inject
    private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler;

    @Inject
    private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;

    @Inject
    @Qualifier("casUserDetailsService")
    private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> casAuthenticationUserDetailsService;

    @Inject
    @Qualifier("formUserDetailsService")
    private UserDetailsService userDetailsService;

    @Inject
    private Http401UnauthorizedEntryPoint authenticationEntryPoint;

    @Bean
    public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
        return new Cas20ServiceTicketValidator(env.getRequiredProperty(CAS_URL_SERVER));
    }

    @Bean(name="webAuthProvider")
    public CasAuthenticationProvider webCasAuthenticationProvider() {
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
        casAuthenticationProvider.setStatelessTicketCache(new NullStatelessTicketCache());
        casAuthenticationProvider.setKey("CAS_WEB_AUTHENTICATION_PROVIDER");
        casAuthenticationProvider.setAuthenticationUserDetailsService(casAuthenticationUserDetailsService);
        casAuthenticationProvider.setMessageSource(new SpringSecurityMessageSource());
        casAuthenticationProvider.setServiceProperties(webServiceProperties());

        return casAuthenticationProvider;
    }//CasAuthenticationProvider

    @Bean(name="mobileAuthProvider")
    public CasAuthenticationProvider mobileCasAuthenticationProvider() {
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
        casAuthenticationProvider.setStatelessTicketCache(new NullStatelessTicketCache());
        casAuthenticationProvider.setKey("CAS_MOBILE_AUTHENTICATION_PROVIDER");
        casAuthenticationProvider.setAuthenticationUserDetailsService(casAuthenticationUserDetailsService);
        casAuthenticationProvider.setMessageSource(new SpringSecurityMessageSource());
        casAuthenticationProvider.setServiceProperties(mobileServiceProperties());
        return casAuthenticationProvider;
    }//CasAuthenticationProvider

    @Bean
    public SingleSignOutFilter singleSignOutFilter() {
        SingleSignOutFilter filter = new SingleSignOutFilter();
        return filter;
    }//SingleSignOutFilter

    @Inject
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder())
            .and()
            .authenticationProvider(mobileCasAuthenticationProvider())
            .authenticationProvider(webCasAuthenticationProvider());
    }

    @Bean(name = "webCasFilter")
    public CasAuthenticationFilter webCasAuthenticationFilter() throws Exception {
        CasAuthenticationFilter authenticationFilter = new CasAuthenticationFilter();
        authenticationFilter.setBeanName("webCasFilter");
        authenticationFilter.setAuthenticationManager(authenticationManager());
        authenticationFilter.setServiceProperties(webServiceProperties());
        authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/cas"));
        //authenticationFilter.setFilterProcessesUrl("/auth/cas");
        SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler =
            new SimpleUrlAuthenticationSuccessHandler(env.getRequiredProperty(CAS_REDIRECT_TARGET));
        authenticationFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
        return authenticationFilter;
    }//CasAuthenticationFilter


    @Bean(name = "mobileCasFilter")
    public CasAuthenticationFilter mobileCasAuthenticationFilter() throws Exception {
        CasAuthenticationFilter authenticationFilter = new CasAuthenticationFilter();
        authenticationFilter.setBeanName("mobileCasFilter");
        authenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/cas/mobile"));
        authenticationFilter.setAuthenticationManager(authenticationManager());
        authenticationFilter.setServiceProperties(mobileServiceProperties());
        //authenticationFilter.setFilterProcessesUrl("/auth/cas/mobile");
        SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler =
            new SimpleUrlAuthenticationSuccessHandler("/mobile/deep-link");
        authenticationFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
        return authenticationFilter;
    }//CasAuthenticationFilter

    @Bean(name="webCasAuthenticationEntryPoint")
    public CasAuthenticationEntryPoint webCasAuthenticationEntryPoint() {
        CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
        entryPoint.setLoginUrl(env.getRequiredProperty(CAS_URL_LOGIN));
        entryPoint.setServiceProperties(webServiceProperties());
        return entryPoint;
    }//CasAuthenticationEntryPoint

    @Bean(name="mobileCasAuthenticationEntryPoint")
    public CasAuthenticationEntryPoint mobileCasAuthenticationEntryPoint() {
        CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
        entryPoint.setLoginUrl(env.getRequiredProperty(CAS_URL_LOGIN));
        entryPoint.setServiceProperties(mobileServiceProperties());
        return entryPoint;
    }//CasAuthenticationEntryPoint

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
            .ignoring()
            .antMatchers("/scripts/**/*.{js,html}")
            .antMatchers("/bower_components/**")
            .antMatchers("/i18n/**")
            .antMatchers("/assets/**")
            .antMatchers("/swagger-ui/**")
            .antMatchers("/test/**")
            .antMatchers("/console/**");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    private class CasRedirectionFilter implements Filter {

        public void init(FilterConfig fConfig) throws ServletException {
        }

        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
            HttpServletResponse res = (HttpServletResponse) response;
            //CasAuthenticationEntryPoint caep = casAuthenticationEntryPoint();
            res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
            HttpServletRequest req = (HttpServletRequest) request;;
            String contextPath = req.getRequestURI();
            if(contextPath.equals("/api/login/mobile")){
                String redirectUrl = "https://cas.server.com/cas/login?service=http://localhost:8080/auth/cas/mobile";
                res.setHeader("Location", redirectUrl);
            }else {
                String redirectUrl = "https://cas.server.com/cas/login?service=http://localhost:8080/auth/cas";                
                res.setHeader("Location", redirectUrl);
            }
        }

        public void destroy() {
        }
    }

    @Bean
    public FilterChainProxy loginFilter() throws Exception {
        List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>();
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/api/login/cas"), new CasRedirectionFilter()));
        chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/api/login/mobile"), new CasRedirectionFilter()));
        log.debug("loginFilter {}", chains);
        return new FilterChainProxy(chains);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
            .and()
            .csrf()
            .disable()
            .addFilterBefore(mobileCasAuthenticationFilter(),CasAuthenticationFilter.class)
            .addFilterBefore(webCasAuthenticationFilter(),CasAuthenticationFilter.class)
            .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
            .addFilter(loginFilter())
            .exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint)
            .and()
            .logout()
            .logoutUrl("/api/logout")
            .clearAuthentication(true)
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
            .logoutSuccessUrl(env.getRequiredProperty(CAS_URL_LOGOUT))
            .permitAll()
            .and()
            .headers()
            .frameOptions()
            .disable()
            .and()
            .formLogin()
            //.defaultSuccessUrl(env.getRequiredProperty(CAS_REDIRECT_TARGET), true)
            .successHandler(ajaxAuthenticationSuccessHandler)
            .failureHandler(ajaxAuthenticationFailureHandler)
            .loginProcessingUrl("/api/authentication")
            .usernameParameter("j_username")
            .passwordParameter("j_password")
            .permitAll()
            .and()
            .authorizeRequests()
            .antMatchers(org.springframework.http.HttpMethod.OPTIONS, "/api/**").permitAll()
            .antMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
            .antMatchers("/app/**").authenticated()
            .antMatchers(HttpMethod.GET, "/api/login").authenticated()
            .antMatchers("/api/login/mobile").authenticated()
            .antMatchers("/api/login/cas").authenticated()
            .antMatchers("/api/register").permitAll()
            .antMatchers("/api/activate").permitAll()
            .antMatchers("/api/authenticate").authenticated()
            .antMatchers("/api/logs/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/api/**").authenticated()
            .antMatchers("/metrics/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/mobile/**").permitAll()
            .antMatchers("/health/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/dump/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/shutdown/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/beans/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/configprops/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/info/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/autoconfig/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/env/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/api-docs/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/protected/**").authenticated();

    }

    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
    private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
        @Inject
        ConferenceRepository conferenceRepository;
        @Inject
        UserRepository userRepository;

        public GlobalSecurityConfiguration() {
            super();
        }

        @Override
        protected MethodSecurityExpressionHandler createExpressionHandler() {
            PermissionChecker permissionEvaluator = new PermissionChecker(conferenceRepository, userRepository);

            DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
            expressionHandler.setPermissionEvaluator(permissionEvaluator);
            return expressionHandler;
        }
    }

    @Bean(name="webServiceProperties")
    public ServiceProperties webServiceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService("http://localhost:8080/auth/cas");
        serviceProperties.setSendRenew(true);
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }//serviceProperties

    @Bean(name="mobileServiceProperties")
    public ServiceProperties mobileServiceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService("http://localhost:8080/auth/cas/mobile");
        serviceProperties.setSendRenew(true);
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }//serviceProperties
}

这在某种程度上有效。当发布移动身份验证流程时,它会按预期运行,但是当前端问题/api/login/cas时,首先使用针对TicketGrantingTicket的移动过滤器检查来自CAS的service=/auth/cas/mobile,但是针对service=/auth/cas发出了TGT casWebAuthenticationFilter以及后续使用CasAuthenticationFilter进行验证时使用的是无效票证。

所以现在我没有想法如何强制AuthenticationProvider只处理某些票?也许我对自己的想法如此纠结,以至于我看不到更简单的解决方案?也许我应该做两个单独的http安全配置?

编辑: 似乎这一切归结为我放置@Inject public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()) .and() .authenticationProvider(webCasAuthenticationProvider()) .authenticationProvider(mobileCasAuthenticationProvider()); } 的顺序:

mobileAuthenticationProider()

var car1 = context.Cars.FirstOrDefault(c => c.Make == " BMW "); if(car1 == null) { context.Cars.Add(new Car { Id = 1, Make = "BMW", Model = "750i" }); } 首先出现时,移动登录工作正常,而当我切换调用它们的顺序时,网络不会,那么移动验证失败并且网络开始工作。

1 个答案:

答案 0 :(得分:0)

好的,所以我开始工作,它看起来不是最好,最强大的解决方案,所以它可能需要更多的调查和关注。不过在这里它是:

 @Bean
    public AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource() {

        return new AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails>() {

            @Override
            public WebAuthenticationDetails buildDetails(
                HttpServletRequest request) {
                return new CustomAuthenticationDetails(request);
            }

        };
    }

    @Bean
    public AuthenticationProvider customAuthenticationProvider() {
        return new AuthenticationProvider() {
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                String serviceUrl;
                serviceUrl = ((CustomAuthenticationDetails) authentication.getDetails()).getURI();
                if (serviceUrl.equals(env.getRequiredProperty(CAS_URL_CALLBACK_MOBILE))) {
                    return mobileCasAuthenticationProvider().authenticate(authentication);
                } else {
                    return webCasAuthenticationProvider().authenticate(authentication);
                }

            }

            public boolean supports(final Class<?> authentication) {
                return (UsernamePasswordAuthenticationToken.class
                    .isAssignableFrom(authentication))
                    || (CasAuthenticationToken.class.isAssignableFrom(authentication))
                    || (CasAssertionAuthenticationToken.class
                    .isAssignableFrom(authentication));
            }
        };
    }

我添加了我的自定义AuthenticationProvider,它区分了我真正需要的两个。它使用另一个自定义类,即CustomAuthenticationDetails,它存储有关请求来自何处的信息。

public class CustomAuthenticationDetails extends WebAuthenticationDetails {
    private final Logger log = LoggerFactory.getLogger(CustomAuthenticationDetails.class);
    private final String URI;
    private final String sessionId;
    public CustomAuthenticationDetails(HttpServletRequest request) {
        super(request);
        this.URI = request.getRequestURI();
        HttpSession session = request.getSession(false);
        this.sessionId = (session != null) ? session.getId() : null;
    }

    public String getURI() {
        return URI;
    }

    public String getSessionId() {
        return sessionId;
    }
}

所有这些都是使用AuthenticationFilterauthenticationFilter.setAuthenticationDetailsSource(authenticationDetailsSource());中连接在一起的。希望它可以帮助某人解决未来的问题或至少引导正确的方向。