【问题标题】:Spring security OAuth2 accept JSONSpring security OAuth2 接受 JSON
【发布时间】:2016-11-05 00:42:56
【问题描述】:

我从 Spring OAuth2 开始。我想将用户名和密码以 application/json 格式的 POST 正文发送到 /oauth/token 端点。

curl -X POST -H "Authorization: Basic YWNtZTphY21lc2VjcmV0" -H "Content-Type: application/json" -d '{
"username": "user",
"password": "password",
"grant_type": "password"
}' "http://localhost:9999/api/oauth/token"

这可能吗?

你能给我一个建议吗?

【问题讨论】:

  • 为什么要在请求中使用 JSON HTTP 内容类型? OAuth 规范 (tools.ietf.org/html/rfc6749) 非常清楚应该在请求中使用什么内容类型。如果您遵循 JSON 路径,您的客户端将绑定到仅与您的定制服务器端代码兼容的定制实现。如果您愿意,您也无法与多个 OAuth 提供商集成并让最终用户选择登录方法,例如:Google、Facebook、Amazon 等。

标签: java spring rest spring-security-oauth2 spring-oauth2


【解决方案1】:

解决方案(不确定是否正确,但它可以正常工作):

资源服务器配置:

@Configuration
public class ServerEndpointsConfiguration extends ResourceServerConfigurerAdapter {

    @Autowired
    JsonToUrlEncodedAuthenticationFilter jsonFilter;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(jsonFilter, ChannelProcessingFilter.class)
            .csrf().and().httpBasic().disable()
            .authorizeRequests()
            .antMatchers("/test").permitAll()
            .antMatchers("/secured").authenticated();
    }
}

过滤器:

@Component
@Order(value = Integer.MIN_VALUE)
public class JsonToUrlEncodedAuthenticationFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        if (Objects.equals(request.getContentType(), "application/json") && Objects.equals(((RequestFacade) request).getServletPath(), "/oauth/token")) {
            InputStream is = request.getInputStream();
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();

            int nRead;
            byte[] data = new byte[16384];

            while ((nRead = is.read(data, 0, data.length)) != -1) {
                buffer.write(data, 0, nRead);
            }
            buffer.flush();
            byte[] json = buffer.toByteArray();

            HashMap<String, String> result = new ObjectMapper().readValue(json, HashMap.class);
            HashMap<String, String[]> r = new HashMap<>();
            for (String key : result.keySet()) {
                String[] val = new String[1];
                val[0] = result.get(key);
                r.put(key, val);
            }

            String[] val = new String[1];
            val[0] = ((RequestFacade) request).getMethod();
            r.put("_method", val);

            HttpServletRequest s = new MyServletRequestWrapper(((HttpServletRequest) request), r);
            chain.doFilter(s, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
    }
}

请求包装器:

public class MyServletRequestWrapper extends HttpServletRequestWrapper {
    private final HashMap<String, String[]> params;

    public MyServletRequestWrapper(HttpServletRequest request, HashMap<String, String[]> params) {
        super(request);
        this.params = params;
    }

    @Override
    public String getParameter(String name) {
        if (this.params.containsKey(name)) {
            return this.params.get(name)[0];
        }
        return "";
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return this.params;
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return new Enumerator<>(params.keySet());
    }

    @Override
    public String[] getParameterValues(String name) {
        return params.get(name);
    }
}

授权服务器配置(禁用 /oauth/token 端点的基本身份验证:

    @Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    ...

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.allowFormAuthenticationForClients(); // Disable /oauth/token Http Basic Auth
    }

    ...

}

【讨论】:

  • 从哪里获得 RequestFacade
  • 过滤器注册在ResourceServerConfigurerAdapter 中是不必要的——实际上它什么也没做。正在发生的事情是 Spring boot 正在获取所有 Filter 实现并将它们注册为标准 servlet 过滤器而不是 spring 安全过滤器,这就是这项工作的原因。我在非 Spring Boot 应用程序中遇到了这个问题。
【解决方案2】:

来自OAuth 2 specification

客户端通过发送
以下参数使用“application/x-www-form-urlencoded”

访问令牌请求应使用application/x-www-form-urlencoded

在 Spring Security 中,Resource Owner Password Credentials Grant Flow 由 Spring Security 中的ResourceOwnerPasswordTokenGranter#getOAuth2Authentication 处理:

