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
Spring security OAuth2 accept JSON
提问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.
您可以发送username和password请求参数。
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,则有一种解决方法。如您所见,username并password从请求参数中检索。因此,如果您将它们从 JSON 正文传递到请求参数中,它将起作用。
The idea is like follows:
思路如下:
- Create a custom spring security filter.
- In your custom filter, create a class to subclass
HttpRequestWrapper. The class allow you to wrap the original request and get parameters from JSON. - In your subclass of
HttpRequestWrapper, parse your JSON in request body to getusername,passwordandgrant_type, and put them with the original request parameter into a newHashMap. Then, override method ofgetParameterValues,getParameter,getParameterNamesandgetParameterMapto return values from that newHashMap - Pass your wrapped request into the filter chain.
- Configure your custom filter in your Spring Security Config.
- 创建自定义 spring 安全过滤器。
- 在您的自定义过滤器中,创建一个类来子类化
HttpRequestWrapper。该类允许您包装原始请求并从 JSON 获取参数。 - 在您的 子类中
HttpRequestWrapper,解析请求正文中的 JSON 以获取username、password和grant_type,并将它们与原始请求参数放入一个新的HashMap. 然后,覆盖方法getParameterValues,getParameter,getParameterNames并getParameterMap从该新的返回值HashMap - 将您包装的请求传递到过滤器链中。
- 在 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);
}
}

