java Spring 安全 OAuth2 接受 JSON

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38165131/
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-03 03:11:34  来源:igfitidea点击:

Spring security OAuth2 accept JSON

javaspringrestspring-security-oauth2spring-oauth2

提问by Jakub Kop?iva

I am starting with Spring OAuth2. I would like to send the username and password to /oauth/token endpoint in POST body in application/json format.

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

Is that possible?

那可能吗?

Could you please give me an advice?

你能给我一个建议吗?

回答by Jakub Kop?iva

Solution (not sure if correct, but it seam that it is working):

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

Resource Server Configuration:

资源服务器配置:

@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();
    }
}

Filter:

筛选:

@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() {
    }
}

Request Wrapper:

请求包装器:

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);
    }
}

Authorization Server Configuration (disable Basic Auth for /oauth/token endpoint:

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

    @Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    ...

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

    ...

}

回答by Wilson

From the OAuth 2 specification,

根据OAuth 2 规范

The client makes a request to the token endpoint by sending the
following parameters using the "application/x-www-form-urlencoded"

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

Access token request should use application/x-www-form-urlencoded.

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

In Spring security, the Resource Owner Password Credentials Grant Flow is handled by ResourceOwnerPasswordTokenGranter#getOAuth2Authenticationin Spring Security:

在 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);

You can send usernameand passwordto request parameter.

您可以发送usernamepassword请求参数。

If you really need to use JSON, there is a workaround. As you can see, usernameand passwordis retrieved from request parameter. Therefore, it will work if you pass them from JSON body into the request parameter.

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

The idea is like follows:

思路如下:

  1. Create a custom spring security filter.
  2. In your custom filter, create a class to subclass HttpRequestWrapper. The class allow you to wrap the original request and get parameters from JSON.
  3. In your subclass of HttpRequestWrapper, parse your JSON in request body to get username, passwordand grant_type, and put them with the original request parameter into a new HashMap. Then, override method of getParameterValues, getParameter, getParameterNamesand getParameterMapto return values from that new HashMap
  4. Pass your wrapped request into the filter chain.
  5. Configure your custom filter in your Spring Security Config.
  1. 创建自定义 spring 安全过滤器。
  2. 在您的自定义过滤器中,创建一个类来子类化HttpRequestWrapper。该类允许您包装原始请求并从 JSON 获取参数。
  3. 在您的 子类中HttpRequestWrapper,解析请求正文中的 JSON 以获取usernamepasswordgrant_type,并将它们与原始请求参数放入一个新的HashMap. 然后,覆盖方法getParameterValuesgetParametergetParameterNamesgetParameterMap从该新的返回值HashMap
  4. 将您包装的请求传递到过滤器链中。
  5. 在 Spring Security Config 中配置自定义过滤器。

Hope this can help

希望这可以帮助

回答by redbuo

Also you can modify @jakub-kop?iva solution to support http basic auth for oauth.

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

Resource Server Configuration:

资源服务器配置:

@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();
    }
}

Filter with internal RequestWrapper

使用内部 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);
        }
    }
}

And also you need to allow x-www-form-urlencoded authentication

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

    @Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    ...

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.allowFormAuthenticationForClients();
    }

    ...

}

With this approach you can still use basic auth for oauth token and request token with json like this:

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

Header:

标题:

Authorization: Basic bG9yaXpvbfgzaWNwYQ==

Body:

身体:

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

回答by mmphantom

With Spring Security 5 I only had to add .allowFormAuthenticationForClients() + the JsontoUrlEncodedAuthenticationFilter noted in the other answer to get it to accept json in addition to x-form post data. There was no need to register the resource server or anything.

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

回答by Neeraj Verma

You can modify @jakub-kop?iva solution to implement only authorization server with below code.

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

}

回答by Davide Pugliese

Hello based on @Jakub Kop?iva answer I have made improvements in order to create working integration tests. Just so you know, Catalina RequestFacade throws an error in Junit and MockHttpServletRequest, used by mockmvc, does not contain a field "request" as I expect in the filter (therefore throwning NoSuchFieldException when using getDeclaredField()): Field f = request.getClass().getDeclaredField("request");
This is why I used "Rest Assured". However at this point I ran into another issue which is that for whatever reason the content-type from 'application/json' is overwritten into 'application/json; charset=utf8' even though I use MediaType.APPLICATION_JSON_VALUE. However, the condition looks for something like 'application/json;charset=UTF-8' which lies behind MediaType.APPLICATION_JSON_UTF8_VALUE, and in conclusion this will always be false.
Therefore I behaved as I used to do when I coded in PHP and I have normalized the strings (all characters are lowercase, no spaces). After this the integration test finally passes.

你好,基于@Jakub Kop?iva 的回答,为了创建有效的集成测试,我进行了改进。只是你知道,Catalina RequestFacade 在 Junit 和 MockHttpServletRequest 中抛出一个错误,由 mockmvc 使用,不包含我在过滤器中期望的字段“请求”(因此在使用 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

---- JsonToUrlEncodedAuthenticationFilter.java

package com.example.springdemo.configs;

import com.fasterxml.Hymanson.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

这是带有集成测试的 repo