文章学习来源:基于 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作为分页的返回接口,暂时先到这里,写累了。
版权归原作者 进击的阿宣 所有, 如有侵权,请联系我们删除。