Spring 4 Security + CORS + AngularJS - 奇怪的重定向

Posted

技术标签:

【中文标题】Spring 4 Security + CORS + AngularJS - 奇怪的重定向【英文标题】:Spring 4 Security + CORS + AngularJS - Weird redirect 【发布时间】:2019-07-06 23:17:26 【问题描述】:

我的 Spring 后端和 AngularJS 前端出现问题。作为一个信息,我对 Spring Security 很陌生,并且也在学习这个项目。

我没有使用 SpringBoot。两者都单独工作,并且应该能够在单独的机器上运行。 ATM 我的前端通过 https://localhost:3000 上的 gulp 服务器在本地运行,后端在 https://localhost:8443/context 上的 Tomcat 中运行。我在 Java 中设置了CORSFilter

到目前为止,一切都很好。如果我启动前端,则会对后端进行调用以获取资源,并且我正在查看登录页面。如果我选择登录,则会按预期拨打https://localhost:8443/context/login 的电话。 但是:在后台处理登录后,后台重定向到https://localhost:8443/context而不是https://localhost:3000,当然会创建一个 404 并导致登录失败(前端)。我只是找不到这个奇怪的重定向是在哪里进行的。

SpringSecurityConfig

private static final String C440_LOGIN = "/login";
private static final String c440_START_PAGE = "/index.html";
private static final String FAVICON_ICO = "/favicon.ico";

