开源SpringBoot3+Vue3单体项目权限管理系统学习(一)

文章学习来源:基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Knife4j、Vue 3、Element-Plus 构建的前后端分离单体权限管理系统。

一、项目框架

1.1 common

1.1.1 base基本类

1.BaseEntity用于作为业务实体类的父类,其他所有业务实现类继承BaseEntity,继承的主要作用在于对创建时间、更新时间进行基准化。

其中@TableField能够对单个字段进行增加,而在plugin包的mybatis handle MyMetaObjectHandler自定义了填充的内容

2.BasePageQuery用于作为分页查询数据的基准类

3.BaseVO作为视图对象基准类

4.IBaseEnum作为枚举通用接口,设置枚举值与枚举标签

这里的静态方法相当于匹配,给出属性值value,获取枚举得到的实例。

1.1.2 constant 常量

1.给出JwtClaimConstants的常量包括USER_ID、DEPT_ID(部门)、DATA_SCOPE(数据权限常量)、AUTHORITIES(角色集合)名称全部用于生成token,主要用于JwtUtils工具类中,权限信息是一个Collection<SimpleGrantAuthority>,最后将信息存储返回map和key中,key。

2.SecurityConstants中规定了(1)角色和权限缓存前缀,定义的验证码前缀。如下图所示,规定验证码的缓存前缀为cptche_code

private final RedisTemplate<String, Object> redisTemplate;
private final CodeGenerator codeGenerator;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
// 生成验证码
    public CaptchaResult getCaptcha() {
        String captchaType = captchaProperties.getType();//获取验证码类型
        int width = captchaProperties.getWidth();//获取验证码宽度
        int height = captchaProperties.getHeight();//获取验证码高度
        int interfereCount = captchaProperties.getInterfereCount();//获取验证码干扰元素数量
        int codeLength = captchaProperties.getCode().getLength();//获取验证码字符长度

        AbstractCaptcha captcha;//新建一个AbstractCaptcha对象
// if语句判断验证码是什么类型的,此处使用了枚举类CaptchaTypeEnum定义常量,并进行创建
        if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
            captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
        } else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
            captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
        } else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
            captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
        } else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
            captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
        } else {
            throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
        }
//设置captcha
        captcha.setGenerator(codeGenerator);
        captcha.setTextAlpha(captchaProperties.getTextAlpha());
        captcha.setFont(captchaFont);
        String captchaCode = captcha.getCode();
        String imageBase64Data = captcha.getImageBase64Data();
        // 验证码文本缓存至Redis,用于登录校验
        String captchaKey = IdUtil.fastSimpleUUID();
//redis中缓存名为captcha_code:UUID序列化结果,captcha.getCode()返回验证码的值
        redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,
                captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
        return CaptchaResult.builder()
                .captchaKey(captchaKey)
                .captchaBase64(imageBase64Data)
                .build();
    }

(2)在登录时的验证码校验,包含一个验证码登录的过滤器,因此验证码是在缓存中,从缓存中获取验证码

 @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 检验登录接口的验证码,判断请求路径是否匹配
        if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
            // 请求中的验证码
            String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
            // TODO 兼容没有验证码的版本(线上请移除这个判断)
            if (StrUtil.isBlank(captchaCode)) {
                chain.doFilter(request, response);
                return;
            }
            // 缓存中的验证码,获取参数中的验证码
            String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
            String cacheVerifyCode = (String) redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
            if (cacheVerifyCode == null) {
                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
            } else {
                // 验证码比对
                if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
                    chain.doFilter(request, response);
                } else {
                    ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
                }
            }
        } else {
            // 非登录接口放行
            chain.doFilter(request, response);
        }
    }

(3)SecurityConstants类中设置常量ROLE_PERMS_PREFIX添加角色和权限的前缀即其中的key,匹配其中的value值。

