5

我从 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"

那可能吗?

你能给我一个建议吗?

4

6 回答 6

11

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

资源服务器配置:

@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
    }

    ...

}
于 2016-07-09T17:04:21.497 回答
3

根据OAuth 2 规范


客户端通过使用“application/x-www-form-urlencoded”发送以下参数向令牌端点发出请求

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

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

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,有一个解决方法。如您所见,username并且password是从请求参数中检索的。因此,如果您将它们从 JSON 正文传递到请求参数中,它将起作用。

思路如下:

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

希望这可以帮助

于 2016-07-03T08:52:14.930 回答
1

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

于 2018-12-12T00:21:47.473 回答
0

您可以修改@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);
      }

}
于 2019-01-10T13:00:24.407 回答
0

您好,基于@Jakub Kopřiva 的回答,我已经进行了改进以创建有效的集成测试。正如您所知,Catalina RequestFacade 在 Junit 中抛出错误,而 mockmvc 使用的 MockHttpServletRequest 不包含我在过滤器中所期望的字段“请求”(因此在使用 getDeclaredField() 时抛出 NoSuchFieldException): Field f = request.getClass().getDeclaredField("request");
这就是我使用“放心”。但是,此时我遇到了另一个问题,即无论出于何种原因,“application/json”中的内容类型都被覆盖为“application/json;”。charset=utf8' 即使我使用MediaType.APPLICATION_JSON_VALUE. 但是,该条件会查找 'application/json;charset=UTF-8' 之类的东西,它位于 后面MediaType.APPLICATION_JSON_UTF8_VALUE,总之这将始终为假。
因此,当我在 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);
        }
    }

这是带有集成测试的仓库

于 2019-05-06T20:10:45.323 回答
0

您还可以修改@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"
}
于 2017-05-10T11:26:20.220 回答