从 Ember 发布到 DRF API 时出现 406(不可接受)

Posted

技术标签:

【中文标题】从 Ember 发布到 DRF API 时出现 406(不可接受)【英文标题】:406 (Not Acceptable) when POSTing from Ember to DRF API 【发布时间】:2016-06-23 11:13:52 【问题描述】:

我使用ember-simple-authember-simple-auth-token 来允许用户登录我的应用程序。但是,当我使用 POST 请求在后端调用 Django Rest Framework 以使用用户名和密码进行身份验证时,我收到 406(不可接受)错误。这在 DRF 可浏览 API 中不会发生,因此后端似乎工作正常。

我怀疑与 CORS 相关的事情。我在 Django 中使用 django-cors-headers,并在我的开发环境中允许所有内容。我还使用django-rest-framework-jwtdjango-rest-framework-json-api 包,如果这很重要的话。

我的 API 显示一个 OPTIONS,然后是一个 POST 调用:

[09/Mar/2016 07:15:54] "OPTIONS /api-token-auth/ HTTP/1.1" 200 0
[09/Mar/2016 07:15:54] "POST /api-token-auth/ HTTP/1.1" 406 114

响应标头:

HTTP/1.0 406 Not Acceptable
Date: Wed, 09 Mar 2016 07:15:54 GMT
Server: WSGIServer/0.2 CPython/3.5.1
X-Frame-Options: SAMEORIGIN
Access-Control-Allow-Origin: *
Content-Type: application/vnd.api+json
Allow: POST, OPTIONS
Vary: Accept

请求标头:

POST /api-token-auth/ HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Content-Length: 2
Accept: application/json, text/javascript
Origin: http://localhost:4200
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (Khtml, like Gecko) Ubuntu Chromium/48.0.2564.116 Chrome/48.0.2564.116 Safari/537.36
Content-Type: application/json
Referer: http://localhost:4200/login
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8

请求标头不显示application/vnd.api+json,而是显示application/json。不幸的是,无论我在 Ember 中做什么,都能解决这个问题。我尝试将我应用的 JSONAPIAdapter 和 ENV['ember-simple-auth-token'] 的标头设置为 "Accept": "application/vnd.api+json",但未成功。

【问题讨论】:

听说ember和DRF默认是不兼容的。你在用github.com/dustinfarris/ember-django-adapter之类的东西吗? @ilse2005,我正在使用 django-rest-framework-json-api 使 DRF 响应 JSON API 规范兼容。 【参考方案1】:

实现您自己的身份验证器,用于设置身份验证请求期间使用的标头:

// your-app/authenticators/your-custom-authenticator.js
import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';

export default OAuth2PasswordGrant.extend(

  /**
   * your backend authentication endpoint
   * @overrides
   */
  serverTokenEndpoint: `https://your.authentication.endpoint.sth/login`,

  /**
   * Makes a request to the authentication server.
   * This is what you need to override to customize your headers
   * set up for purposes of authentication.
   * @overrides
   */
  makeRequest(url, data) 
    const options = 
      url: url,
      data: data,
      type: 'GET',
      dataType: 'json',
      accept: 'application/vnd.api+json',
      headers: 
        "Content-Type": 'application/vnd.api+json'
      
    ;

    return Ember.$.ajax(options);
  
);

在您的(登录)路由/控制器/您需要的任何地方参考此自定义身份验证器:

this.get('session').authenticate('authenticator:yourCustomAuthenticator', username, password).then(() => 
          // success, redirect, as you like..
        )

查看 ember-simple-auth 文档的 Authenticators 部分,根据需要选择最接近您需要的父身份验证器:ember-simple-auth - Authenticators

【讨论】:

【参考方案2】:

我或多或少地设法解决了这个问题。这是一个不幸的包组合,导致 Ember 和 DRF 之间的 JSON API 规范出现一些问题。

首先,通过简单地将标头作为参数添加到.authenticate,我设法在controllers/login.js 中覆盖标头。任何 args 都会传递给 ember-simple-auth 身份验证器。 (正如 Pavol 在他的回答中建议的那样,我不需要实现自己的身份验证器。)

// controllers/login.js
import Ember from 'ember';

export default Ember.Controller.extend(
  session: Ember.inject.service('session'),

  actions: 
    authenticate: function() 
      var credentials = this.getProperties('identification', 'password'),
        authenticator = 'authenticator:jwt',
        // Set headers to accept JSON API format
        headers = 
          'Accept': 'application/vnd.api+json',
          'Content-Type': 'application/vnd.api+json'
        ;

      this.get('session').authenticate(authenticator, credentials, headers);
    
  
);

