You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apache代理下DRF的Swagger UI遇Basic Auth 403禁止错误

问题:Apache添加Basic Auth后Django DRF Swagger UI返回403 Forbidden

环境背景

  • 部署Django Rest Framework应用,后端运行在gunicorn(端口8000)
  • 采用Apache做反向代理(因应用部分模块的认证需求必须使用Apache)

正常运行的Apache配置

<Location "/api/v1/schema">
    Require all granted
    #
    ProxyPass        http://localhost:8000/api/v1/schema
    ProxyPassReverse http://localhost:8000/api/v1/schema
</Location>

添加Basic Auth后的配置(出现403错误)

<Location "/api/v1/schema">
    AuthType Basic
    AuthName "MyAPI Swagger UI"
    AuthUserFile /etc/apache2/swagger.htpasswd
    Require valid-user
    #
    ProxyPass        http://localhost:8000/api/v1/schema
    ProxyPassReverse http://localhost:8000/api/v1/schema
</Location>

错误日志

Apache日志

myapi.example.com:443 130.211.2.57 - swagger [21/Jul/2024:18:51:59 +0000] "GET /api/v1/schema/swagger-ui/ HTTP/1.1" 403 378 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"

gunicorn日志

Forbidden: /api/v1/schema/swagger-ui/

Django DRF相关配置

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}

更新信息

执行showurls命令获取到对应路由:

/api/v1/schema/swagger-ui/  drf_spectacular.views.SpectacularSwaggerView  swagger-ui

查看drf_spectacular模块中views.py的对应视图代码:

class SpectacularSwaggerView(APIView):
    renderer_classes = [TemplateHTMLRenderer]
    permission_classes = spectacular_settings.SERVE_PERMISSIONS
    authentication_classes = AUTHENTICATION_CLASSES
    url_name: str = 'schema'
    url: Optional[str] = None
    template_name: str = 'drf_spectacular/swagger_ui.html'
    template_name_js: str = 'drf_spectacular/swagger_ui.js'
    title: str = spectacular_settings.TITLE

    @extend_schema(exclude=True)
    def get(self, request, *args, **kwargs):
        return Response(
            data={
                'title': self.title,
                'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'),
                'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'),
                'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'),
                'favicon_href': self._swagger_ui_favicon(),
                'schema_url': self._get_schema_url(request),
                'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS),
                'oauth2_config': self._dump(spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG),
                'template_name_js': self.template_name_js,
                'csrf_header_name': self._get_csrf_header_name(),
                'schema_auth_names': self._dump(self._get_schema_auth_names()),
            },
            template_name=self.template_name,
            headers={
                "Cross-Origin-Opener-Policy": "unsafe-none",
            }
        )

    def _dump(self, data):
        return data if isinstance(data, str) else json.dumps(data, indent=2)

    def _get_schema_url(self, request):
        schema_url = self.url or get_relative_url(reverse(self.url_name, request=request))
        return set_query_parameters(
            url=schema_url,
            lang=request.GET.get('lang'),
            version=request.GET.get('version')
        )

    def _get_csrf_header_name(self):
        csrf_header_name = settings.CSRF_HEADER_NAME
        if csrf_header_name.startswith('HTTP_'):
            csrf_header_name = csrf_header_name[5:]
        return csrf_header_name.replace('_', '-')

    def _get_schema_auth_names(self):
        from drf_spectacular.extensions import OpenApiAuthenticationExtension
        if spectacular_settings.SERVE_PUBLIC:
            return []
        auth_extensions = [
            OpenApiAuthenticationExtension.get_match(klass)
            for klass in self.authentication_classes
        ]
        return [auth.name for auth in auth_extensions if auth]

    @staticmethod
    def _swagger_ui_resource(filename):
        if spectacular_settings.SWAGGER_UI_DIST == 'SIDECAR':
            return _get_sidecar_url(f'swagger-ui-dist/{filename}')
        return f'{spectacular_settings.SWAGGER_UI_DIST}/{filename}'

    @staticmethod
    def _swagger_ui_favicon():
        if spectacular_settings.SWAGGER_UI_FAVICON_HREF == 'SIDECAR':
            return _get_sidecar_url('swagger-ui-dist/favicon-32x32.png')
        return spectacular_settings.SWAGGER_UI_FAVICON_HREF

解决方案

问题根源是Apache会将Basic Auth的Authorization请求头转发给Django,而SpectacularSwaggerView默认继承了DRF的认证配置,会尝试验证该头信息,但Django中不存在Apache htpasswd里的用户,因此返回403。以下两种方法可解决:

方法1:修改Apache配置,不传递Authorization头给Django

让Apache完成Basic Auth验证后,移除请求头中的Authorization信息再转发给gunicorn:

<Location "/api/v1/schema">
    AuthType Basic
    AuthName "MyAPI Swagger UI"
    AuthUserFile /etc/apache2/swagger.htpasswd
    Require valid-user
    # 移除Authorization头
    RequestHeader unset Authorization
    ProxyPass        http://localhost:8000/api/v1/schema nocanon
    ProxyPassReverse http://localhost:8000/api/v1/schema
</Location>

方法2:修改drf-spectacular配置,允许匿名访问Swagger UI

在Django的settings.py中添加配置,强制Swagger UI视图允许匿名访问:

SPECTACULAR_SETTINGS = {
    'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'],
    'SERVE_AUTHENTICATION': [],
}

内容的提问来源于stack exchange,提问作者rlandster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 14:35:55