首页 > 解决方案 > Spring 具有两种安全配置 - 失败的 API 登录重定向到表单登录页面。如何改变?

问题描述

我有一个带有两种安全配置(两个WebSecurityConfigurerAdapter)的 Spring Boot 应用程序,一种用于具有“ /api/**”端点的 REST API,另一种用于所有其他端点的 Web 前端。安全配置在 Github 上,这里有一些相关部分:

@Configuration
@Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
        jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
        jwtAuthenticationFilter.setPostOnly(true);

        http.antMatcher("/api/**")
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf().disable()
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .addFilter(jwtAuthenticationFilter)
                .addFilter(new JWTAuthorizationFilter(authenticationManager()));
    }
}

@Configuration
public static class FrontEndSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage("/login").permitAll()
                .defaultSuccessUrl("/")
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/?logout")
                .and()
                .authorizeRequests()
                .mvcMatchers("/").permitAll()
                .mvcMatchers("/home").authenticated()
                .anyRequest().denyAll()
                .and();
    }
}

JWTAuthenticationFilter是一个自定义子类,它处理对 REST API的UsernamePasswordAuthenticationFilter登录尝试(通过带有 JSON 正文的 HTTP POST /api/login),如果成功,则在“授权”标头中返回 JWT 令牌。

所以这是问题所在:失败的登录尝试/api/login(使用错误的凭据或缺少 JSON 正文)正在重定向到 HTML 登录表单/api/login。对其他“”端点的未经身份验证的请求会/api/**产生一个简单的 JSON 响应,例如:

{
    "timestamp": "2019-11-22T21:03:07.892+0000",
    "status": 403,
    "error": "Forbidden",
    "message": "Access Denied",
    "path": "/api/v1/agency"
}

{
    "timestamp": "2019-11-22T21:04:46.663+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/api/v1/badlink"
}

未经身份验证的用户尝试访问其他受保护的 URL(不以“ /api/”开头)重定向到登录表单/login,这是所需的行为。但我不希望 API 调用/api/login重定向到该表单!

如何为失败的 API 登录编写正确的行为? 这是为该过滤器添加新处理程序的问题吗?或者可能为我已经定义的某些行为添加排除项?

有关异常和处理的更多详细信息:

我查看了日志,针对错误凭据或格式错误的 JSON 引发的异常是org.springframework.security.authentication.AuthenticationException. 日志显示,例如:

webapp_1  | 2019-11-25 15:30:16.048 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter   : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
(...stack trace...)
webapp_1  | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter   : Updated SecurityContextHolder to contain null Authentication
webapp_1  | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter   : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@7f9648b6
webapp_1  | 2019-11-25 15:30:16.133 DEBUG 1 --- [nio-8080-exec-6] n.j.webapps.granite.home.HomeController  : Accessing /login page.

当我访问另一个 URL 时,例如一个不存在的 URL,例如/api/x,它看起来非常不同。它非常冗长,但看​​起来服务器正在尝试重定向/error并且没有发现它是授权 URL。有趣的是,如果我在 Web 浏览器中尝试此操作,我会使用我的自定义错误页面 (error.html) 格式化错误,但如果我使用 Postman 访问它,我只会收到一条 JSON 消息。日志示例:

webapp_1  | 2019-11-25 16:07:22.157 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter     : Access is denied (user is anonymous); redirecting to authentication entry point
webapp_1  |
webapp_1  | org.springframework.security.access.AccessDeniedException: Access is denied
...
webapp_1  | 2019-11-25 16:07:22.174 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter     : Calling Authentication entry point.
webapp_1  | 2019-11-25 16:07:22.175 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.Http403ForbiddenEntryPoint     : Pre-authenticated entry point called. Rejecting access
...
webapp_1  | 2019-11-25 16:07:22.211 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/error'; against '/api/**'
...
webapp_1  | 2019-11-25 16:07:22.214 DEBUG 1 --- [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : /error reached end of additional filter chain; proceeding with original chain
webapp_1  | 2019-11-25 16:07:22.226 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
webapp_1  | 2019-11-25 16:07:22.230 DEBUG 1 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
webapp_1  | 2019-11-25 16:07:22.564 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
webapp_1  | 2019-11-25 16:07:22.577 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Mon Nov 25 16:07:22 GMT 2019, status=403, error=Forbidden, message=Access Denied, path=/a (truncated)...]
webapp_1  | 2019-11-25 16:07:22.903 DEBUG 1 --- [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
webapp_1  | 2019-11-25 16:07:22.905 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 403

所以看起来我可能需要做的是为 REST API 配置“身份验证失败处理程序”以转到“/error”而不是“/login”,但仅适用于/api/**.

标签: spring-bootspring-securityrest-security

解决方案


unsuccessfulAuthentication()我在身份验证过滤器中添加了一个 @Override

祖父类 ( AbstractAuthenticationProcessingFilter) 有一个用于不成功认证的方法 (ie AuthenticationException),它委托给认证失败处理程序类。我本可以创建自己的自定义身份验证失败处理程序,但决定简单地unsuccessfulAuthentication使用一些代码覆盖该方法,该代码发送回具有 401 状态和 JSON 错误消息的响应:

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    // TODO: enrich/improve error messages
    response.setStatus(response.SC_UNAUTHORIZED);
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
    response.getWriter().write("{\"error\": \"authentication error?\"}");
}

...并添加了一个自定义AuthenticationEntryPoint以使其他错误匹配

这没有我在其他端点看到的错误消息的确切形式,所以我还创建了一个自定义AuthenticationEntryPoint(处理对受保护端点的未经授权请求的类),它基本上做同样的事情。我的实现:

public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        // TODO: enrich/improve error messages
        response.setStatus(response.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        response.getWriter().write("{\"error\": \"unauthorized?\"}");
    }
}

现在 REST 端点的安全配置如下所示(注意添加了“ .exceptionHandling().authenticationEntryPoint()”:

@Configuration
@Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
        jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");

        http.antMatcher("/api/**")
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf().disable()
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .exceptionHandling()
                    .authenticationEntryPoint(new RESTAuthenticationEntryPoint())
                    .and()
                .addFilter(jwtAuthenticationFilter)
                .addFilter(new JWTAuthorizationFilter(authenticationManager()));
    }
}

我需要处理我的错误消息,以便它们提供更多信息和安全。这并不能完全回答我最初的问题,即如何获得我喜欢的那些默认样式的 JSON 响应,但它确实允许我自定义来自所有类型的访问失败的错误,而与 Web 配置无关。(外部的端点/api/**仍然可以使用 Web 表单登录。)

截至当前提交的完整代码:github


推荐阅读