public void refreshRolePermsCache(String roleCode) {
        // 清理权限缓存
        redisTemplate.opsForHash().delete(SecurityConstants.ROLE_PERMS_PREFIX, roleCode);
        List<RolePermsBO> list = this.baseMapper.getRolePermsList(roleCode);
// 循环权限,将内容存储至redis中
        if (CollectionUtil.isNotEmpty(list)) {
            RolePermsBO rolePerms = list.get(0);
            if (rolePerms == null) {
                return;
            }
            Set<String> perms = rolePerms.getPerms();
            redisTemplate.opsForHash().put(SecurityConstants.ROLE_PERMS_PREFIX, roleCode, perms);
        }
    }

(4)在SecurityConstants中设置黑名单token缓存,主要用于Jwt token校验过滤器和用户退出登录的情况,黑名单判断。

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);

        try {
            if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
                token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); 
// 去除 Bearer 前缀

                // 解析 Token
                JWT jwt = JWTUtil.parseToken(token);

                // 检查 Token 是否有效(验签 + 是否过期)
                boolean isValidate = jwt.setKey(secretKey).validate(0);
                if (!isValidate) {
                    ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
                    return;
                }

                // 检查 Token 是否已被加入黑名单(注销)
                JSONObject payloads = jwt.getPayloads();
                String jti = payloads.getStr(JWTPayload.JWT_ID);
                boolean isTokenBlacklisted = Boolean.TRUE.equals(redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti));
                if (isTokenBlacklisted) {
                    ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
                    return;
                }

                // Token 有效将其解析为 Authentication 对象,并设置到 Spring Security 上下文中
                Authentication authentication = JwtUtils.getAuthentication(payloads);
                SecurityContextHolder.getContext().setAuthentication(authentication);

            }
        } catch (Exception e) {
            SecurityContextHolder.clearContext();
            ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
            return;
        }
        // Token有效或无Token时继续执行过滤链
        filterChain.doFilter(request, response);
    }
// 主要用于用户退出登录
public void logout() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
            token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
            // 解析Token以获取有效载荷(payload)
            JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
            // 解析 Token 获取 jti(JWT ID) 和 exp(过期时间)
            String jti = payloads.getStr(JWTPayload.JWT_ID);
            Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); // 过期时间(秒)
            // 如果exp存在,则计算Token剩余有效时间
            if (expiration != null) {
                long currentTimeSeconds = System.currentTimeMillis() / 1000;
                if (expiration < currentTimeSeconds) {
                    // Token已过期,不再加入黑名单
                    return;
                }
                // 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除
                long ttl = expiration - currentTimeSeconds;
                redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS);
            } else {
                // 如果exp不存在,说明Token永不过期,则永久加入黑名单
                redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null);
            }
        }
        // 清空Spring Security上下文
        SecurityContextHolder.clearContext();
    }

 (5)其他的常量比较好理解:

 3.SystemConstants接口主要用于表示系统常量

 菜单数用于判断是否为根节点。

1.1.3 enums枚举类

不进行解释了 ,直接看代码。

1.1.4 exception异常类

自定义业务异常

@Getter
public class BusinessException extends RuntimeException {
    public IResultCode resultCode;
    public BusinessException(IResultCode errorCode) {
        super(errorCode.getMsg());
        this.resultCode = errorCode;
    }
    public BusinessException(String message){
        super(message);
    }
    public BusinessException(String message, Throwable cause){
        super(message, cause);
    }
    public BusinessException(Throwable cause){
        super(cause);
    }
}

全局系统异常处理

该框架的全局异常处理比较完善,包括其中的主要问题