protected OAuth2Authentication getOAuth2Authentication(AuthorizationRequest clientToken) {
    Map parameters = clientToken.getAuthorizationParameters();
    String username = (String)parameters.get("username");
    String password = (String)parameters.get("password");
    UsernamePasswordAuthenticationToken userAuth = new UsernamePasswordAuthenticationToken(username, password);

您可以发送usernamepassword请求参数。

如果您确实需要使用 JSON,有一种解决方法。如您所见,usernamepassword 是从请求参数中检索到的。因此,如果您将它们从 JSON 正文传递到请求参数中,它将起作用。

思路如下:

  1. 创建自定义弹簧安全过滤器。
  2. 在您的自定义过滤器中,创建一个子类HttpRequestWrapper。该类允许您包装原始请求并从 JSON 获取参数。
  3. HttpRequestWrapper 的子类中,解析请求正文中的JSON 以获得usernamepasswordgrant_type,并将它们与原始请求参数一起放入新的HashMap。然后,覆盖getParameterValuesgetParametergetParameterNamesgetParameterMap 的方法以从新的HashMap 返回值
  4. 将您打包的请求传递到过滤器链中。
  5. 在 Spring Security Config 中配置自定义过滤器。

希望对你有帮助

【讨论】:

    【解决方案3】:

    使用 Spring Security 5,我只需要添加 .allowFormAuthenticationForClients() + 另一个答案中提到的 JsontoUrlEncodedAuthenticationFilter 即可使其接受 json 以及 x-form 发布数据。不需要注册资源服务器什么的。

    【讨论】:

      【解决方案4】:

      您还可以修改 @jakub-kopřiva 解决方案以支持 oauth 的 http 基本身份验证。

      资源服务器配置:

      @Configuration
      public class ServerEndpointsConfiguration extends ResourceServerConfigurerAdapter {
      
          @Autowired
          JsonToUrlEncodedAuthenticationFilter jsonFilter;
      
          @Override
          public void configure(HttpSecurity http) throws Exception {
              http
                  .addFilterAfter(jsonFilter, BasicAuthenticationFilter.class)
                  .csrf().disable()
                  .authorizeRequests()
                  .antMatchers("/test").permitAll()
                  .antMatchers("/secured").authenticated();
          }
      }
      

      使用内部 RequestWrapper 过滤

      @Component
      public class JsonToUrlEncodedAuthenticationFilter extends OncePerRequestFilter {
      
          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      
              if (Objects.equals(request.getServletPath(), "/oauth/token") && Objects.equals(request.getContentType(), "application/json")) {
      
                  byte[] json = ByteStreams.toByteArray(request.getInputStream());
      
                  Map<String, String> jsonMap = new ObjectMapper().readValue(json, Map.class);;
                  Map<String, String[]> parameters =
                          jsonMap.entrySet().stream()
                                  .collect(Collectors.toMap(
                                          Map.Entry::getKey,
                                          e ->  new String[]{e.getValue()})
                                  );
                  HttpServletRequest requestWrapper = new RequestWrapper(request, parameters);
                  filterChain.doFilter(requestWrapper, response);
              } else {
                  filterChain.doFilter(request, response);
              }
          }
      
      
          private class RequestWrapper extends HttpServletRequestWrapper {
      
              private final Map<String, String[]> params;
      
              RequestWrapper(HttpServletRequest request, Map<String, String[]> params) {
                  super(request);
                  this.params = params;
              }
      
              @Override
              public String getParameter(String name) {
                  if (this.params.containsKey(name)) {
                      return this.params.get(name)[0];
                  }
                  return "";
              }
      
              @Override
              public Map<String, String[]> getParameterMap() {
                  return this.params;
              }
      
              @Override
              public Enumeration<String> getParameterNames() {
                  return new Enumerator<>(params.keySet());
              }
      
              @Override
              public String[] getParameterValues(String name) {
                  return params.get(name);
              }
          }
      }
      

      您还需要允许 x-www-form-urlencoded 身份验证

          @Configuration
      public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
      
          ...
      
          @Override
          public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
              oauthServer.allowFormAuthenticationForClients();
          }
      
          ...
      
      }
      

      通过这种方法,您仍然可以将基本身份验证用于 oauth 令牌并使用 json 请求令牌,如下所示:

      标题:

      Authorization: Basic bG9yaXpvbfgzaWNwYQ==
      

      主体:

      {
          "grant_type": "password", 
          "username": "admin", 
          "password": "1234"
      }
      

      【讨论】:

        【解决方案5】:

        您可以修改@jakub-kopřiva 解决方案以仅使用以下代码实现授权服务器。

         @Configuration
         @Order(Integer.MIN_VALUE)
         public class AuthorizationServerSecurityConfiguration
            extends org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration {
        
              @Autowired
              JsonToUrlEncodedAuthenticationFilter jsonFilter;
        
              @Override
              protected void configure(HttpSecurity httpSecurity) throws Exception {
                     httpSecurity
                           .addFilterBefore(jsonFilter, ChannelProcessingFilter.class);
                     super.configure(httpSecurity);
              }
        
        }
        

        【讨论】:

          【解决方案6】:

          您好,基于@Jakub Kopřiva 的回答,我已经进行了改进以创建有效的集成测试。 正如您所知,Catalina RequestFacade 在 Junit 中引发错误,而 mockmvc 使用的 MockHttpServletRequest 不包含我在过滤器中所期望的字段“request”(因此在使用 getDeclaredField() 时抛出 NoSuchFieldException): Field f = request.getClass().getDeclaredField("request");
          这就是我使用“放心”的原因。但是,此时我遇到了另一个问题,即无论出于何种原因,“application/json”中的内容类型都被覆盖为“application/json;”。 charset=utf8' 即使我使用MediaType.APPLICATION_JSON_VALUE。但是,该条件会查找位于 MediaType.APPLICATION_JSON_UTF8_VALUE 后面的类似 'application/json;charset=UTF-8' 之类的内容,因此这始终是错误的。
          因此,当我在 PHP 中编码并且我已经对字符串进行了规范化(所有字符都是小写,没有空格)时,我的行为就像我过去所做的那样。 在此之后,集成测试终于通过了。

          ---- JsonToUrlEncodedAuthenticationFilter.java

          package com.example.springdemo.configs;
          
          import com.fasterxml.jackson.databind.ObjectMapper;
          import lombok.SneakyThrows;
          import org.apache.catalina.connector.Request;
          import org.springframework.core.annotation.Order;
          import org.springframework.http.MediaType;
          import org.springframework.security.web.savedrequest.Enumerator;
          import org.springframework.stereotype.Component;
          
          import javax.servlet.*;
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletRequestWrapper;
          import java.io.BufferedReader;
          import java.io.InputStream;
          import java.io.InputStreamReader;
          import java.lang.reflect.Field;
          import java.util.*;
          import java.util.stream.Collectors;
          
          @Component
          @Order(value = Integer.MIN_VALUE)
          
          public class JsonToUrlEncodedAuthenticationFilter implements Filter {
          
              private final ObjectMapper mapper;
          
              public JsonToUrlEncodedAuthenticationFilter(ObjectMapper mapper) {
                  this.mapper = mapper;
              }
          
              @Override
              public void init(FilterConfig filterConfig) {
              }
          
              @Override
              @SneakyThrows
              public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
                  Field f = request.getClass().getDeclaredField("request");
                  f.setAccessible(true);
                  Request realRequest = (Request) f.get(request);
          
                 //Request content type without spaces (inner spaces matter)
                 //trim deletes spaces only at the beginning and at the end of the string
                  String contentType = realRequest.getContentType().toLowerCase().chars()
                          .mapToObj(c -> String.valueOf((char) c))
                          .filter(x->!x.equals(" "))
                          .collect(Collectors.joining());
          
                  if ((contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE.toLowerCase())||
                          contentType.equals(MediaType.APPLICATION_JSON_VALUE.toLowerCase()))
                                  && Objects.equals((realRequest).getServletPath(), "/oauth/token")) {
          
                      InputStream is = realRequest.getInputStream();
                      try (BufferedReader br = new BufferedReader(new InputStreamReader(is), 16384)) {
                          String json = br.lines()
                                  .collect(Collectors.joining(System.lineSeparator()));
                          HashMap<String, String> result = mapper.readValue(json, HashMap.class);
                          HashMap<String, String[]> r = new HashMap<>();
          
                          for (String key : result.keySet()) {
                              String[] val = new String[1];
                              val[0] = result.get(key);
                              r.put(key, val);
                          }
                          String[] val = new String[1];
                          val[0] = (realRequest).getMethod();
                          r.put("_method", val);
          
                          HttpServletRequest s = new MyServletRequestWrapper(((HttpServletRequest) request), r);
                          chain.doFilter(s, response);
                      }
          
                  } else {
                      chain.doFilter(request, response);
                  }
              }
          
              @Override
              public void destroy() {
              }
          
              class MyServletRequestWrapper extends HttpServletRequestWrapper {
                  private final HashMap<String, String[]> params;
          
                  MyServletRequestWrapper(HttpServletRequest request, HashMap<String, String[]> params) {
                      super(request);
                      this.params = params;
                  }
          
                  @Override
                  public String getParameter(String name) {
                      if (this.params.containsKey(name)) {
                          return this.params.get(name)[0];
                      }
                      return "";
                  }
          
                  @Override
                  public Map<String, String[]> getParameterMap() {
                      return this.params;
                  }
          
                  @Override
                  public Enumeration<String> getParameterNames() {
                      return new Enumerator<>(params.keySet());
                  }
          
                  @Override
                  public String[] getParameterValues(String name) {
                      return params.get(name);
                  }
              }
          

          Here is the repo with the integration test

          【讨论】:

            猜你喜欢
            • 2019-04-06
            • 2019-09-25
            • 2019-02-24
            • 2021-11-21
            • 2019-04-29
            • 2017-09-11
            • 2019-04-28
            • 2015-11-14
            • 2016-07-12
            相关资源
            最近更新 更多