system消息通知:修复已读消息bug;permission:接口权限完成。

pull/4/head
李强 2021-03-29 01:52:46 +08:00
parent c9a34da1a5
commit 2514d98173
14 changed files with 222 additions and 46 deletions

View File

@ -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

View File

@ -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,11 +301,11 @@ USERNAME_FIELD = 'username'
# ************** 登录验证码配置 ************** #
# ================================================= #
CAPTCHA_STATE = CAPTCHA_STATE
#字母验证码
# 字母验证码
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_FOREGROUND_COLOR = '#0033FF' # 前景色
@ -312,7 +313,7 @@ CAPTCHA_BACKGROUND_COLOR = '#F5F7F4' # 背景色
CAPTCHA_NOISE_FUNCTIONS = (
# '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)}

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')
# ================================================= #

View File

@ -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),

View File

@ -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):

View File

@ -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

View File

@ -1,7 +1,7 @@
"""
常用的Response以及Django的ResponseDRF的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)

View File

@ -39,3 +39,5 @@ CAPTCHA_STATE = True
# 操作日志配置
API_LOG_ENABLE = True
API_LOG_METHODS = ['POST', 'DELETE', 'PUT'] # 'ALL' or ['POST', 'DELETE']
# 接口权限
INTERFACE_PERMISSION = True

View File

@ -39,9 +39,9 @@
</template>
<script>
import { getToken } from "@/utils/auth";
import {getToken} from "@/utils/auth";
export default {
export default {
props: {
//
value: [String, Object, Array],
@ -135,8 +135,12 @@ export default {
},
//
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.$message.success("上传成功");
this.$emit("input", res.url);
this.$emit("input", res.data.file);
} else {
this.$message.error(res.msg);
}
},
//
handleDelete(index) {