@Override
protected void configure(HttpSecurity http) throws Exception 
    // HttpSecurity workHttp = http.addFilterBefore(new CORSFilter(), SessionManagementFilter.class); does not work!
    HttpSecurity workHttp = http.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class);
    workHttp.addFilterBefore(new CookieFilter(), ChannelProcessingFilter.class);
    workHttp.addFilterBefore(getUsernamePasswordPortalAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

    // set authorizations
    workHttp = authorizeRequests(http);
    // login handling
    workHttp = formLogin(workHttp);
    // exception handling
    workHttp = exceptionHandling(workHttp);
    // logout handling
    workHttp = logout(workHttp);
    // cookie handling
    workHttp = rememberMe(workHttp);

    // disable caching because if IE11 webfonds bug
    // https://***.com/questions/7748140/font-face-eot-not-loading-over-https
    http.headers().cacheControl().disable();

    csrf(workHttp);


/**
 * Configures request authorization.
 *
 * @param http The security configuration.
 * @return The configured security configuration.
 * @throws Exception is throws if the configuration fails.
 */
protected HttpSecurity authorizeRequests(HttpSecurity http) throws Exception 
    return http
        .authorizeRequests()

        // secured pages
        .antMatchers("/", getCustomerdWebRessourceSecuredPath()).authenticated()

        // common resources
        .antMatchers("/app/**").permitAll()
        .antMatchers("/profiles/**").permitAll()
        .antMatchers("/captcha/**").permitAll()

        .antMatchers("/", getCustomerRessourcePath()).permitAll()
        .antMatchers("/", getCustomerWebRessourcePath()).permitAll()

        .antMatchers("/", c440_START_PAGE).permitAll()
        .antMatchers("/", FAVICON_ICO).permitAll()
        .antMatchers(C440_LOGIN).permitAll()

        // frontend services
        .antMatchers("/services/userService/**").permitAll()
        .antMatchers("/services/applicationService/**").permitAll()
        .antMatchers("/services/textContentService/**").permitAll()
        .antMatchers("/services/textContentBlockService/**").permitAll()
        .antMatchers("/services/menuItemService/**").permitAll()
        .antMatchers("/services/calculatorService/**").permitAll()

        .anyRequest().authenticated()
        .and();


private String getCustomerRessourcePath() 
    return "/resources/app-" + portalFrontendBase + "/**";


private String getCustomerWebRessourcePath() 
    return "/app-" + portalFrontendBase + "/**";


private String getCustomerdWebRessourceSecuredPath() 
    return "/app-" + portalFrontendBase + "/secure/**";


/**
 * Configures form login.
 *
 * @param http The security configuration.
 * @return The configured security configuration.
 * @throws Exception is throws if the configuration fails.
 */
protected HttpSecurity exceptionHandling(HttpSecurity http) throws Exception 
    return http
        .exceptionHandling()
        .authenticationEntryPoint((request, response, authException) -> 
            if (authException != null) 
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                /**
                 * IMPORTANT: do not redirect the requests. The front-end will be responsible to do this.
                 * Otherwise the unauthorized status cannot be caught in the front-end correctly.
                 */
                return;
            
        )
        .and();


/**
 * Configures form login.
 *
 * @param http The security configuration.
 * @return The configured security configuration.
 * @throws Exception is throws if the configuration fails.
 */
protected HttpSecurity formLogin(HttpSecurity http) throws Exception 
    return http
        .formLogin()
            .loginPage(c440_START_PAGE)
            .successHandler(getAuthenticationSuccessHandler())
            .failureHandler(getAuthenticationFailureHandler())
            .loginProcessingUrl(C440_LOGIN)
            .permitAll()
            .and();


/**
 * Configures logout.
 *
 * @param http The security configuration.
 * @return The configured security configuration.
 * @throws Exception is throws if the configuration fails.
 */
protected HttpSecurity logout(HttpSecurity http) throws Exception 
    return http
        .logout()
            .logoutUrl(portalLogoutURL)
            .addLogoutHandler(getLogoutHandler())
            .logoutSuccessHandler(getLogoutSuccessHandler())
            .invalidateHttpSession(true)
            .and();


@Bean
public UsernamePasswordPortalAuthenticationFilter getUsernamePasswordPortalAuthenticationFilter() throws Exception 
    UsernamePasswordPortalAuthenticationFilter customFilter = new UsernamePasswordPortalAuthenticationFilter();
    customFilter.setAuthenticationManager(authenticationManagerBean());
    return customFilter;

UsernamePasswordPortalAuthenticationFilter.java

    @PropertySource(value = "classpath:application.properties")
    public class UsernamePasswordPortalAuthenticationFilter extends 
    UsernamePasswordAuthenticationFilter 
    private Logger log = Logger.getLogger(this.getClass());

    @Value("$captchaActive")
    private boolean captchaActive;

    @Override
    public AuthenticationManager getAuthenticationManager() 
        return super.getAuthenticationManager();
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 
        UsernamePasswordPortalAuthenticationToken authRequest = getAuthenticationTokenFromRequest(request);
        return getAuthenticationManager().authenticate(authRequest);
    

    /**
     * Reads the UsernamePasswordPortalAuthenticationToken from the data of the request.
     *
     * @param request The request to read the data from.
     * @return The authentication token.
     * @throws AuthenticationException is thrown if the data cannot be read.
     */
    public UsernamePasswordPortalAuthenticationToken getAuthenticationTokenFromRequest(final HttpServletRequest request) throws AuthenticationException 
        StringBuffer buf = new StringBuffer();
        String line = null;

        try 
            BufferedReader reader = request.getReader();
            while ((line = reader.readLine()) != null) 
                buf.append(line);
            

            UsernamePasswordPortalAuthenticationToken loginDataWithCaptcha =
                new ObjectMapper().readValue(buf.toString(), UsernamePasswordPortalAuthenticationToken.class);

            if (this.captchaActive) 
                String answer = (String) request.getSession().getAttribute("COLLPHIRCAPTCHA");

                List<CaptchaCookieDto> captchaCookieDtos;
                captchaCookieDtos = (List<CaptchaCookieDto>) request.getAttribute("captchaCookies");

                CaptchaCookieDto captchaCookieDto = captchaCookieDtos.stream().filter(captchaCookie -> captchaCookie.getUsername().equals(
                    loginDataWithCaptcha.getUsername())).findAny().orElse(null);

                if (captchaCookieDto != null && captchaCookieDto.getCounter() >= 2) 
                    if (answer.equals(loginDataWithCaptcha.getConfirmCaptcha())) 
                        return new ObjectMapper().readValue(loginDataWithCaptcha.loginDataToStringWithoutCaptcha(),
                            UsernamePasswordPortalAuthenticationToken.class);
                     else 
                        throw new BadCredentialsException("invalid data");
                    
                 else 
                    return new ObjectMapper().readValue(loginDataWithCaptcha.loginDataToStringWithoutCaptcha(),
                        UsernamePasswordPortalAuthenticationToken.class);
                
             else 
                return new ObjectMapper().readValue(loginDataWithCaptcha.loginDataToStringWithoutCaptcha(), UsernamePasswordPortalAuthenticationToken.class);
            


         catch (Exception e) 
            throw new BadCredentialsException("invalid data");
        

    


我尝试更改我的两个自定义过滤器(CORSFilterCookieFilter)的顺序,或者将 CORSFilter 放在其他位置(addFilterBefore SessionManagementFilter 不起作用,如果我这样做,登录 -由于缺少 CORS 标头,调用将无法正常工作)以及几乎所有其他内容......

我还尝试使用来自https://www.baeldung.com/spring_redirect_after_login 的authsuccesshandler 的想法,在该想法中,我只得到请求origin 标头(应该是前端URL https://localhost:3000)来重定向回它:

@Component
public class MyTestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler 

    private Logger LOG = Logger.getLogger(this.getClass());
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    public MyTestAuthenticationSuccessHandler() 
        super();
        setUseReferer(true);
    

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException 
        LOG.info("onAuthenticationSuccess");
        SecurityContextHolder.getContext().setAuthentication(auth);

        handle(request, response, auth);
    

    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException 

        String targetUrl = determineTargetUrl(request);

        if (response.isCommitted()) 
            LOG.info("Response has already been committed. Unable to redirect to " + targetUrl);
            return;
        

        redirectStrategy.sendRedirect(request, response, targetUrl);
    

    protected String determineTargetUrl(HttpServletRequest request) 
        return request.getHeader("Origin");
    

但它仍然不起作用。

另外,如果我尝试调试后端并在authsuccesshandlerauthfailurehandler 内设置断点,它仍然不会停在那里。不应该就此打住吗?

.formLogin()
        .loginPage(c440_START_PAGE)
        .successHandler(getAuthenticationSuccessHandler())
        .failureHandler(getAuthenticationFailureHandler())
        .loginProcessingUrl(C440_LOGIN)
        .permitAll()
        .and();

我真的不明白这个重定向发生在哪里以及为什么它不会使用我的新authsuccesshandler

更新 07.03.19: 似乎根本没有调用 successhandler,即使我将前端和后端都部署在与一个捆绑的 WAR 文件,它使登录再次工作。奇怪的是,即使我从 SecurityConfig 内的配置方法中删除了 .formLogin() 东西,登录仍然有效。所以我想看起来所有的魔法都发生在我们自定义的 UsernamePasswordAuthenticationFilter 中调用的 AuthenticationProvider 中:

UsernamePasswordAuthenticationFilter

[...]
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 
    UsernamePasswordPortalAuthenticationToken authRequest = getAuthenticationTokenFromRequest(request);
    return getAuthenticationManager().authenticate(authRequest);

[...]

AuthenticationProvider:

[...]
@Override
public CollphirAuthentication authenticate(Authentication authentication) throws AuthenticationException 

    if (authentication == null) 
        throw new IllegalArgumentException("authentication");
    

    if (UsernamePasswordPortalAuthenticationToken.class.isAssignableFrom(authentication.getClass())) 
        UsernamePasswordPortalAuthenticationToken clientAuthentication = (UsernamePasswordPortalAuthenticationToken) authentication;

        CollphirUser user = getUserService().loginUser(
                clientAuthentication.getName(), clientAuthentication.getCredentials().toString(), clientAuthentication.getPortal(), clientAuthentication.getArbeitgeber());
        CollphirAuthentication auth = null;

        if (user == null || user.getBenutzerkennung() == null || user.getCOLRolle() == null) 
            LOG.info("authentication failed");
            Notification[] notifications = user.getNotifications();
            String msg = null;

            if (notifications != null && notifications[0] != null && notifications[0].getText() != null) 
                msg = notifications[0].getText();
            

            throw new BadCredentialsException(msg);
        
        Referenz arbeitgeberReference = getArbeitgeberReference(user, clientAuthentication.getPortal(), clientAuthentication.getArbeitgeber());

        auth = new CollphirAuthentication(user, arbeitgeberReference);
        auth.setArbeitgeber(getArbeitgeber( arbeitgeberReference));

        LOG.debug("is authenticated: " + auth.isAuthenticated());

        return auth;
    

    throw new BadCredentialsException("type");

[...]

所以我的猜测是:在 UsernamePasswordPortalAuthenticationFilterAuthenticationProvider 内的某个地方进行了重定向。如果我想一想,在通过 REST 调用后端的 AngularJS 前端中,重定向根本没有意义,对吧?后端不应该只发回状态代码或AngularJS控制器可以评估以更改状态或显示错误消息的东西吗?

看起来这个应用程序中的整个登录过程真的很奇怪。我无法想象通常不使用 .formLogin().successHandler()?问题是,我没有 AngularJS 前端和 Spring Security 后端的最佳实践示例作为比较...

【问题讨论】:

在调用 localhost:8443/context/login(返回 302)之后,只有一个对 localhost:8443/context 的 GET-Request 而已...... 浏览器可能有问题?检查这是否有帮助***.com/a/43563899/5383945 @Jazib 不,不是这样,但是谢谢! @Vortilion 你使用 Spring Boot 吗?您是否检查过您的配置是否已执行? @Vortilion:你的问题真的很难阅读。你能把你的问题清理干净吗?如果我正确理解您的问题,则您已成功登录,但您的重定向错误。如果这是正确的,那么 CORS 配置可以正常工作,请将其从您的问题中删除。您的 AuthenticationProvider 也可以正常工作,请将其从您的问题中删除。 【参考方案1】:

你使用 Springs 默认的 AuthenticationSuccessHandler 吗?我认为这个处理程序只是重定向到应用程序基本路径,如果您的前端位于另一个上下文或 URL,它就不起作用。因此,您必须在成功登录回前端后执行重定向。

看看这个页面:https://www.baeldung.com/spring-security-redirect-login

您可以在这里找到处理这种情况的几种可能性。

【讨论】:

谢谢。我看了看它并尝试编辑我的 AuthenticationSuccessHandler,但它没有改变任何东西......奇怪的是,如果我尝试登录,它不会在这个 SuccessHandler 中的断点处停止......

以上是关于Spring 4 Security + CORS + AngularJS - 奇怪的重定向的主要内容,如果未能解决你的问题,请参考以下文章

针对授权标头的Spring Security OAuth2 CORS问题

授权标头的 Spring Security OAuth2 CORS 问题

Angular 1.4、POST、Spring Security、CORS

Spring security 4.2.3,OAUTH 2,/oauth/token 端点,CORS 不工作

Spring Security CORS 过滤器不起作用

[CORS][Spring Security] PreFlight 请求未处理