java 自定义登录表单。配置 Spring 安全以获取 JSON 响应

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/32498868/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-02 20:17:40  来源:igfitidea点击:

Custom login form. Configure Spring security to get a JSON response

javaspring-securityspring-boot

提问by Arnaud Denoyelle

I have a simple application which is splitted into 2 parts :

我有一个简单的应用程序,它分为两部分:

  • A backend which exposes REST services with Spring-boot / Spring-security
  • A frontend which contains only static files.
  • 使用 Spring-boot / Spring-security 公开 REST 服务的后端
  • 仅包含静态文件的前端。

The requests are received by a nginx server which listens on port 80.

请求由侦听端口 80 的 nginx 服务器接收。

  • If the request URL begins with /api/, the request is redirected to the backend.
  • Else, the request is handled by nginx which serves the static files.
  • 如果请求 URL 以 /api/ 开头,则请求将重定向到后端。
  • 否则,请求由提供静态文件的 nginx 处理。

I created a custom login form (in the frontend part) and I am trying to configure the Spring-boot server.

我创建了一个自定义登录表单(在前端部分)并且我正在尝试配置 Spring-boot 服务器。

There are a lot of examples where I can see how to define a "login success" url and a "login error" url but I do not want Spring-security to redirect the user. I want Spring-security to answer with HTTP 200 if the login succeeded or HTTP 40x is the login failed.

有很多示例,我可以看到如何定义“登录成功”url 和“登录错误”url,但我不希望 Spring-security 重定向用户。如果登录成功或 HTTP 40x 是登录失败,我希望 Spring-security 用 HTTP 200 回答。

In other words : I want the backend to only answer with JSON, never HTML.

换句话说:我希望后端只回答 JSON,而不是 HTML。

Up to now, when I submit the login form, the request is redirected and I get the default Spring login form as an answer.

到目前为止,当我提交登录表单时,请求被重定向,我得到默认的 Spring 登录表单作为答案。

I tried to use .formLogin().loginProcessingUrl("/login");instead of loginPage(""):

我尝试使用.formLogin().loginProcessingUrl("/login");而不是loginPage("")

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
      .withUser("user").password("password").roles("ADMIN");
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .anyRequest().authenticated()
        .and()
      .formLogin()
        .loginProcessingUrl("/login");

回答by Arnaud Denoyelle

Thanks to M. Deinum and thanks to this guide, I could find the solution.

感谢 M. Deinum 和本指南,我可以找到解决方案。

First, I had a configuration problem with the login form itself. As the backend has a context-path set to /api, the custom form should have submitted the form params to /api/loginbut I was actually submitting the data to /api/login/(Notice the extra /at the end).

首先,我的登录表单本身存在配置问题。由于后端的上下文路径设置为/api,因此自定义表单应该将表单参数提交给,/api/login但我实际上是将数据提交给/api/login/(注意最后的额外内容/)。

As a result, I was unknowingly trying to access a protected resource! Hence, the request was handled by the default AuthenticationEntryPointwhich default behavior is to redirect the user to the login page.

结果,我在不知不觉中尝试访问受保护的资源!因此,请求由默认处理AuthenticationEntryPoint,默认行为是将用户重定向到登录页面。

As a solution, I implemented a custom AuthenticationEntryPoint :

作为解决方案,我实现了一个自定义 AuthenticationEntryPoint :

private AuthenticationEntryPoint authenticationEntryPoint() {
  return new AuthenticationEntryPoint() {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
      httpServletResponse.getWriter().append("Not authenticated");
      httpServletResponse.setStatus(401);
    }
  };
}

Then used it in the configuration :

然后在配置中使用它:

http
  .exceptionHandling()
  .authenticationEntryPoint(authenticationEntryPoint())

and I did the same for the other handlers :

