Logo
Section 1 / 3

XHR 请求(JavaScript 客户端场景)

学习如何以敏捷的方式为客户端设置 JavaScript。

导入

XHR 请求(JavaScript 客户端场景)

学习如何以敏捷的方式为客户端设置 JavaScript。

在使用 JavaScript 以敏捷方式实现客户端开发之前,你需要配置一些关键设置。

CORS 策略

如果你希望在客户端使用 JavaScript 和 XML HTTP 请求(XHR),则需要在你的授权服务器上创建一个过滤器,用于为每个请求设置跨域资源共享(CORS)策略。

具体实现方式取决于你使用的 Spring 版本。

对于 Spring Boot 1.5,你可能需要创建一个 WebMvcConfigurer 类的自定义 Bean,如下所示:

1@Configuration 2public class WebConfig { 3 @Bean 4 public WebMvcConfigurer corsConfigurer() { 5 return new WebMvcConfigurerAdapter() { 6 @Override 7 public void addCorsMappings(CorsRegistry registry) { 8 registry.addMapping("/oauth/**"); 9 } 10 }; 11 } 12}

然而,对于 Spring Boot 2.1.x,我们需要采用更高效的方式。

我们将创建一个继承自 OncePerRequestFilter 的 Bean:

1@Component 2@Order(PriorityOrdered.HIGHEST_PRECEDENCE) 3public class CorsFilter extends OncePerRequestFilter { 4 @Override 5 @SneakyThrows 6 protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) { 7 if (req.getServletPath().equals("/oauth/token")) { 8 res.addHeader("Access-Control-Allow-Origin", "*"); 9 res.addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); 10 res.addHeader("Access-Control-Max-Age", "3600"); 11 res.addHeader("Access-Control-Allow-Headers", "X-PINGOTHER,Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization"); 12 if (req.getMethod().equals(HttpMethod.OPTIONS.name())) { 13 res.setStatus(HttpServletResponse.SC_OK); 14 } else { 15 chain.doFilter(req, res); 16 } 17 } else { 18 chain.doFilter(req, res); 19 } 20 } 21}

通过 JSON 发送登录凭证(/oauth/token)—— 授权服务器

这种场景通常出现在我们发送 AJAX 请求时,客户端以 JSON 对象形式传递数据,尤其适用于“密码授权模式”。

在 JsonToUrlEncodedAuthenticationFilter 的 doFilter 方法中,我们依赖于私有字段 request 来将客户端发送的 JSON 对象进行转换。这里使用反射并将该字段设为可访问。随后我们会把解析出的 JSON 数据转换为 HashMap,并用它包装请求。

原因在于 OAuth2 原始标准要求使用 application/x-www-form-urlencoded 格式,而 Spring Security 不支持其他格式(与 Postman 等工具不同)。因此,在此阶段,请求中的 URL 编码数据必须是 HashMap 格式。我们需要将客户端通过 JavaScript 发送的 JSON 数据转换为 HashMap。

1@Override 2@SneakyThrows 3public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { 4 Field f = request.getClass().getDeclaredField("request"); 5 f.setAccessible(true); 6 Request realRequest = (Request) f.get(request); 7 8 // 去除内容类型中的空格(内部空格很重要) 9 // trim 只移除字符串首尾的空格 10 String contentType = realRequest.getContentType().toLowerCase().chars() 11 .mapToObj(c -> String.valueOf((char) c)) 12 .filter(x -> !x.equals(" ")) 13 .collect(Collectors.joining()); 14 15 if ((contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE.toLowerCase()) || 16 contentType.equals(MediaType.APPLICATION_JSON_VALUE.toLowerCase())) 17 && Objects.equals(realRequest.getServletPath(), "/oauth/token")) { 18 19 InputStream is = realRequest.getInputStream(); 20 try (BufferedReader br = new BufferedReader(new InputStreamReader(is), 16384)) { 21 String json = br.lines() 22 .collect(Collectors.joining(System.lineSeparator())); 23 HashMap<String, String> result = mapper.readValue(json, HashMap.class); 24 HashMap<String, String[]> r = new HashMap<>(); 25 26 for (String key : result.keySet()) { 27 String[] val = new String[1]; 28 val[0] = result.get(key); 29 r.put(key, val); 30 } 31 String[] val = new String[1]; 32 val[0] = realRequest.getMethod(); 33 r.put("_method", val); 34 35 HttpServletRequest s = new MyServletRequestWrapper((HttpServletRequest) request, r); 36 chain.doFilter(s, response); 37 } 38 39 } else { 40 chain.doFilter(request, response); 41 } 42}

上述处理流程允许我们在请求体中以 application/json 内容类型发送数据,而不是传统的 application/x-www-form-urlencoded。

这意味着我们可以更方便地使用 JavaScript 作为 HTTP 客户端,例如直接使用 fetch,无需借助隐藏表单输入来承载对象内容。

...
OAuth2 密码授权示例(Postman)

重要提示

请注意,除非特别说明,上述流程中的 POST 请求默认并不会接收 JSON 对象。

若要发送 JSON 对象,必须使用如上所述的过滤器,使服务器能够正确解析。

理论

回到核心