diff --git a/ruoyi-fastapi-backend/.env.dev b/ruoyi-fastapi-backend/.env.dev index c0735a7..0048481 100644 --- a/ruoyi-fastapi-backend/.env.dev +++ b/ruoyi-fastapi-backend/.env.dev @@ -17,6 +17,10 @@ APP_RELOAD = true APP_IP_LOCATION_QUERY = true # 应用是否允许账号同时登录 APP_SAME_TIME_LOGIN = true +# 应用是否禁用Swagger文档 +APP_DISABLE_SWAGGER = false +# 应用是否禁用ReDoc文档 +APP_DISABLE_REDOC = false # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.dockermy b/ruoyi-fastapi-backend/.env.dockermy index 6963cd2..7c0796e 100644 --- a/ruoyi-fastapi-backend/.env.dockermy +++ b/ruoyi-fastapi-backend/.env.dockermy @@ -17,6 +17,10 @@ APP_RELOAD = false APP_IP_LOCATION_QUERY = true # 应用是否允许账号同时登录 APP_SAME_TIME_LOGIN = true +# 应用是否禁用Swagger文档 +APP_DISABLE_SWAGGER = true +# 应用是否禁用ReDoc文档 +APP_DISABLE_REDOC = true # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.dockerpg b/ruoyi-fastapi-backend/.env.dockerpg index ccaeb53..e254e15 100644 --- a/ruoyi-fastapi-backend/.env.dockerpg +++ b/ruoyi-fastapi-backend/.env.dockerpg @@ -17,6 +17,10 @@ APP_RELOAD = false APP_IP_LOCATION_QUERY = true # 应用是否允许账号同时登录 APP_SAME_TIME_LOGIN = true +# 应用是否禁用Swagger文档 +APP_DISABLE_SWAGGER = true +# 应用是否禁用ReDoc文档 +APP_DISABLE_REDOC = true # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.prod b/ruoyi-fastapi-backend/.env.prod index 8ed5d3c..5237abc 100644 --- a/ruoyi-fastapi-backend/.env.prod +++ b/ruoyi-fastapi-backend/.env.prod @@ -17,6 +17,10 @@ APP_RELOAD = false APP_IP_LOCATION_QUERY = true # 应用是否允许账号同时登录 APP_SAME_TIME_LOGIN = true +# 应用是否禁用Swagger文档 +APP_DISABLE_SWAGGER = true +# 应用是否禁用ReDoc文档 +APP_DISABLE_REDOC = true # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py index 153b7a1..518c068 100644 --- a/ruoyi-fastapi-backend/config/env.py +++ b/ruoyi-fastapi-backend/config/env.py @@ -23,6 +23,8 @@ class AppSettings(BaseSettings): app_reload: bool = True app_ip_location_query: bool = True app_same_time_login: bool = True + app_disable_swagger: bool = False + app_disable_redoc: bool = False class JwtSettings(BaseSettings): diff --git a/ruoyi-fastapi-backend/server.py b/ruoyi-fastapi-backend/server.py index 724d8b5..42bc4bd 100644 --- a/ruoyi-fastapi-backend/server.py +++ b/ruoyi-fastapi-backend/server.py @@ -1,9 +1,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from fastapi import FastAPI, applications -from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html -from fastapi.responses import HTMLResponse +from fastapi import FastAPI from common.router import auto_register_routers from config.env import AppConfig @@ -15,6 +13,7 @@ from sub_applications.handle import handle_sub_applications from utils.common_util import worship from utils.log_util import logger +from utils.server_util import APIDocsUtil, IPUtil # 生命周期事件 @@ -28,48 +27,35 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: await RedisUtil.init_sys_config(app.state.redis) await SchedulerUtil.init_system_scheduler() logger.info(f'🚀 {AppConfig.app_name}启动成功') - yield - await RedisUtil.close_redis_pool(app) - await SchedulerUtil.close_system_scheduler() - - -def setup_docs_static_resources( - redoc_js_url: str = 'https://registry.npmmirror.com/redoc/2/files/bundles/redoc.standalone.js', - redoc_favicon_url: str = 'https://fastapi.tiangolo.com/img/favicon.png', - swagger_js_url: str = 'https://registry.npmmirror.com/swagger-ui-dist/5/files/swagger-ui-bundle.js', - swagger_css_url: str = 'https://registry.npmmirror.com/swagger-ui-dist/5/files/swagger-ui.css', - swagger_favicon_url: str = 'https://fastapi.tiangolo.com/img/favicon.png', -) -> None: - """ - 配置文档静态资源 + host = AppConfig.app_host + port = AppConfig.app_port + if host == '0.0.0.0': + local_ip = IPUtil.get_local_ip() + network_ips = IPUtil.get_network_ips() + else: + local_ip = host + network_ips = [host] - :param redoc_js_url: 用于加载ReDoc JavaScript的URL - :param redoc_favicon_url: ReDoc要使用的favicon的URL - :param swagger_js_url: 用于加载Swagger UI JavaScript的URL - :param swagger_css_url: 用于加载Swagger UI CSS的URL - :param swagger_favicon_url: Swagger UI要使用的favicon的URL - :return: - """ + app_links = [f'🏠 Local: http://{local_ip}:{port}'] + app_links.extend(f'📡 Network: http://{ip}:{port}' for ip in network_ips) + logger.opt(colors=True).info('💻 应用地址:\n' + '\n'.join(app_links)) - def redoc_monkey_patch(*args, **kwargs) -> HTMLResponse: - return get_redoc_html( - *args, - **kwargs, - redoc_js_url=redoc_js_url, - redoc_favicon_url=redoc_favicon_url, + if not AppConfig.app_disable_swagger: + swagger_links = [f'🏠 Local: http://{local_ip}:{port}{APIDocsUtil.docs_url()}'] + swagger_links.extend( + f'📡 Network: http://{ip}:{port}{APIDocsUtil.docs_url()}' for ip in network_ips ) + logger.opt(colors=True).info('📄 Swagger文档:\n' + '\n'.join(swagger_links)) - def swagger_ui_monkey_patch(*args, **kwargs) -> HTMLResponse: - return get_swagger_ui_html( - *args, - **kwargs, - swagger_js_url=swagger_js_url, - swagger_css_url=swagger_css_url, - swagger_favicon_url=swagger_favicon_url, + if not AppConfig.app_disable_redoc: + redoc_links = [f'🏠 Local: http://{local_ip}:{port}{APIDocsUtil.redoc_url()}'] + redoc_links.extend( + f'📡 Network: http://{ip}:{port}{APIDocsUtil.redoc_url()}' for ip in network_ips ) - - applications.get_redoc_html = redoc_monkey_patch - applications.get_swagger_ui_html = swagger_ui_monkey_patch + logger.opt(colors=True).info('📚 ReDoc文档:\n' + '\n'.join(redoc_links)) + yield + await RedisUtil.close_redis_pool(app) + await SchedulerUtil.close_system_scheduler() def create_app() -> FastAPI: @@ -78,16 +64,23 @@ def create_app() -> FastAPI: :return: FastAPI对象 """ - # 配置文档静态资源 - setup_docs_static_resources() + # 配置API文档静态资源 + APIDocsUtil.setup_docs_static_resources() # 初始化FastAPI对象 app = FastAPI( title=AppConfig.app_name, description=f'{AppConfig.app_name}接口文档', version=AppConfig.app_version, lifespan=lifespan, + openapi_url=APIDocsUtil.proxy_openapi_url(), + docs_url=APIDocsUtil.proxy_docs_url(), + redoc_url=APIDocsUtil.proxy_redoc_url(), + swagger_ui_oauth2_redirect_url=APIDocsUtil.proxy_oauth2_redirect_url(), ) + # 自定义API文档路由,修复无法直接通过后端地址访问文档的问题 + APIDocsUtil.custom_api_docs_router(app) + # 挂载子应用 handle_sub_applications(app) # 加载中间件处理方法 diff --git a/ruoyi-fastapi-backend/utils/server_util.py b/ruoyi-fastapi-backend/utils/server_util.py new file mode 100644 index 0000000..873fe89 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/server_util.py @@ -0,0 +1,399 @@ +import ipaddress +import socket +from collections.abc import Callable + +import psutil +from fastapi import FastAPI, Request, applications +from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html +from fastapi.openapi.utils import get_openapi +from fastapi.responses import HTMLResponse, JSONResponse + +from config.env import AppConfig + + +class APIDocsUtil: + """ + API文档工具类 + """ + + # API文档URLs + _OPENAPI_URL = '/openapi.json' + _PROXY_OPENAPI_URL = '/proxy-openapi.json' + _DOCS_URL = '/docs' + _PROXY_DOCS_URL = '/proxy-docs' + _REDOC_URL = '/redoc' + _PROXY_REDOC_URL = '/proxy-redoc' + _OAUTH2_REDIRECT_URL = '/docs/oauth2-redirect' + _PROXY_OAUTH2_REDIRECT_URL = '/proxy-docs/oauth2-redirect' + + # 文档静态资源URLs + DEFAULT_REDOC_JS_URL = 'https://registry.npmmirror.com/redoc/2/files/bundles/redoc.standalone.js' + DEFAULT_REDOC_FAVICON_URL = 'https://fastapi.tiangolo.com/img/favicon.png' + DEFAULT_SWAGGER_JS_URL = 'https://registry.npmmirror.com/swagger-ui-dist/5/files/swagger-ui-bundle.js' + DEFAULT_SWAGGER_CSS_URL = 'https://registry.npmmirror.com/swagger-ui-dist/5/files/swagger-ui.css' + DEFAULT_SWAGGER_FAVICON_URL = 'https://fastapi.tiangolo.com/img/favicon.png' + + @classmethod + def proxy_openapi_url(cls) -> str: + """ + 代理OpenAPI文档URL + """ + return cls._PROXY_OPENAPI_URL if not AppConfig.app_disable_swagger and not AppConfig.app_disable_redoc else None + + @classmethod + def docs_url(cls) -> str: + """ + 文档URL + """ + return cls._DOCS_URL + + @classmethod + def proxy_docs_url(cls) -> str: + """ + 代理文档URL + """ + return cls._PROXY_DOCS_URL if not AppConfig.app_disable_swagger else None + + @classmethod + def redoc_url(cls) -> str: + """ + ReDoc文档URL + """ + return cls._REDOC_URL + + @classmethod + def proxy_redoc_url(cls) -> str: + """ + 代理ReDoc文档URL + """ + return cls._PROXY_REDOC_URL if not AppConfig.app_disable_redoc else None + + @classmethod + def proxy_oauth2_redirect_url(cls) -> str: + """ + 代理OAuth2重定向URL + """ + return cls._PROXY_OAUTH2_REDIRECT_URL if not AppConfig.app_disable_swagger else None + + @classmethod + def setup_docs_static_resources( + cls, + redoc_js_url: str = DEFAULT_REDOC_JS_URL, + redoc_favicon_url: str = DEFAULT_REDOC_FAVICON_URL, + swagger_js_url: str = DEFAULT_SWAGGER_JS_URL, + swagger_css_url: str = DEFAULT_SWAGGER_CSS_URL, + swagger_favicon_url: str = DEFAULT_SWAGGER_FAVICON_URL, + ) -> None: + """ + 配置文档静态资源 + + :param redoc_js_url: 用于加载ReDoc JavaScript的URL + :param redoc_favicon_url: ReDoc要使用的favicon的URL + :param swagger_js_url: 用于加载Swagger UI JavaScript的URL + :param swagger_css_url: 用于加载Swagger UI CSS的URL + :param swagger_favicon_url: Swagger UI要使用的favicon的URL + :return: + """ + + def redoc_monkey_patch(*args, **kwargs) -> HTMLResponse: + return get_redoc_html( + *args, + **kwargs, + redoc_js_url=redoc_js_url, + redoc_favicon_url=redoc_favicon_url, + ) + + def swagger_ui_monkey_patch(*args, **kwargs) -> HTMLResponse: + return get_swagger_ui_html( + *args, + **kwargs, + swagger_js_url=swagger_js_url, + swagger_css_url=swagger_css_url, + swagger_favicon_url=swagger_favicon_url, + ) + + applications.get_redoc_html = redoc_monkey_patch + applications.get_swagger_ui_html = swagger_ui_monkey_patch + + @classmethod + def custom_api_docs_router( + cls, + app: FastAPI, + redoc_js_url: str = DEFAULT_REDOC_JS_URL, + redoc_favicon_url: str = DEFAULT_REDOC_FAVICON_URL, + swagger_js_url: str = DEFAULT_SWAGGER_JS_URL, + swagger_css_url: str = DEFAULT_SWAGGER_CSS_URL, + swagger_favicon_url: str = DEFAULT_SWAGGER_FAVICON_URL, + ) -> None: + """ + 自定义API文档路由 + + :param app: FastAPI对象 + :param redoc_js_url: 用于加载ReDoc JavaScript的URL + :param redoc_favicon_url: ReDoc要使用的favicon的URL + :param swagger_js_url: 用于加载Swagger UI JavaScript的URL + :param swagger_css_url: 用于加载Swagger UI CSS的URL + :param swagger_favicon_url: Swagger UI要使用的favicon的URL + :return: + """ + + async def custom_openapi(request: Request) -> JSONResponse: + return await cls._custom_openapi(app) + + async def custom_redoc(request: Request) -> HTMLResponse: + return await cls._custom_redoc(app, redoc_js_url, redoc_favicon_url) + + async def custom_swagger(request: Request) -> HTMLResponse: + return await cls._custom_swagger(app, swagger_js_url, swagger_css_url, swagger_favicon_url) + + async def custom_swagger_ui_redirect(request: Request) -> HTMLResponse: + return await cls._custom_swagger_ui_redirect(app, swagger_favicon_url) + + # 注册路由 + app.add_route(cls._OPENAPI_URL, custom_openapi, include_in_schema=False) + cls._register_docs_routes(app, custom_swagger, custom_swagger_ui_redirect, custom_redoc) + + @classmethod + async def _custom_openapi(cls, app: FastAPI) -> JSONResponse: + """ + 自定义 OpenAPI 路由处理函数 + + :param app: FastAPI对象 + :return: openapi的json响应 + """ + openapi_schema = get_openapi( + title=app.title, + version=app.version, + openapi_version=app.openapi_version, + summary=app.summary, + description=app.description, + terms_of_service=app.terms_of_service, + contact=app.contact, + license_info=app.license_info, + routes=app.routes, + webhooks=app.webhooks.routes, + tags=app.openapi_tags, + separate_input_output_schemas=app.separate_input_output_schemas, + external_docs=app.openapi_external_docs, + ) + return JSONResponse(openapi_schema) + + @classmethod + async def _custom_redoc( + cls, + app: FastAPI, + redoc_js_url: str, + redoc_favicon_url: str, + ) -> HTMLResponse: + """ + 自定义 ReDoc 路由处理函数 + + :param app: FastAPI对象 + :param redoc_js_url: 用于加载ReDoc JavaScript的URL + :param redoc_favicon_url: ReDoc要使用的favicon的URL + :return: ReDoc HTML响应 + """ + if not AppConfig.app_disable_redoc: + return get_redoc_html( + openapi_url=cls._OPENAPI_URL, + title=f'{app.title} - ReDoc', + redoc_js_url=redoc_js_url, + redoc_favicon_url=redoc_favicon_url, + ) + return cls._get_disabled_html_content( + f'{app.title} - ReDoc', + 'ReDoc', + redoc_favicon_url, + ) + + @classmethod + async def _custom_swagger( + cls, + app: FastAPI, + swagger_js_url: str, + swagger_css_url: str, + swagger_favicon_url: str, + ) -> HTMLResponse: + """ + 自定义 Swagger UI 路由处理函数 + + :param app: FastAPI对象 + :param swagger_js_url: 用于加载Swagger UI JavaScript的URL + :param swagger_css_url: 用于加载Swagger UI CSS的URL + :param swagger_favicon_url: Swagger UI要使用的favicon的URL + :return: Swagger UI HTML响应 + """ + if not AppConfig.app_disable_swagger: + return get_swagger_ui_html( + openapi_url=cls._OPENAPI_URL, + title=f'{app.title} - Swagger UI', + swagger_js_url=swagger_js_url, + swagger_css_url=swagger_css_url, + swagger_favicon_url=swagger_favicon_url, + oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, + init_oauth=app.swagger_ui_init_oauth, + swagger_ui_parameters=app.swagger_ui_parameters, + ) + return cls._get_disabled_html_content( + f'{app.title} - Swagger UI', + 'Swagger UI', + swagger_favicon_url, + ) + + @classmethod + async def _custom_swagger_ui_redirect( + cls, + app: FastAPI, + swagger_favicon_url: str, + ) -> HTMLResponse: + """ + 自定义 Swagger UI OAuth2 重定向路由处理函数 + + :param app: FastAPI对象 + :param swagger_favicon_url: Swagger UI要使用的favicon的URL + :return: Swagger UI OAuth2重定向HTML响应 + """ + if not AppConfig.app_disable_swagger: + return get_swagger_ui_oauth2_redirect_html() + return cls._get_disabled_html_content( + f'{app.title} - Swagger UI OAuth2 Redirect', + 'Swagger UI OAuth2 Redirect', + swagger_favicon_url, + ) + + @staticmethod + def _get_disabled_html_content(title: str, name: str, favicon_url: str) -> HTMLResponse: + """ + 生成禁用文档的HTML内容 + + :param title: 页面标题 + :param name: 文档名称 + :param favicon_url: 图标地址 + :return: 禁用文档HTML响应 + """ + html = f""" + + + + {title} + + + + + + + + + +