/**
 * 全局系统异常处理器
 * <p>
 * 调整异常处理的HTTP状态码,丰富异常处理类型
 **/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 表单绑定数据异常
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(BindException e) {
        log.error("BindException:{}", e.getMessage());
        String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
        return Result.failed(ResultCode.PARAM_ERROR, msg);
    }

    /**
     * RequestParam参数的校验
     * @param e
     * @param <T>
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(ConstraintViolationException e) {
        log.error("ConstraintViolationException:{}", e.getMessage());
        String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
        return Result.failed(ResultCode.PARAM_ERROR, msg);
    }

    /**
     * RequestBody参数的校验异常
     * @param e
     * @param <T>
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(MethodArgumentNotValidException e) {
        log.error("MethodArgumentNotValidException:{}", e.getMessage());
        String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
        return Result.failed(ResultCode.PARAM_ERROR, msg);
    }

    /**
     * 404异常
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public <T> Result<T> processException(NoHandlerFoundException e) {
        log.error(e.getMessage(), e);
        return Result.failed(ResultCode.RESOURCE_NOT_FOUND);
    }

    /**
     * 缺少请求参数
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(MissingServletRequestParameterException e) {
        log.error(e.getMessage(), e);
        return Result.failed(ResultCode.PARAM_IS_NULL);
    }

    /**
     * 参数类型不匹配
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
        log.error(e.getMessage(), e);
        return Result.failed(ResultCode.PARAM_ERROR, "类型错误");
    }

    /**
     * Servlet异常
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(ServletException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(ServletException e) {
        log.error(e.getMessage(), e);
        return Result.failed(e.getMessage());
    }


    /**
     * 参数非法抛出
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
        log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
        return Result.failed(e.getMessage());
    }

    /**
     * Json序列化转换异常
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(JsonProcessingException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> handleJsonProcessingException(JsonProcessingException e) {
        log.error("Json转换异常,异常原因:{}", e.getMessage(), e);
        return Result.failed(e.getMessage());
    }

    /**
     * 无法读取请求体
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(HttpMessageNotReadableException e) {
        log.error(e.getMessage(), e);
        String errorMessage = "请求体不可为空";
        Throwable cause = e.getCause();
        if (cause != null) {
            errorMessage = convertMessage(cause);
        }
        return Result.failed(errorMessage);
    }

    /**
     * 类型不匹配
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(TypeMismatchException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> processException(TypeMismatchException e) {
        log.error(e.getMessage(), e);
        return Result.failed(e.getMessage());
    }

    /**
     * SQL语法错误
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(BadSqlGrammarException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
        log.error(e.getMessage(), e);
        String errorMsg = e.getMessage();
        if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
            return Result.failed(ResultCode.FORBIDDEN_OPERATION);
        } else {
            return Result.failed(e.getMessage());
        }
    }

    /**
     * SQL语法错误
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(SQLSyntaxErrorException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
        log.error(e.getMessage(), e);
        return Result.failed(e.getMessage());
    }

    /**
     * 业务自定义异常
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> handleBizException(BusinessException e) {
        log.error("biz exception: {}", e.getMessage());
        if (e.getResultCode() != null) {
            return Result.failed(e.getResultCode());
        }
        return Result.failed(e.getMessage());
    }

    /**
     * 默认异常处理
     * @param e
     * @return
     * @param <T>
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public <T> Result<T> handleException(Exception e) throws Exception{
        // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
        if (e instanceof AccessDeniedException
                || e instanceof AuthenticationException) {
            throw e;
        }
        log.error("unknown exception: {}", e.getMessage());
        e.printStackTrace();
        return Result.failed(e.getLocalizedMessage());
    }

    /**
     * 传参类型错误时,用于消息转换
     *
     * @param throwable 异常
     * @return 错误信息
     */
    private String convertMessage(Throwable throwable) {
        String error = throwable.toString();
        String regulation = "\\[\"(.*?)\"]+";
        Pattern pattern = Pattern.compile(regulation);
        Matcher matcher = pattern.matcher(error);
        String group = "";
        if (matcher.find()) {
            String matchString = matcher.group();
            matchString = matchString.replace("[", "").replace("]", "");
            matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", ""));
            group += matchString;
        }
        return group;
    }
}

1.1.5 model 模型类

keyValue作为下拉选项的对象、option作为选项的值,用于获取菜单的键值对(这里需要动动脑子,先放一下)

1.1.6 result类

设置IResultCode用于作为Result的接口,设置PageResult作为分页的返回接口,暂时先到这里,写累了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值