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 # 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 cd dvadmin-ui
# 安装依赖 # 安装依赖
npm install
# 建议不要直接使用cnpm安装依赖会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题。
npm install --registry=https://registry.npm.taobao.org npm install --registry=https://registry.npm.taobao.org
# 启动服务 # 启动服务
@ -95,7 +92,12 @@ npm run build:prod
~~~bash ~~~bash
1. 进入项目目录 cd dvadmin-backend 1. 进入项目目录 cd dvadmin-backend
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py 2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
3. 在 env.py 中配置数据库信息 3. 在 env.py 中配置数据库信息
mysql数据库版本建议:5.7以上
mysql数据库字符集utf8mb4
mysql数据库排序规则utf8mb4_0900_ai_ci
4. 安装依赖环境 4. 安装依赖环境
pip3 install -r requirements.txt pip3 install -r requirements.txt
5. 执行迁移命令: 5. 执行迁移命令:
@ -104,10 +106,13 @@ npm run build:prod
6. 初始化数据 6. 初始化数据
python3 manage.py init python3 manage.py init
7. 启动项目 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 celery -A application worker -B --loglevel=info
注:
Windows 运行celery 需要安装 pip install eventlet
celery -A application worker -P eventlet --loglevel=info
初始账号admin 密码123456 初始账号admin 密码123456

View File