我对其他处理程序做了同样的事情:

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user").password("password").roles("ADMIN");
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
          .anyRequest().authenticated()
        .and()
          .formLogin()
          .successHandler(successHandler())
          .failureHandler(failureHandler())
        .and()
          .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
            .authenticationEntryPoint(authenticationEntryPoint())
        .and()
          .csrf().csrfTokenRepository(csrfTokenRepository()).and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)
    ;
  }

  private AuthenticationSuccessHandler successHandler() {
    return new AuthenticationSuccessHandler() {
      @Override
      public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.getWriter().append("OK");
        httpServletResponse.setStatus(200);
      }
    };
  }

  private AuthenticationFailureHandler failureHandler() {
    return new AuthenticationFailureHandler() {
      @Override
      public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.getWriter().append("Authentication failure");
        httpServletResponse.setStatus(401);
      }
    };
  }

  private AccessDeniedHandler accessDeniedHandler() {
    return new AccessDeniedHandler() {
      @Override
      public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.getWriter().append("Access denied");
        httpServletResponse.setStatus(403);
      }
    };
  }

  private AuthenticationEntryPoint authenticationEntryPoint() {
    return new AuthenticationEntryPoint() {
      @Override
      public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.getWriter().append("Not authenticated");
        httpServletResponse.setStatus(401);
      }
    };
  }

  private Filter csrfHeaderFilter() {
    return new OncePerRequestFilter() {
      @Override
      protected void doFilterInternal(HttpServletRequest request,
                                      HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {
        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
            .getName());
        if (csrf != null) {
          Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
          String token = csrf.getToken();
          if (cookie == null || token != null
              && !token.equals(cookie.getValue())) {
            cookie = new Cookie("XSRF-TOKEN", token);
            cookie.setPath("/");
            response.addCookie(cookie);
          }
        }
        filterChain.doFilter(request, response);
      }
    };
  }

  private CsrfTokenRepository csrfTokenRepository() {
    HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
    repository.setHeaderName("X-XSRF-TOKEN");
    return repository;
  }
}

回答by Eugene Maysyuk

Here's configuration for Spring Boot 2.2.5.RELEASE:

这是 Spring Boot 2.2.5.RELEASE 的配置:

package com.may.config.security;

import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password(passwordEncoder().encode("user")).roles("USER").and()
            .withUser("admin").password(passwordEncoder().encode("admin")).roles("USER", "ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .requestCache().disable() // do not preserve original request before redirecting to login page as we will return status code instead of redirect to login page (this is important to disable otherwise session will be created on every request (not containing sessionId/authToken) to non existing endpoint aka curl -i -X GET 'http://localhost:8080/unknown')
            .authorizeRequests()
                .antMatchers("/health", "/swagger-ui.html/**", "/swagger-resources/**", "/webjars/springfox-swagger-ui/**", "/v2/api-docs").permitAll()
                .anyRequest().hasRole("USER").and()
            .exceptionHandling()
                .accessDeniedHandler((req, resp, ex) -> resp.setStatus(SC_FORBIDDEN)) // if someone tries to access protected resource but doesn't have enough permissions
                .authenticationEntryPoint((req, resp, ex) -> resp.setStatus(SC_UNAUTHORIZED)).and() // if someone tries to access protected resource without being authenticated (LoginUrlAuthenticationEntryPoint used by default)
            .formLogin()
                .loginProcessingUrl("/login") // authentication url
                .successHandler((req, resp, auth) -> resp.setStatus(SC_OK)) // success authentication
                .failureHandler((req, resp, ex) -> resp.setStatus(SC_FORBIDDEN)).and() // bad credentials
            .sessionManagement()
                .invalidSessionStrategy((req, resp) -> resp.setStatus(SC_UNAUTHORIZED)).and() // if user provided expired session id
            .logout()
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); // return status code on logout
    }

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

Important aspects here:

这里的重要方面:

http.requestCache().disable()

important to disable otherwise new session will be created on every request to non existing endpoint (e.g. curl -i -X GET 'http://localhost:8080/unknown')

at least this is how it works with spring-session configured in the project

if not overridden - ExceptionTranslationFilter will use requestCache to preserve original URL to session(that creates session if non is exist) while handling AccessDeniedException.

重要的是禁用否则将在对非现有端点的每个请求中创建新会话(例如 curl -i -X GET ' http://localhost:8080/unknown')

至少这是它如何与项目中配置的spring-session一起工作

如果没有被覆盖 - ExceptionTranslationFilter 将在处理 AccessDeniedException时使用 requestCache 来保留会话的原始 URL(如果不存在则创建会话)。

http.sessionManagement().invalidSessionStrategy((req, resp) -> resp.setStatus(SC_UNAUTHORIZED))

return 401 status code in case user supplied expired sessionId in request

if not overridden - fallbacks to authenticationEntryPoint

can be helpful to provide meaningful message in response (aka "Your session has expired")

如果用户在请求中提供了过期的 sessionId,则返回 401 状态代码

如果没有被覆盖 - 回退到 authenticationEntryPoint

有助于提供有意义的响应消息(又名“您的会话已过期”)

http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());

return 200 status code on logout

if not overridden - redirects web client to login page

退出时返回 200 状态码

如果没有被覆盖 - 将 Web 客户端重定向到登录页面