这引入了下一个问题:我的内容类型实际上不是 JSON API 规范,所以我确实需要实现自己的验证器来翻译 ember-simple-auth-token 的 JWT 验证器以生成 JSON API 规范兼容的格式。没有让它工作,但像这样:

// authenticators/jwt.js
import Base from 'ember-simple-auth-token/authenticators/token';

export default Base.extend(
  /**
    Returns an object used to be sent for authentication.

    @method getAuthenticateData
    @return object An object with properties for authentication.
  */
  // Make sure this is JSON API compatible format.
  getAuthenticateData(credentials) 
    const authentication = 
      // This is apparently not valid JSON API spec, but you get the gist...
      'data': [
        [this.identificationField]: credentials.identification,
        [this.passwordField]: credentials.password
      ]
    ;

    return authentication;
  
);

现在,在后端,rest_framework_jwtrest_framework_json_api 仍然不能很好地协同工作。

此时,我决定在身份验证端点上放弃对 JSON API 规范的需求要简单得多:Ember 的包没有生成它,而且 DRF 无法解析它!

所以,我还原了 Ember 端的所有内容,让它根据我原来的问题生成请求。在 DRF 方面,我对 rest_framework_jwt 的视图进行了子类化,并将解析器设置为 DRF 的默认 JSONParser

"""
Make the JWT Views ignore JSON API package and use standard JSON.
"""

from rest_framework_jwt.views import ObtainJSONWebToken, RefreshJSONWebToken, \
    VerifyJSONWebToken
from rest_framework.parsers import JSONParser
from rest_framework.renderers import JSONRenderer


class ObtainJSONWebTokenPlainJSON(ObtainJSONWebToken):
    parser_classes = (JSONParser, )
    renderer_classes = (JSONRenderer, )


class RefreshJSONWebTokenPlainJSON(RefreshJSONWebToken):
    parser_classes = (JSONParser, )
    renderer_classes = (JSONRenderer,)


class VerifyJSONWebTokenPlainJSON(VerifyJSONWebToken):
    parser_classes = (JSONParser, )
    renderer_classes = (JSONRenderer,)

最终结果:通过让我的 API 在除令牌身份验证端点之外的所有地方都遵循 JSON API 规范来解决。

【讨论】:

FWI,只要确保 rest_framework.parsers.JSONParserrest_framework_json_api.parsers.JSONParser 都包含在 DRF 设置的 DEFAULT_PARSER_CLASSES 中,我就能够使 JSON API 和普通 JSON 请求都工作(并因此进行身份验证)以及DEFAULT_RENDERER_CLASSES 设置中的rest_framework_json_api.renderers.JSONRendererrest_framework.renderers.JSONRenderer @TimmyO'Mahony,这也对我有用。谢谢,它使我的代码更加简洁。【参考方案3】:

您应该能够在适配器中显式设置内容类型:

export default DS.JSONAPIAdapter.extend( 
 // set content-type upon every ajax request 
 ajax: function(url, type, hash) 
 hash = hash ||  ;
 hash.headers = hash.headers || ;
 hash.headers['Content-Type'] = 'application/vnd.api+json';
 return this._super(url, type, hash); 
  
);

它能解决你的问题吗?

【讨论】:

您能否至少确认您的请求在标头的content-type 字段中包含application/vnd.api+json 而不是application/json 它没有。如果可以,那可能会解决我的问题。 我的建议是在上面的ajax()函数中打断点,明确程序执行到断点。之后我可能会跳到 this.super() 并观察 Content-Type 标题重写的确切位置 你是对的,只有 Ember-Data 请求才会达到这一点,而不是你的身份验证请求。请看我下面的帖子,它现在应该可以解决您的问题;)

以上是关于从 Ember 发布到 DRF API 时出现 406(不可接受)的主要内容,如果未能解决你的问题,请参考以下文章

使用带有 Ajax 查询的预先输入时出现 ember-select-2 问题

使用带有 Ajax 查询的预先输入时出现 ember-select-2 问题

尝试从客户端登录到后端烧瓶 api 时出现 401 UNAUTHORIZED 状态

尝试在 debian linux 上运行 ember 测试时出现奇怪的错误

尝试向 API 发送 POST 请求时出现属性错误 - Django

使用 Django Rest Framework 将输入文件从 Vue.js 发送到 Django 时出现问题