简体   繁体   English

Spring安全性OAuth2接受JSON

[英]Spring security OAuth2 accept JSON

I am starting with Spring OAuth2. 我从Spring OAuth2开始。 I would like to send the username and password to /oauth/token endpoint in POST body in application/json format. 我想以app / 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? 你能给我一个建议吗?

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
    }

    ...

}

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#getOAuth2Authentication in Spring Security: 在Spring安全性中,资源所有者密码凭据授权流由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);

You can send username and password to request parameter. 您可以发送usernamepassword来请求参数。

If you really need to use JSON, there is a workaround. 如果您确实需要使用JSON,则有一种解决方法。 As you can see, username and password is retrieved from request parameter. 如您所见,从请求参数中检索usernamepassword Therefore, it will work if you pass them from JSON body into the request parameter. 因此,如果将它们从JSON主体传递到请求参数,它将起作用。

The idea is like follows: 这个想法如下:

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

Hope this can help 希望这可以提供帮助

Also you can modify @jakub-kopřiva solution to support http basic auth for oauth. 您也可以修改@ jakub-kopřiva解决方案以支持oauth的http basic auth。

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

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. 使用Spring Security 5,我只需添加.allowFormAuthenticationForClients()+另一个答案中提到的JsontoUrlEncodedAuthenticationFilter,除了x-form post数据外,还要接受json。 There was no need to register the resource server or anything. 无需注册资源服务器或任何其他内容。

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

}

Hello based on @Jakub Kopřiva answer I have made improvements in order to create working integration tests. 您好基于@JakubKopřiva回答我已经做了改进,以创建有效的集成测试。 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"); 您知道,Catalina RequestFacade在Junit中抛出一个错误,并且mockmvc使用的MockHttpServletRequest不包含我在过滤器中预期的字段“request”(因此在使用getDeclaredField()时抛出NoSuchFieldException): Field f = request.getClass().getDeclaredField("request");
This is why I used "Rest Assured". 这就是我使用“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; 但是在这一点上我遇到了另一个问题,即无论出于何种原因,'application / json'中的内容类型被覆盖到'application / json; charset=utf8' even though I use MediaType.APPLICATION_JSON_VALUE . charset = utf8'即使我使用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. 但是,该条件会查找类似于'application / json; charset = UTF-8'的内容,它位于MediaType.APPLICATION_JSON_UTF8_VALUE ,总之,这将始终为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). 因此,当我用PHP编码时,我的行为与以往一样,并且我对字符串进行了规范化(所有字符都是小写,没有空格)。 After this the integration test finally passes. 在此之后,集成测试最终通过。

---- JsonToUrlEncodedAuthenticationFilter.java ---- 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 这是集成测试的repo

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM