diff --git a/README.md b/README.md index a678795..ea9949d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Django-Vue-Admin -[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/pypi/v/django-simpleui.svg)](https://pypi.org/project/django-simpleui/#history) [![img](https://img.shields.io/badge/python-%3E=3.6.x-green.svg)](https://python.org/) ![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-2.2-blue)[![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/download/releases/)[![img](https://img.shields.io/pypi/dm/django-simpleui.svg)](https://pypi.org/project/django-simpleui/) +[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/pypi/v/django-simpleui.svg)](https://pypi.org/project/django-simpleui/#history) [![img](https://img.shields.io/badge/python-%3E=3.6.x-green.svg)](https://python.org/) ![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-2.2-blue)![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen) @@ -68,9 +68,6 @@ git clone https://gitee.com/liqianglog/django-vue-admin.git cd dvadmin-ui # 安装依赖 -npm install - -# 建议不要直接使用cnpm安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题。 npm install --registry=https://registry.npm.taobao.org # 启动服务 @@ -95,7 +92,12 @@ npm run build:prod ~~~bash 1. 进入项目目录 cd dvadmin-backend 2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py + 3. 在 env.py 中配置数据库信息 + mysql数据库版本建议:5.7以上 + mysql数据库字符集:utf8mb4 + mysql数据库排序规则:utf8mb4_0900_ai_ci + 4. 安装依赖环境 pip3 install -r requirements.txt 5. 执行迁移命令: @@ -104,10 +106,13 @@ npm run build:prod 6. 初始化数据 python3 manage.py init 7. 启动项目 - python3 manage.py runserver 0.0.0.0:8000 + python3 manage.py runserver 127.0.0.1:8000 定时任务启动命令: celery -A application worker -B --loglevel=info +注: + Windows 运行celery 需要安装 pip install eventlet + celery -A application worker -P eventlet --loglevel=info 初始账号:admin 密码:123456 diff --git a/dvadmin-backend/application/settings.py b/dvadmin-backend/application/settings.py index 387f062..ee3fc5b 100644 --- a/dvadmin-backend/application/settings.py +++ b/dvadmin-backend/application/settings.py @@ -21,7 +21,7 @@ from conf.env import * # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0,os.path.join(BASE_DIR,'apps')) +sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ @@ -54,6 +54,7 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ + 'vadmin.op_drf.middleware.PermissionModeMiddleware', # 权限中间件 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -202,7 +203,7 @@ LOGGING = { 'loggers': { # default日志 '': { - 'handlers': ['console','error','file'], + 'handlers': ['console', 'error', 'file'], 'level': 'INFO', }, # 数据库相关日志 @@ -300,19 +301,19 @@ USERNAME_FIELD = 'username' # ************** 登录验证码配置 ************** # # ================================================= # CAPTCHA_STATE = CAPTCHA_STATE -#字母验证码 -CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小 -CAPTCHA_LENGTH = 4 # 字符个数 -CAPTCHA_TIMEOUT = 1 # 超时(minutes) -#加减乘除验证码 +# 字母验证码 +CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小 +CAPTCHA_LENGTH = 4 # 字符个数 +CAPTCHA_TIMEOUT = 1 # 超时(minutes) +# 加减乘除验证码 CAPTCHA_OUTPUT_FORMAT = '%(image)s %(text_field)s %(hidden_field)s ' -CAPTCHA_FONT_SIZE = 40 # 字体大小 +CAPTCHA_FONT_SIZE = 40 # 字体大小 CAPTCHA_FOREGROUND_COLOR = '#0033FF' # 前景色 CAPTCHA_BACKGROUND_COLOR = '#F5F7F4' # 背景色 CAPTCHA_NOISE_FUNCTIONS = ( - # 'captcha.helpers.noise_arcs', # 线 - # 'captcha.helpers.noise_dots', # 点 - ) + # 'captcha.helpers.noise_arcs', # 线 + # 'captcha.helpers.noise_dots', # 点 +) # CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge' @@ -320,5 +321,10 @@ API_LOG_ENABLE = True # API_LOG_METHODS = 'ALL' # ['POST', 'DELETE'] # API_LOG_METHODS = ['POST', 'DELETE'] # ['POST', 'DELETE'] BROKER_URL = f'redis://:{REDIS_PASSWORD if REDIS_PASSWORD else ""}@{os.getenv("REDIS_HOST") or REDIS_HOST}:' \ - f'{REDIS_PORT}/{locals().get("CELERY_DB",2)}' #Broker使用Redis -CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' #Backend数据库 + f'{REDIS_PORT}/{locals().get("CELERY_DB", 2)}' # Broker使用Redis +CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' # Backend数据库 +# ================================================= # +# ************** 其他配置 ************** # +# ================================================= # +# 接口权限 +INTERFACE_PERMISSION = {locals().get("INTERFACE_PERMISSION", False)} diff --git a/dvadmin-backend/application/urls.py b/dvadmin-backend/application/urls.py index 9ad09e0..1bd1ed6 100644 --- a/dvadmin-backend/application/urls.py +++ b/dvadmin-backend/application/urls.py @@ -22,7 +22,7 @@ from django.urls import re_path, include from django.views.static import serve from rest_framework.views import APIView -from apps.vadmin.op_drf.response import SuccessResponse +from vadmin.utils.response import SuccessResponse class CaptchaRefresh(APIView): diff --git a/dvadmin-backend/apps/vadmin/op_drf/middleware.py b/dvadmin-backend/apps/vadmin/op_drf/middleware.py index ad3f98d..bf28947 100644 --- a/dvadmin-backend/apps/vadmin/op_drf/middleware.py +++ b/dvadmin-backend/apps/vadmin/op_drf/middleware.py @@ -1,14 +1,20 @@ """ django中间件 """ +import logging +import os from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.utils.deprecation import MiddlewareMixin +from apps.vadmin.permission.models import Menu from apps.vadmin.system.models import OperationLog from ..utils.request_util import get_request_ip, get_request_data, get_request_path, get_browser, get_os, \ - get_login_location + get_login_location, get_request_canonical_path, get_request_user +from ..utils.response import ErrorJsonResponse + +logger = logging.getLogger(__name__) class ApiLoggingMiddleware(MiddlewareMixin): @@ -77,3 +83,78 @@ class PermissionModeMiddleware(MiddlewareMixin): """ 权限模式拦截判断 """ + + def process_request(self, request): + """ + 判断环境变量中,是否为演示模式(正常可忽略此判断) + :param request: + :return: + """ + white_list = ['/admin/logout/', '/admin/login/'] + if os.getenv('DEMO_ENV') and not request.method == 'GET' and request.path not in white_list: + return ErrorJsonResponse(data={}, msg=f'演示模式,不允许操作!') + + def has_interface_permission(self, request, method, view_path, user=None): + """ + 接口权限验证,优先级: + (1)接口是否接入权限管理, 是:继续; 否:通过 + (2)认证的user是否superuser, 是:通过; 否:继续 + (3)user的角色有该接口权限, 是:通过, 否:不通过 + + auth_code含义: auth_code >=0, 表示接口认证通过; auth_code < 0, 表示无接口访问权限, 具体含义如下 + -1: + -10: 该请求已认证的用户没有这个接口的访问权限 + 0: + 1: 白名单 + 10: 该接口没有录入权限系统, 放行 请求中认证的用户为超级管理员, 直接放行 + 20: 请求中认证的用户是superuser放行 + 30: 请求中认证的用户对应的角色中,某个角色包含了该接口的访问权限, 放行 + 1. 先获取所有录入系统的接口 + 2 判断此用户是否为 superuser + 3. 获取此用户所请求的接口 + 4. 获取此用户关联角色所有有权限的接口 + + :param interface: 接口模型 + :param path: 接口路径 + :param method: 请求方法 + :param project: 接口所属项目 + :param args: + :param kwargs: + :return: + """ + interface_dict = Menu.get_interface_dict() + # (1) 接口是否接入权限管理, 是:继续; 否:通过 + if not view_path in interface_dict.get(method, []): + return 10 + # (2)认证的user是否superuser, 是:通过; 否:继续 + if user.is_superuser or (hasattr(user, 'role') and user.role.filter(status='1', admin=True).count()): + return 20 + # (3)user的角色有该接口权限, 是:通过, 否:不通过 + if view_path in user.get_user_interface_dict: + return 30 + return -10 + + def process_view(self, request, view_func, view_args, view_kwargs): + if not settings.INTERFACE_PERMISSION: + return + user = get_request_user(request) + + if user and not isinstance(user, AnonymousUser): + method = request.method.upper() + if method == 'GET': # GET 不设置接口权限 + return + view_path = get_request_canonical_path(request, *view_args, **view_kwargs) + auth_code = self.has_interface_permission(request, method, view_path, user) + logger.info(f"[{user.username}] {method}:{view_path}, 权限认证:{auth_code}") + if auth_code >= 0: + return + return ErrorJsonResponse(data={}, msg=f'无接口访问权限!') + + def process_response(self, request, response): + """ + 主要请求处理完之后记录 + :param request: + :param response: + :return: + """ + return response diff --git a/dvadmin-backend/apps/vadmin/permission/models/menu.py b/dvadmin-backend/apps/vadmin/permission/models/menu.py index 67788e9..acee146 100644 --- a/dvadmin-backend/apps/vadmin/permission/models/menu.py +++ b/dvadmin-backend/apps/vadmin/permission/models/menu.py @@ -1,4 +1,5 @@ -from django.db.models import IntegerField, ForeignKey, CharField, CASCADE +from django.core.cache import cache +from django.db.models import IntegerField, ForeignKey, CharField, CASCADE, Q from ...op_drf.models import CoreModel @@ -34,6 +35,31 @@ class Menu(CoreModel): visible = CharField(max_length=8, verbose_name="显示状态") isCache = CharField(max_length=8, verbose_name="是否缓存") + @classmethod + def get_interface_dict(cls): + """ + 获取所有接口列表 + :return: + """ + interface_dict = cache.get('permission_interface_dict', {}) + if not interface_dict: + for ele in Menu.objects.filter(~Q(interface_path=''), ~Q(interface_path=None), status='1', ).values( + 'interface_path', 'interface_method'): + if ele.get('interface_method') in interface_dict: + interface_dict[ele.get('interface_method', '')].append(ele.get('interface_path')) + else: + interface_dict[ele.get('interface_method', '')] = [ele.get('interface_path')] + cache.set('permission_interface_dict', interface_dict, 84600) + return interface_dict + + @classmethod + def delete_cache(cls): + """ + 清空缓存中的接口列表 + :return: + """ + cache.delete('permission_interface_dict') + class Meta: verbose_name = '菜单管理' verbose_name_plural = verbose_name diff --git a/dvadmin-backend/apps/vadmin/permission/models/users.py b/dvadmin-backend/apps/vadmin/permission/models/users.py index 822577c..2fead2f 100644 --- a/dvadmin-backend/apps/vadmin/permission/models/users.py +++ b/dvadmin-backend/apps/vadmin/permission/models/users.py @@ -1,6 +1,7 @@ from uuid import uuid4 from django.contrib.auth.models import UserManager, AbstractUser +from django.core.cache import cache from django.db.models import IntegerField, ForeignKey, CharField, TextField, ManyToManyField, CASCADE from ...op_drf.fields import CreateDateTimeField, UpdateDateTimeField @@ -28,6 +29,29 @@ class UserProfile(AbstractUser): create_datetime = CreateDateTimeField() update_datetime = UpdateDateTimeField() + @property + def get_user_interface_dict(self): + interface_dict = cache.get(f'permission_interface_dict{self.username}', {}) + if not interface_dict: + for ele in self.role.filter(status='1', menu__status='1').values('menu__interface_path', + 'menu__interface_method').distinct(): + interface_path = ele.get('menu__interface_path') + if interface_path is None or interface_path == '': + continue + if ele.get('menu__interface_method') in interface_dict: + interface_dict[ele.get('menu__interface_method', '')].append(interface_path) + else: + interface_dict[ele.get('menu__interface_method', '')] = [interface_path] + cache.set(f'permission_interface_dict_{self.username}', interface_dict, 84600) + return interface_dict + + @property + def delete_cache(self): + """ + 清空缓存中的接口列表 + :return: + """ + return cache.delete(f'permission_interface_dict_{self.username}') class Meta: verbose_name = '用户管理' verbose_name_plural = verbose_name diff --git a/dvadmin-backend/apps/vadmin/permission/permissions.py b/dvadmin-backend/apps/vadmin/permission/permissions.py index 7bde60e..0186adb 100644 --- a/dvadmin-backend/apps/vadmin/permission/permissions.py +++ b/dvadmin-backend/apps/vadmin/permission/permissions.py @@ -87,11 +87,9 @@ class CommonPermission(CustomPermission): return int(instance.dept_belong_id) in list(set(dept_list)) def has_permission(self, request: Request, view: APIView): - """判断是否为演示模式""" return True def has_object_permission(self, request: Request, view: APIView, instance): self.message = f"没有此数据操作权限!" res = self.check_queryset(request, instance) - print(res) return res diff --git a/dvadmin-backend/apps/vadmin/permission/serializers.py b/dvadmin-backend/apps/vadmin/permission/serializers.py index 64648a1..f9bfaf3 100644 --- a/dvadmin-backend/apps/vadmin/permission/serializers.py +++ b/dvadmin-backend/apps/vadmin/permission/serializers.py @@ -37,6 +37,10 @@ class MenuCreateUpdateSerializer(CustomModelSerializer): # raise APIException(message=f'仅Manger能创建/更新角色为公共角色') return super().validate(attrs) + def save(self, **kwargs): + Menu.delete_cache() + return super().save(**kwargs) + class Meta: model = Menu fields = '__all__' @@ -91,7 +95,7 @@ class DeptTreeSerializer(serializers.ModelSerializer): class Meta: model = Dept - fields = ('id', 'label', 'parentId','status') + fields = ('id', 'label', 'parentId', 'status') # ================================================= # diff --git a/dvadmin-backend/apps/vadmin/permission/views.py b/dvadmin-backend/apps/vadmin/permission/views.py index 02374fa..bec3b8e 100644 --- a/dvadmin-backend/apps/vadmin/permission/views.py +++ b/dvadmin-backend/apps/vadmin/permission/views.py @@ -25,6 +25,7 @@ class GetUserProfileView(APIView): user_dict = UserProfileSerializer(request.user).data permissions_list = ['*:*:*'] if user_dict.get('admin') else Menu.objects.filter( role__userprofile=request.user).values_list('perms', flat=True) + delete_cache = request.user.delete_cache return SuccessResponse({ 'permissions': [ele for ele in permissions_list if ele], 'roles': Role.objects.filter(userprofile=request.user).values_list('roleKey', flat=True), diff --git a/dvadmin-backend/apps/vadmin/urls.py b/dvadmin-backend/apps/vadmin/urls.py index 093e65d..cf4051d 100644 --- a/dvadmin-backend/apps/vadmin/urls.py +++ b/dvadmin-backend/apps/vadmin/urls.py @@ -22,9 +22,9 @@ from django.urls import re_path, include from rest_framework.documentation import include_docs_urls from rest_framework.views import APIView -from .op_drf.response import SuccessResponse from .permission.views import GetUserProfileView, GetRouters from .utils.login import LoginView, LogoutView +from .utils.response import SuccessResponse class CaptchaRefresh(APIView): diff --git a/dvadmin-backend/apps/vadmin/utils/request_util.py b/dvadmin-backend/apps/vadmin/utils/request_util.py index f1c6897..6952cf0 100644 --- a/dvadmin-backend/apps/vadmin/utils/request_util.py +++ b/dvadmin-backend/apps/vadmin/utils/request_util.py @@ -8,10 +8,10 @@ from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import AnonymousUser from django.core.cache import cache from django.urls.resolvers import ResolverMatch -from rest_framework.authentication import BaseAuthentication -from rest_framework.settings import api_settings as drf_settings from user_agents import parse +from apps.vadmin.utils.authentication import OpAuthJwtAuthentication + logger = logging.getLogger(__name__) @@ -26,18 +26,8 @@ def get_request_user(request, authenticate=True): user: AbstractBaseUser = getattr(request, 'user', None) if user and user.is_authenticated: return user - authentication: BaseAuthentication = None - for authentication_class in drf_settings.DEFAULT_AUTHENTICATION_CLASSES: - try: - authentication = authentication_class() - user_auth_tuple = authentication.authenticate(request) - if user_auth_tuple is not None: - user, token = user_auth_tuple - if authenticate: - request.user = user - return user - except Exception: - pass + user, tokrn = OpAuthJwtAuthentication().authenticate(request) + print(22, user) return user or AnonymousUser() @@ -127,9 +117,11 @@ def get_request_canonical_path(request, *args, **kwargs): for value in resolver_match.args: path = path.replace(f"/{value}", "/{id}") for key, value in resolver_match.kwargs.items(): - path = path.replace(f"/{value}", f"/{{{key}}}") if key == 'pk': - pass + path = path.replace(f"/{value}", f"/{{id}}") + continue + path = path.replace(f"/{value}", f"/{{{key}}}") + return path diff --git a/dvadmin-backend/apps/vadmin/utils/response.py b/dvadmin-backend/apps/vadmin/utils/response.py index 4a0bbe7..ccfb35d 100644 --- a/dvadmin-backend/apps/vadmin/utils/response.py +++ b/dvadmin-backend/apps/vadmin/utils/response.py @@ -1,7 +1,7 @@ """ 常用的Response以及Django的Response、DRF的Response """ -from django.http.response import DjangoJSONEncoder +from django.http.response import DjangoJSONEncoder, JsonResponse from rest_framework.response import Response @@ -56,3 +56,36 @@ class ErrorResponse(Response): def __str__(self): return str(self.std_data) + + +class SuccessJsonResponse(JsonResponse): + """ + 标准JsonResponse, SuccessJsonResponse(data)SuccessJsonResponse(data=data) + (1)仅SuccessResponse无法使用时才能推荐使用SuccessJsonResponse + """ + + def __init__(self, data, msg='success', encoder=DjangoJSONEncoder, safe=True, json_dumps_params=None, **kwargs): + std_data = { + "code": 200, + "data": data, + "msg": msg, + "status": 'success' + } + super().__init__(std_data, encoder, safe, json_dumps_params, **kwargs) + + +class ErrorJsonResponse(JsonResponse): + """ + 标准JsonResponse, 仅ErrorResponse无法使用时才能使用ErrorJsonResponse + (1)默认错误码返回2001, 也可以指定其他返回码:ErrorJsonResponse(code=xxx) + """ + + def __init__(self, data, msg='error', code=201, encoder=OpDRFJSONEncoder, safe=True, json_dumps_params=None, + **kwargs): + std_data = { + "code": code, + "data": data, + "msg": msg, + "status": 'error' + } + super().__init__(std_data, encoder, safe, json_dumps_params, **kwargs) diff --git a/dvadmin-backend/conf/env.example.py b/dvadmin-backend/conf/env.example.py index 38e41b1..be45875 100644 --- a/dvadmin-backend/conf/env.example.py +++ b/dvadmin-backend/conf/env.example.py @@ -39,3 +39,5 @@ CAPTCHA_STATE = True # 操作日志配置 API_LOG_ENABLE = True API_LOG_METHODS = ['POST', 'DELETE', 'PUT'] # 'ALL' or ['POST', 'DELETE'] +# 接口权限 +INTERFACE_PERMISSION = True diff --git a/dvadmin-ui/src/components/FileUpload/index.vue b/dvadmin-ui/src/components/FileUpload/index.vue index 8447bca..32bce09 100755 --- a/dvadmin-ui/src/components/FileUpload/index.vue +++ b/dvadmin-ui/src/components/FileUpload/index.vue @@ -39,9 +39,9 @@