{name} has been disabled. Please enable it first.

+ + + """ + return HTMLResponse(html) + + @classmethod + def _register_docs_routes( + cls, app: FastAPI, swagger_handler: Callable, redirect_handler: Callable, redoc_handler: Callable + ) -> None: + """ + 注册文档路由 + + :param app: FastAPI对象 + :param swagger_handler: Swagger UI 路由处理函数 + :param redirect_handler: Swagger UI OAuth2 重定向路由处理函数 + :param redoc_handler: ReDoc 路由处理函数 + :return: + """ + swagger_urls: list[str] = ( + [cls._DOCS_URL] if not AppConfig.app_disable_swagger else [cls._DOCS_URL, cls._PROXY_DOCS_URL] + ) + swagger_redirect_urls: list[str] = ( + [cls._OAUTH2_REDIRECT_URL] + if not AppConfig.app_disable_swagger + else [cls._OAUTH2_REDIRECT_URL, cls._PROXY_OAUTH2_REDIRECT_URL] + ) + redoc_urls: list[str] = ( + [cls._REDOC_URL] if not AppConfig.app_disable_redoc else [cls._REDOC_URL, cls._PROXY_REDOC_URL] + ) + + for url in swagger_urls: + app.add_route(url, swagger_handler, include_in_schema=False) + for url in swagger_redirect_urls: + app.add_route(url, redirect_handler, include_in_schema=False) + for url in redoc_urls: + app.add_route(url, redoc_handler, include_in_schema=False) + + +class IPUtil: + """ + IP工具类 + """ + + @classmethod + def get_local_ip(cls) -> str: + """ + 获取本机Local IP + """ + try: + for snics in psutil.net_if_addrs().values(): + for snic in snics: + if snic.family == socket.AF_INET and snic.address.startswith('127.'): + return snic.address + except Exception: + pass + + return '127.0.0.1' + + @classmethod + def get_network_ips(cls) -> list[str]: + """ + 获取本机Network IP列表 + """ + network_ips = [] + try: + # 获取网卡状态 + stats = psutil.net_if_stats() + for name, snics in psutil.net_if_addrs().items(): + # 过滤掉状态为DOWN的网卡 + if name in stats and not stats[name].isup: + continue + + for snic in snics: + if snic.family == socket.AF_INET: + try: + ip_obj = ipaddress.ip_address(snic.address) + if ip_obj.is_loopback or ip_obj.is_link_local: + continue + network_ips.append(snic.address) + except ValueError: + continue + except Exception: + pass + + # 优先显示首选出站IP + preferred_ip = None + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(('8.8.8.8', 80)) + preferred_ip = s.getsockname()[0] + except Exception: + pass + + if preferred_ip: + if preferred_ip in network_ips: + network_ips.remove(preferred_ip) + network_ips.insert(0, preferred_ip) + + if not network_ips: + network_ips = ['127.0.0.1'] + + return network_ips diff --git a/ruoyi-fastapi-frontend/src/views/tool/swagger/index.vue b/ruoyi-fastapi-frontend/src/views/tool/swagger/index.vue index fc94340..6b29a98 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/swagger/index.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/swagger/index.vue @@ -8,7 +8,7 @@ export default { components: { iFrame }, data() { return { - url: process.env.VUE_APP_BASE_API + "/docs" + url: process.env.VUE_APP_BASE_API + "/proxy-docs" }; }, };