Bladeren bron

优化权限与注册

SpringSunYY 4 maanden geleden
bovenliggende
commit
c4d015fd64

+ 24 - 15
ruoyi-ui/src/views/register.vue

@@ -4,7 +4,7 @@
       <h3 class="title">若依后台管理系统</h3>
       <el-form-item prop="username">
         <el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
-          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
+          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon"/>
         </el-input>
       </el-form-item>
       <el-form-item prop="password">
@@ -15,7 +15,7 @@
           placeholder="密码"
           @keyup.enter.native="handleRegister"
         >
-          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
+          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
         </el-input>
       </el-form-item>
       <el-form-item prop="confirmPassword">
@@ -26,7 +26,7 @@
           placeholder="确认密码"
           @keyup.enter.native="handleRegister"
         >
-          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
+          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
         </el-input>
       </el-form-item>
       <el-form-item prop="code" v-if="captchaOnOff">
@@ -37,7 +37,7 @@
           style="width: 63%"
           @keyup.enter.native="handleRegister"
         >
-          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
+          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/>
         </el-input>
         <div class="register-code">
           <img :src="codeUrl" @click="getCode" class="register-code-img"/>
@@ -67,7 +67,7 @@
 </template>
 
 <script>
-import { getCodeImg, register } from "@/api/login";
+import {getCodeImg, register} from "@/api/login";
 
 export default {
   name: "Register",
@@ -90,21 +90,21 @@ export default {
       },
       registerRules: {
         username: [
-          { required: true, trigger: "blur", message: "请输入您的账号" },
-          { min: 2, max: 20, message: '用户账号长度必须介于 2 和 20 之间', trigger: 'blur' }
+          {required: true, trigger: "blur", message: "请输入您的账号"},
+          {min: 2, max: 20, message: '用户账号长度必须介于 2 和 20 之间', trigger: 'blur'}
         ],
         password: [
-          { required: true, trigger: "blur", message: "请输入您的密码" },
-          { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
+          {required: true, trigger: "blur", message: "请输入您的密码"},
+          {min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur'}
         ],
         confirmPassword: [
-          { required: true, trigger: "blur", message: "请再次输入您的密码" },
-          { required: true, validator: equalToPassword, trigger: "blur" }
+          {required: true, trigger: "blur", message: "请再次输入您的密码"},
+          {required: true, validator: equalToPassword, trigger: "blur"}
         ],
-        code: [{ required: true, trigger: "change", message: "请输入验证码" }]
+        code: [{required: true, trigger: "change", message: "请输入验证码"}]
       },
       loading: false,
-      captchaOnOff: true
+      captchaOnOff: false
     };
   },
   created() {
@@ -113,7 +113,6 @@ export default {
   methods: {
     getCode() {
       getCodeImg().then(res => {
-        this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff;
         if (this.captchaOnOff) {
           this.codeUrl = "data:image/gif;base64," + res.img;
           this.registerForm.uuid = res.uuid;
@@ -131,7 +130,8 @@ export default {
               type: 'success'
             }).then(() => {
               this.$router.push("/login");
-            }).catch(() => {});
+            }).catch(() => {
+            });
           }).catch(() => {
             this.loading = false;
             if (this.captchaOnOff) {
@@ -154,6 +154,7 @@ export default {
   background-image: url("../assets/images/login-background.jpg");
   background-size: cover;
 }
+
 .title {
   margin: 0px auto 30px auto;
   text-align: center;
@@ -165,32 +166,39 @@ export default {
   background: #ffffff;
   width: 400px;
   padding: 25px 25px 5px 25px;
+
   .el-input {
     height: 38px;
+
     input {
       height: 38px;
     }
   }
+
   .input-icon {
     height: 39px;
     width: 14px;
     margin-left: 2px;
   }
 }
+
 .register-tip {
   font-size: 13px;
   text-align: center;
   color: #bfbfbf;
 }
+
 .register-code {
   width: 33%;
   height: 38px;
   float: right;
+
   img {
     cursor: pointer;
     vertical-align: middle;
   }
 }
+
 .el-register-footer {
   height: 40px;
   line-height: 40px;
@@ -203,6 +211,7 @@ export default {
   font-size: 12px;
   letter-spacing: 1px;
 }
+
 .register-code-img {
   height: 38px;
 }

+ 3 - 1
ruoyi_admin/controller/index/register.py

@@ -3,6 +3,7 @@
 
 from ruoyi_common.base.model import AjaxResponse
 from ruoyi_common.descriptor.serializer import JsonSerializer
+from ruoyi_common.descriptor.validator import BodyValidator
 from ruoyi_common.domain.vo import RegisterBody
 from ruoyi_system.service import SysConfigService
 from ruoyi_framework.service.sys_register import RegisterService
@@ -10,8 +11,9 @@ from ... import reg
 
 
 @reg.api.route("/register", methods=["POST"])
+@BodyValidator()
 @JsonSerializer()
-def index_register(dto:RegisterBody):
+def index_register(dto: RegisterBody):
     '''
     注册接口
     '''

+ 28 - 27
ruoyi_common/base/serializer.py

@@ -36,14 +36,13 @@ del _update_exceptions
 
 
 class HttpException(HTTPException):
-
     code: int | None = None
     description: str | None = None
 
     def __init__(
-        self,
-        description: str | None = None,
-        response: Response | None = None,
+            self,
+            description: str | None = None,
+            response: Response | None = None,
     ) -> None:
         super().__init__()
         if description is not None:
@@ -51,7 +50,7 @@ class HttpException(HTTPException):
         self.response = response
 
     @classmethod
-    def from_http_exception(cls, exc: HTTPException) -> "HttpException": 
+    def from_http_exception(cls, exc: HTTPException) -> "HttpException":
         """
         从HTTPException转换为HttpException
 
@@ -64,12 +63,12 @@ class HttpException(HTTPException):
         error = cls(description=exc.description, response=exc.response)
         error.code = exc.code
         return error
-    
+
     @property
     def name(self) -> str:
         """
         状态名称
-        
+
         Returns:
             str: 状态名称
         """
@@ -78,13 +77,13 @@ class HttpException(HTTPException):
         return HTTP_STATUS_CODES.get(self.code, "Unknown Error")  # type: ignore
 
     def get_description(
-        self,
-        environ: WSGIEnvironment | None = None,
-        scope: dict[str, t.Any] | None = None,
+            self,
+            environ: WSGIEnvironment | None = None,
+            scope: dict[str, t.Any] | None = None,
     ) -> str:
         """
         异常描述
-        
+
         Args:
             environ (WSGIEnvironment, optional): 环境变量. Defaults to None.
             scope (dict[str, t.Any], optional): 作用域. Defaults to None.
@@ -95,13 +94,13 @@ class HttpException(HTTPException):
         return self.description or ""
 
     def get_body(
-        self,
-        environ: WSGIEnvironment | None = None,
-        scope: dict[str, t.Any] | None = None,
+            self,
+            environ: WSGIEnvironment | None = None,
+            scope: dict[str, t.Any] | None = None,
     ) -> str:
         """
         异常响应体
-        
+
         Args:
             environ (WSGIEnvironment, optional): 环境变量. Defaults to None.
             scope (dict[str, t.Any], optional): 作用域. Defaults to None.
@@ -112,18 +111,18 @@ class HttpException(HTTPException):
         ajax_resposne = AjaxResponse.from_error(msg=self.description)
         ajax_resposne.code = self.code
         return ajax_resposne.model_dump_json(
-            exclude_unset = True,
-            exclude_none = True,
+            exclude_unset=True,
+            exclude_none=True,
         )
 
     def get_headers(
-        self,
-        environ: WSGIEnvironment | None = None,
-        scope: dict[str, t.Any] | None = None,
+            self,
+            environ: WSGIEnvironment | None = None,
+            scope: dict[str, t.Any] | None = None,
     ) -> list[tuple[str, str]]:
         """
         异常请求头
-        
+
         Args:
             environ (WSGIEnvironment, optional): 环境变量. Defaults to None.
             scope (dict[str, t.Any], optional): 作用域. Defaults to None.
@@ -145,11 +144,11 @@ def json_default(obj):
         _type_: 可序列化对象
     """
     if isinstance(obj, date):
-        return http_date(obj) 
+        return http_date(obj)
 
     if isinstance(obj, decimal.Decimal):
         return str(obj)
-    
+
     if isinstance(obj, uuid.UUID):
         return obj.hex
 
@@ -172,11 +171,11 @@ class JsonProvider(DefaultJSONProvider):
     Args:
         DefaultJSONProvider: 默认flask的json序列化
     """
-    
+
     default = staticmethod(json_default)
 
 
-def handle_http_exception(error:HTTPException) -> Response:
+def handle_http_exception(error: HTTPException) -> Response:
     """
     处理http异常
 
@@ -191,9 +190,11 @@ def handle_http_exception(error:HTTPException) -> Response:
     return error.get_response()
 
 
-def handle_util_exception(error:UtilException) -> Response:
+def handle_util_exception(error: UtilException) -> Response:
     """
     处理业务工具类异常,保持和若依Java版一致的json结构
+    注意:HTTP状态码始终返回200,业务状态码放在响应体的code字段中
+    这样前端可以正确读取msg字段,而不是显示"接口XXX异常"
     """
     status = getattr(error, "status", HttpStatus.ERROR)
     ajax_response = AjaxResponse.from_error(msg=str(error))
@@ -203,7 +204,7 @@ def handle_util_exception(error:UtilException) -> Response:
             exclude_unset=True,
             exclude_none=True,
         ),
-        status=status,
+        status=HttpStatus.SUCCESS,  # HTTP状态码始终返回200,业务状态码在响应体的code中
         mimetype="application/json"
     )
     return response

+ 7 - 9
ruoyi_common/exception.py

@@ -7,33 +7,31 @@ from werkzeug.exceptions import HTTPException
 
 
 class CreatedException(HTTPException):
-    
     code = 201
     description = 'Request Create Status'
 
-    
+
 class AcceptedException(HTTPException):
-    
     code = 202
     description = 'Request Accept Status'
 
 
 class NoContentException(HTTPException):
-    
     code = 204
     description = 'Request No Content Status'
 
 
 class ServiceException(AcceptedException):
-    
     description = 'Service Accept Status'
-    
+
 
 class CaptchaException(AcceptedException):
-    
     description = 'Captcha Accept Status'
-    
+
+
+class NotContentException(AcceptedException):
+    description = 'Not Content Accept Status'
+
 
 class CaptchaExpireException(AcceptedException):
-    
     description = 'Captcha Expire Status'

+ 80 - 71
ruoyi_framework/descriptor/permission.py

@@ -7,38 +7,40 @@ from flask_login import UserMixin
 
 from ruoyi_common.domain.entity import LoginUser
 from ruoyi_common.utils import security_util as SecurityUtil
+from ruoyi_common.utils.base import UtilException
+from ruoyi_common.constant import HttpStatus
 
 
 class PermissionService:
     """
     菜单权限
     """
-    
+
     # 所有权限标识
     ALL_PERMISSION = "*:*:*"
-    
-    # 管理员角色权限标识 
+
+    # 管理员角色权限标识
     SUPER_ADMIN = "admin"
-    
+
     ROLE_DELIMETER = ","
-    
+
     PERMISSION_DELIMETER = ","
-    
+
     @classmethod
-    def has_perm(cls, permission:str) -> bool:
+    def has_perm(cls, permission: str) -> bool:
         """
         验证用户是否具备某权限
-        
+
         Args:
             permission (str): 权限标识
-        
+
         Returns:
             bool: True:具备该权限,False:不具备该权限
         """
         if not permission:
             return False
-        login_user:LoginUser = SecurityUtil.get_login_user()
-        if not login_user: 
+        login_user: LoginUser = SecurityUtil.get_login_user()
+        if not login_user:
             return False
         else:
             if not isinstance(login_user, UserMixin):
@@ -47,34 +49,34 @@ class PermissionService:
             if not user_authorities: return False
         return cls.ALL_PERMISSION in user_authorities \
             or permission.strip() in user_authorities
-    
+
     @classmethod
-    def no_perm(cls, permission:str) -> bool:
+    def no_perm(cls, permission: str) -> bool:
         """
         验证用户是否不具备某权限
-        
+
         Args:
             permission (str): 权限标识
-        
+
         Returns:
             bool: True:不具备该权限,False:具备该权限
         """
         return not cls.has_perm(permission)
-    
+
     @classmethod
-    def any_perm(cls, permissions:str) -> bool:
+    def any_perm(cls, permissions: str) -> bool:
         """
         验证用户是否具备某权限列表中的任意一个权限
-        
+
         Args:
             permissions (str): 权限标识列表,多个权限标识以逗号分隔
-        
+
         Returns:
             bool: True:具备任意一个权限,False:不具备任何一个权限
         """
         if not permissions: return False
-        login_user:LoginUser = SecurityUtil.get_login_user()
-        if not login_user: 
+        login_user: LoginUser = SecurityUtil.get_login_user()
+        if not login_user:
             return False
         else:
             user_authorities = login_user.permissions
@@ -83,84 +85,84 @@ class PermissionService:
             if permission.strip() in user_authorities:
                 return True
         return False
-    
+
     @classmethod
-    def has_role(cls, role:str) -> bool:
+    def has_role(cls, role: str) -> bool:
         """
         验证用户是否具备某角色
-        
+
         Args:
             role (str): 角色标识
-        
+
         Returns:
             bool: True:具备该权限,False:不具备该权限
         """
         if not role:
             return False
-        login_user:LoginUser = SecurityUtil.get_login_user()
+        login_user: LoginUser = SecurityUtil.get_login_user()
         if not login_user or not login_user.user.roles:
             return False
         for sys_role in login_user.user.roles:
-            if sys_role.role_key == cls.SUPER_ADMIN  \
-                or sys_role.role_key == role.strip():
+            if sys_role.role_key == cls.SUPER_ADMIN \
+                    or sys_role.role_key == role.strip():
                 return True
         return False
-    
+
     @classmethod
-    def no_role(cls, role:str) -> bool:
+    def no_role(cls, role: str) -> bool:
         """
         验证用户是否不具备某角色
-        
+
         Args:
             role (str): 角色标识
-        
+
         Returns:
             bool: True:具备该权限,False:不具备该权限
         """
         return not cls.has_role(role)
-    
+
     @classmethod
-    def any_role(cls, roles:str) -> bool:
+    def any_role(cls, roles: str) -> bool:
         """
         验证用户是否具备某角色列表中的任意一个角色
-        
+
         Args:
             roles (str): 角色标识列表,多个角色标识以逗号分隔
-        
+
         Returns:
             bool: True:具备任意一个角色,False:不具备任何一个角色
         """
         if not roles: return False
-        login_user:LoginUser = SecurityUtil.get_login_user()
+        login_user: LoginUser = SecurityUtil.get_login_user()
         if not login_user or not login_user.user.roles:
             return False
         for role in roles.split(cls.ROLE_DELIMETER):
             for sys_role in login_user.user.roles:
-                if sys_role.role_key == cls.SUPER_ADMIN  \
-                    or sys_role.role_key == role.strip():
+                if sys_role.role_key == cls.SUPER_ADMIN \
+                        or sys_role.role_key == role.strip():
                     return True
         return False
 
 
 class AuthorityCaller:
-    
-    def __init__(self, value:str) -> None:
+
+    def __init__(self, value: str) -> None:
         self._value = value
-    
+
     def __call__(self) -> bool:
         NotImplementedError()
 
 
 def LoginRequired() -> bool:
     """
-    
+
     验证用户是否登录
-    
+
     Returns:
         bool -- True:已登录,False:未登录
     """
-    login_user:LoginUser = SecurityUtil.get_login_user()
-    if not login_user: 
+    login_user: LoginUser = SecurityUtil.get_login_user()
+    if not login_user:
         return False
     if not login_user.is_authenticated:
         return False
@@ -169,76 +171,83 @@ def LoginRequired() -> bool:
 
 class HasPerm(AuthorityCaller):
     """
-    
+
     验证用户是否具备某权限
-    
-    """ 
+
+    """
+
     def __call__(self) -> bool:
         return PermissionService.has_perm(self._value)
 
 
 class NoPerm(AuthorityCaller):
     """
-    
+
     验证用户是否不具备某权限
-    
-    """      
+
+    """
+
     def __call__(self) -> bool:
         return PermissionService.no_perm(self._value)
 
 
 class AnyPerm(AuthorityCaller):
     """
-    
+
     验证用户是否具备某权限列表中的任意一个权限
-    
-    """      
+
+    """
+
     def __call__(self) -> bool:
-        return PermissionService.any_perm(self._value)    
+        return PermissionService.any_perm(self._value)
 
 
 class HasRole(AuthorityCaller):
     """
-    
+
     验证用户是否具备某角色
-    
-    """    
+
+    """
+
     def __call__(self) -> bool:
-        return PermissionService.has_role(self._value)    
+        return PermissionService.has_role(self._value)
 
 
 class NoRole(AuthorityCaller):
     """
-    
+
     验证用户是否不具备某角色
-    
+
     """
+
     def __call__(self) -> bool:
         return PermissionService.no_role(self._role)
 
 
 class AnyRole(AuthorityCaller):
     """
-    
+
     验证用户是否具备某角色列表中的任意一个角色
-    
+
     """
+
     def __call__(self) -> bool:
-        return PermissionService.any_role(self._role)   
+        return PermissionService.any_role(self._role)
 
 
 class PreAuthorize:
-    
-    def __init__(self, auth:AuthorityCaller|Callable):
+
+    def __init__(self, auth: AuthorityCaller | Callable):
         self._auth = auth
-    
+
     def __call__(self, func) -> Callable:
-           
+
         @wraps(func)
         def wrapper(*args, **kwargs):
             if not callable(self._auth):
-                raise Exception("权限验证器必须是可调用对象")
+                raise UtilException("权限验证器必须是可调用对象", HttpStatus.ERROR)
             if not self._auth():
-                raise Exception("无访问权限")
+                raise UtilException("无访问权限", HttpStatus.FORBIDDEN)
             return func(*args, **kwargs)
+
         return wrapper

+ 21 - 19
ruoyi_framework/service/sys_register.py

@@ -5,34 +5,34 @@ from flask import flash
 from ruoyi_common.domain.vo import RegisterBody
 from ruoyi_common.utils import security_util as SecurityUtil
 from ruoyi_common.constant import Constants, UserConstants
-from ruoyi_common.exception import CaptchaException, CaptchaExpireException
+from ruoyi_common.exception import CaptchaException, CaptchaExpireException, NotContentException
 from ruoyi_common.domain.entity import SysUser
-from ruoyi_system.service import SysUserService
+from ruoyi_system.service import SysUserService, SysConfigService
+from ruoyi_system.mapper import SysUserMapper
 from ruoyi_admin.ext import redis_cache
 
-# todo
 
 class RegisterService:
 
     @classmethod
-    def register(cls, body:RegisterBody) -> str:
+    def register(cls, body: RegisterBody) -> str:
         """
         注册用户
-        
+
         Args:
             body (RegisterBody): 注册信息
-        
+
         Returns:
-            str: 注册结果信息    
+            str: 注册结果信息
         """
         msg = ""
         username = body.username
         password = body.password
 
-        # captcha_on_off = cls.config_service.select_captcha_on_off()
+        captcha_on_off = SysConfigService.select_captcha_on_off()
         # Captcha switch
-        # if captcha_on_off:
-            # cls.validate_captcha(username, body.code, body.uuid)
+        if captcha_on_off:
+            cls.validate_captcha(username, body.code, body.uuid)
 
         if not username:
             msg = "Username cannot be empty"
@@ -42,7 +42,7 @@ class RegisterService:
             msg = "Account length must be between 2 and 20 characters"
         elif len(password) < UserConstants.PASSWORD_MIN_LENGTH or len(password) > UserConstants.PASSWORD_MAX_LENGTH:
             msg = "Password length must be between 5 and 20 characters"
-        elif UserConstants.NOT_UNIQUE == SysUserService.check_user_name_unique(username):
+        elif SysUserMapper.check_user_name_unique(username) > 0:
             msg = f"Failed to save user '{username}', registration account already exists"
         else:
             sys_user = SysUser(
@@ -56,25 +56,27 @@ class RegisterService:
             else:
                 flash("user.register.success")
         return msg
-    
+
     @classmethod
-    def validate_captcha(self, username:str, code:str, uuid:str):
+    def validate_captcha(cls, username: str, code: str, uuid: str):
         """
         验证码校验
-        
+
         Args:
             username (str): 用户名
             code (str): 验证码
             uuid (str): 验证码唯一标识
-        
+
         Raises:
             CaptchaException: 验证码错误
             CaptchaExpireException: 验证码过期
         """
+        if not code:
+            raise NotContentException()
         verify_key = Constants.CAPTCHA_CODE_KEY + (uuid if uuid is not None else "")
-        captcha = redis_cache.get_cache_object(verify_key)
-        # redis_cache.delete_object(verify_key)
-        if captcha is None:
+        captcha: bytes = redis_cache.get(verify_key)
+        if not captcha:
             raise CaptchaExpireException()
-        if code.lower() != captcha.lower():
+        redis_cache.delete(verify_key)
+        if code.lower() != captcha.decode("utf-8").lower():
             raise CaptchaException()