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#getOAuth2Authentication
in Spring Security:
在 Spring Security 中,Resource Owner Password Credentials Grant Flow 由ResourceOwnerPasswordTokenGranter#getOAuth2Authentication
Spring 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 username
and password
to request parameter.
您可以发送username
和password
请求参数。
If you really need to use JSON, there is a workaround. As you can see, username
and password
is 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
,password
andgrant_type
, and put them with the original request parameter into a newHashMap
. Then, override method ofgetParameterValues
,getParameter
,getParameterNames
andgetParameterMap
to 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);
}
}