@ -21,7 +21,7 @@ from conf.env import *
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 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 # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
@ -54,6 +54,7 @@ INSTALLED_APPS = [
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'vadmin.op_drf.middleware.PermissionModeMiddleware', # 权限中间件
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@ -202,7 +203,7 @@ LOGGING = {
'loggers': { 'loggers': {
# default日志 # default日志
'': { '': {
'handlers': ['console','error','file'], 'handlers': ['console', 'error', 'file'],
'level': 'INFO', 'level': 'INFO',
}, },
# 数据库相关日志 # 数据库相关日志
@ -300,11 +301,11 @@ USERNAME_FIELD = 'username'
# ************** 登录验证码配置 ************** # # ************** 登录验证码配置 ************** #
# ================================================= # # ================================================= #
CAPTCHA_STATE = CAPTCHA_STATE CAPTCHA_STATE = CAPTCHA_STATE
#字母验证码 # 字母验证码
CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小 CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小
CAPTCHA_LENGTH = 4 # 字符个数 CAPTCHA_LENGTH = 4 # 字符个数
CAPTCHA_TIMEOUT = 1 # 超时(minutes) CAPTCHA_TIMEOUT = 1 # 超时(minutes)
#加减乘除验证码 # 加减乘除验证码
CAPTCHA_OUTPUT_FORMAT = '%(image)s %(text_field)s %(hidden_field)s ' CAPTCHA_OUTPUT_FORMAT = '%(image)s %(text_field)s %(hidden_field)s '
CAPTCHA_FONT_SIZE = 40 # 字体大小 CAPTCHA_FONT_SIZE = 40 # 字体大小
CAPTCHA_FOREGROUND_COLOR = '#0033FF' # 前景色 CAPTCHA_FOREGROUND_COLOR = '#0033FF' # 前景色
@ -312,7 +313,7 @@ CAPTCHA_BACKGROUND_COLOR = '#F5F7F4' # 背景色
CAPTCHA_NOISE_FUNCTIONS = ( CAPTCHA_NOISE_FUNCTIONS = (
# 'captcha.helpers.noise_arcs', # 线 # 'captcha.helpers.noise_arcs', # 线
# 'captcha.helpers.noise_dots', # 点 # 'captcha.helpers.noise_dots', # 点
) )
# CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge'
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_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 = 'ALL' # ['POST', 'DELETE']
# API_LOG_METHODS = ['POST', 'DELETE'] # ['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}:' \ 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 f'{REDIS_PORT}/{locals().get("CELERY_DB", 2)}' # Broker使用Redis
CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' #Backend数据库 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 django.views.static import serve
from rest_framework.views import APIView from rest_framework.views import APIView
from apps.vadmin.op_drf.response import SuccessResponse from vadmin.utils.response import SuccessResponse
class CaptchaRefresh(APIView): class CaptchaRefresh(APIView):

View File

@ -1,14 +1,20 @@
""" """
django中间件 django中间件
""" """
import logging
import os
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from apps.vadmin.permission.models import Menu
from apps.vadmin.system.models import OperationLog 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, \ 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): 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 from ...op_drf.models import CoreModel
@ -34,6 +35,31 @@ class Menu(CoreModel):
visible = CharField(max_length=8, verbose_name="显示状态") visible = CharField(max_length=8, verbose_name="显示状态")
isCache = 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: class Meta:
verbose_name = '菜单管理' verbose_name = '菜单管理'
verbose_name_plural = verbose_name verbose_name_plural = verbose_name

View File

@ -1,6 +1,7 @@
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import UserManager, AbstractUser 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 django.db.models import IntegerField, ForeignKey, CharField, TextField, ManyToManyField, CASCADE
from ...op_drf.fields import CreateDateTimeField, UpdateDateTimeField from ...op_drf.fields import CreateDateTimeField, UpdateDateTimeField
@ -28,6 +29,29 @@ class UserProfile(AbstractUser):
create_datetime = CreateDateTimeField() create_datetime = CreateDateTimeField()
update_datetime = UpdateDateTimeField() 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: class Meta:
verbose_name = '用户管理' verbose_name = '用户管理'
verbose_name_plural = 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)) return int(instance.dept_belong_id) in list(set(dept_list))
def has_permission(self, request: Request, view: APIView): def has_permission(self, request: Request, view: APIView):
"""判断是否为演示模式"""
return True return True
def has_object_permission(self, request: Request, view: APIView, instance): def has_object_permission(self, request: Request, view: APIView, instance):
self.message = f"没有此数据操作权限!" self.message = f"没有此数据操作权限!"
res = self.check_queryset(request, instance) res = self.check_queryset(request, instance)
print(res)
return res return res

View File

@ -37,6 +37,10 @@ class MenuCreateUpdateSerializer(CustomModelSerializer):
# raise APIException(message=f'仅Manger能创建/更新角色为公共角色') # raise APIException(message=f'仅Manger能创建/更新角色为公共角色')
return super().validate(attrs) return super().validate(attrs)
def save(self, **kwargs):
Menu.delete_cache()
return super().save(**kwargs)
class Meta: class Meta:
model = Menu model = Menu
fields = '__all__' fields = '__all__'
@ -91,7 +95,7 @@ class DeptTreeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Dept 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 user_dict = UserProfileSerializer(request.user).data
permissions_list = ['*:*:*'] if user_dict.get('admin') else Menu.objects.filter( permissions_list = ['*:*:*'] if user_dict.get('admin') else Menu.objects.filter(
role__userprofile=request.user).values_list('perms', flat=True) role__userprofile=request.user).values_list('perms', flat=True)
delete_cache = request.user.delete_cache
return SuccessResponse({ return SuccessResponse({
'permissions': [ele for ele in permissions_list if ele], 'permissions': [ele for ele in permissions_list if ele],
'roles': Role.objects.filter(userprofile=request.user).values_list('roleKey', flat=True), '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.documentation import include_docs_urls
from rest_framework.views import APIView from rest_framework.views import APIView
from .op_drf.response import SuccessResponse
from .permission.views import GetUserProfileView, GetRouters from .permission.views import GetUserProfileView, GetRouters
from .utils.login import LoginView, LogoutView from .utils.login import LoginView, LogoutView
from .utils.response import SuccessResponse
class CaptchaRefresh(APIView): 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.contrib.auth.models import AnonymousUser
from django.core.cache import cache from django.core.cache import cache
from django.urls.resolvers import ResolverMatch 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 user_agents import parse
from apps.vadmin.utils.authentication import OpAuthJwtAuthentication
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,18 +26,8 @@ def get_request_user(request, authenticate=True):
user: AbstractBaseUser = getattr(request, 'user', None) user: AbstractBaseUser = getattr(request, 'user', None)
if user and user.is_authenticated: if user and user.is_authenticated:
return user return user
authentication: BaseAuthentication = None user, tokrn = OpAuthJwtAuthentication().authenticate(request)
for authentication_class in drf_settings.DEFAULT_AUTHENTICATION_CLASSES: print(22, user)
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
return user or AnonymousUser() return user or AnonymousUser()
@ -127,9 +117,11 @@ def get_request_canonical_path(request, *args, **kwargs):
for value in resolver_match.args: for value in resolver_match.args:
path = path.replace(f"/{value}", "/{id}") path = path.replace(f"/{value}", "/{id}")
for key, value in resolver_match.kwargs.items(): for key, value in resolver_match.kwargs.items():
path = path.replace(f"/{value}", f"/{{{key}}}")
if key == 'pk': if key == 'pk':
pass path = path.replace(f"/{value}", f"/{{id}}")
continue
path = path.replace(f"/{value}", f"/{{{key}}}")
return path return path

View File

@ -1,7 +1,7 @@
""" """
常用的Response以及Django的ResponseDRF的Response 常用的Response以及Django的ResponseDRF的Response
""" """
from django.http.response import DjangoJSONEncoder from django.http.response import DjangoJSONEncoder, JsonResponse
from rest_framework.response import Response from rest_framework.response import Response
@ -56,3 +56,36 @@ class ErrorResponse(Response):
def __str__(self): def __str__(self):
return str(self.std_data) 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_ENABLE = True
API_LOG_METHODS = ['POST', 'DELETE', 'PUT'] # 'ALL' or ['POST', 'DELETE'] API_LOG_METHODS = ['POST', 'DELETE', 'PUT'] # 'ALL' or ['POST', 'DELETE']
# 接口权限
INTERFACE_PERMISSION = True

View File

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