此文章适用于 学生管理系统、成绩管理系统、在线考试系统、图书管理系统 等,提供源码下载。
技术架构:Java + SpringBoot + Vue3 + MySQL
一、项目搭建
1.1 开发工具
2024年了,我们就不考虑Eclipse了好吧,直接下载IDEA社区版。
下载地址:https://www.jetbrains.com/idea/download/other.html
1.2 环境配置
1.2.1 JDK
(1)下载JDK1.8 windows64位安装版:https://tool4j.com/files/software/jdk-8u102-windows-x64.exe
(2)安装及环境变量配置:https://blog.csdn.net/i_for/article/details/131128502
1.2.2 Maven
(1)下载Maven3.6.3:https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.zip
(2)配置教程:https://www.cnblogs.com/yu-si/p/14586626.html
1.2.3 MySQL
安装配置教程:https://blog.csdn.net/fattigers/article/details/135558127
1.2.4 NodeJs
下载Nodejs16.20.2:https://nodejs.org/dist/v16.20.2/node-v16.20.2-x64.msi
安装教程:没什么特别的步骤,无脑下一步即可,会自动配置好环境变量
1.3 模板工程下载
目前后台管理系统用的比较多的模板工程一般是若依,或者是基于若依改造而来的,虽然若依这套框架存在很多问题,但是对于初学者或者小公司来说,确实可以减少很多工作量,还是有可取之处的。
博主使用的模板工程是:youlai-boot,并进行了优化,删除了大量冗余代码,更易于初学者使用,有需要的可以通过Gitee下载。
前端项目地址:https://gitee.com/dwp1216/boot4j_ui.git
后台项目地址:https://gitee.com/dwp1216/boot4j.git
二、数据库表设计
基础工程所需的数据库表SQL文件已经放在项目根目录 /src/resources/sql 下了。
系统内置表如下:
create table sys_menu
(
id bigint auto_increment
primary key,
parent_id bigint not null comment '父菜单ID',
tree_path varchar(255) null comment '父节点ID路径',
name varchar(64) default '' not null comment '菜单名称',
type tinyint not null comment '菜单类型(1:菜单 2:目录 3:外链 4:按钮)',
path varchar(128) default '' null comment '路由路径(浏览器地址栏路径)',
component varchar(128) null comment '组件路径(vue页面完整路径,省略.vue后缀)',
perm varchar(128) null comment '权限标识',
visible tinyint(1) default 1 not null comment '显示状态(1-显示;0-隐藏)',
sort int default 0 null comment '排序',
icon varchar(64) default '' null comment '菜单图标',
redirect varchar(128) null comment '跳转路径',
create_time datetime null comment '创建时间',
update_time datetime null comment '更新时间',
always_show tinyint null comment '【目录】只有一个子路由是否始终显示(1:是 0:否)',
keep_alive tinyint null comment '【菜单】是否开启页面缓存(1:是 0:否)'
)
comment '菜单管理' charset = utf8mb3;
create table sys_role
(
id bigint auto_increment comment '主键id'
primary key,
role_code varchar(100) null comment '角色标识',
role_name varchar(100) null comment '角色名称',
remark varchar(255) null comment '描述',
create_by varchar(100) null comment '创建人',
create_at datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(100) null comment '修改人',
update_at datetime null on update CURRENT_TIMESTAMP comment '修改时间'
)
comment '系统管理-角色信息表' collate = utf8mb4_bin;
create table sys_role_permission
(
id bigint auto_increment comment '主键id'
primary key,
resource_id bigint null comment '资源ID',
resource_type varchar(100) charset utf8mb4 null comment '资源类型(menu:菜单、btn:按钮、api:服务)',
role_id bigint null comment '角色ID',
create_by varchar(100) charset utf8mb4 null comment '创建人',
create_at datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_by varchar(100) charset utf8mb4 null comment '修改人',
update_at datetime null on update CURRENT_TIMESTAMP comment '修改时间'
)
comment '系统管理-角色权限表' collate = utf8mb4_bin;
create table sys_user
(
id bigint auto_increment comment '主键'
primary key,
user_id bigint null comment '用户ID',
account varchar(255) null comment '账号',
password varchar(255) null comment '密码',
role_id bigint null comment '角色ID',
created_date timestamp default CURRENT_TIMESTAMP null,
updated_date datetime null on update CURRENT_TIMESTAMP comment '修改时间'
)
comment '系统管理-用户信息表' charset = utf8mb4;
create table sys_user_detail
(
id bigint auto_increment comment '主键'
primary key,
user_id bigint null comment '用户ID',
username varchar(255) null comment '用户名',
avatar varchar(255) null comment '头像',
sex varchar(10) null comment '性别:1-男、0-女、2-保密',
age int null comment '年龄',
region varchar(255) null comment '地区',
remark varchar(255) null comment '简介',
created_date timestamp default CURRENT_TIMESTAMP null,
updated_date datetime null on update CURRENT_TIMESTAMP comment '修改时间',
phone_num varchar(255) null comment '手机号',
email varchar(255) null comment '邮箱'
)
comment '系统管理-用户详情信息表' charset = utf8mb4;
在设计数据库表时,我们可以按如下思路来设计:
- 根据需求先设计出有哪些对象,一个对象就是一张表
- 对象之间的关联关系
(1) 如果是一对一、一对多的关系,就在对象表上增加另一个对象的ID字段来关联
(2) 如果是多对多的关系,建议使用单独的关系表来保存关联关系
- 对于比较通用的信息,建议使用更加通用的对象表来存储,例如文件信息表、用户消息表。
三、模板代码生成
表结构设计好以后,可通过 代码生成器 一键生成代码。
复制或者上传sql文件,即可一键生成。
生成后点击下载,会下载一个zip压缩包,解压之后放到src/main/java目录下即可使用。
四、功能实现
4.1 登录认证功能
对于小型项目,我们可以使用Spring提供的 HandlerInterceptor拦截前端请求,进行登录认证。
- 首先创建一个拦截器类SecurityInterceptor:
import com.privacy.guard.util.UserPermission;
import com.privacy.guard.util.security.AuthUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class SecurityInterceptor implements HandlerInterceptor {
@Autowired
private AuthUtil authUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
return authUtil.verify(request);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
UserPermission.remove();
UserPermission.removeToken();
}
}
- AuthUtil 校验工具类:
import com.privacy.guard.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
@Component
@Slf4j
public class AuthUtil {
public boolean verify(HttpServletRequest request) {
try {
String token = request.getHeader("Authorization");
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
Long userId = JwtUtil.verify(token);
Assert.notNull(userId, "Token不合法,请勿非法访问");
return true;
} catch (Exception e) {
String requestURI = request.getRequestURI();
log.error("鉴权未通过, 请求地址 = {}", requestURI, e);
throw new RuntimeException(e.getMessage());
}
}
}
- JwtUtil 生成 Token 工具类:
import cn.hutool.json.JSONUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import java.util.Map;
@Slf4j
public class JwtUtil {
private static final Algorithm key = Algorithm.HMAC256("abcdjhtsrgarehdgjheras");
/**
* 生成Token
*
* @return
*/
public static String createToken(Long userId) {
String sign = JWT.create()
.withClaim("userId", userId)
.sign(key);
return sign;
}
/**
* 验证Token
*
* @param jwtStr
* @return
*/
public static Long verify(String jwtStr) {
JWTVerifier build = JWT.require(key).build();
try {
//验签
DecodedJWT verify = build.verify(jwtStr);
byte[] bytes = Base64.decodeBase64(verify.getPayload());
String json = new String(bytes);
return Long.parseLong(JSONUtil.toBean(json, Map.class).get("userId").toString());
} catch (AlgorithmMismatchException e) {
//算法错误
log.error("加密算法和解密算法不一致");
} catch (SignatureVerificationException e) {
//验签失败,验签pwd密码错误
log.error("解密密码错误");
} catch (TokenExpiredException e) {
//token过期
log.error("Token过期");
} catch (InvalidClaimException e) {
//获取不到对应的负荷信息
log.error("获取不到对应的负荷信息");
} catch (Exception e) {
log.error("未获取到正确的userId", e);
}
return null;
}
}
- 然后创建一个WebConfig类,继承WebMvcConfigurer,实现请求拦截与过滤配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private SecurityInterceptor securityInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/**
* 所有 /api 开头的接口会被 securityInterceptor 拦截器拦截
* 所有 /auth 开头的接口会被 securityInterceptor 拦截器拦截
* 所有 /api/auth 开头的接口不会被拦截
*/
registry.addInterceptor(securityInterceptor)
.addPathPatterns("/api/**")
.addPathPatterns("/auth/**")
.excludePathPatterns("/api/auth/**");
}
}
4.2 菜单权限控制
在模板工程中,我们已经定义好了菜单表的结构,根据用户权限,查询出用户具有的菜单集合,并进行组装,形成菜单树。(完整代码可下载源码查看)
/**
* 获取路由列表
*/
@Override
public List<RouteVO> listRoutes() {
SysUser user = userService.findByUserId(UserPermission.get());
List<RouteBO> menuList = sysMenuMapper.listRoutes(user.getRoleId());
return buildRoutes(Constants.ROOT_NODE_ID, menuList);
}
/**
* 递归生成菜单路由层级列表
*
* @param parentId 父级ID
* @param menuList 菜单列表
* @return 路由层级列表
*/
private List<RouteVO> buildRoutes(Long parentId, List<RouteBO> menuList) {
List<RouteVO> routeList = new ArrayList<>();
for (RouteBO menu : menuList) {
if (menu.getParentId().equals(parentId)) {
RouteVO routeVO = toRouteVo(menu);
List<RouteVO> children = buildRoutes(menu.getId(), menuList);
if (!children.isEmpty()) {
routeVO.setChildren(children);
}
routeList.add(routeVO);
}
}
return routeList;
}
/**
* 根据RouteBO创建RouteVO
*/
private RouteVO toRouteVo(RouteBO routeBO) {
RouteVO routeVO = new RouteVO();
String routeName = StringUtils.capitalize(StrUtil.toCamelCase(routeBO.getPath().replaceAll("-", "_"))); // 路由 name 需要驼峰,首字母大写
routeVO.setName(routeName); // 根据name路由跳转 this.$router.push({name:xxx})
routeVO.setPath(routeBO.getPath()); // 根据path路由跳转 this.$router.push({path:xxx})
routeVO.setRedirect(routeBO.getRedirect());
routeVO.setComponent(routeBO.getComponent());
RouteVO.Meta meta = new RouteVO.Meta();
meta.setTitle(routeBO.getName());
meta.setIcon(routeBO.getIcon());
meta.setRoles(routeBO.getRoles());
meta.setHidden(StatusEnum.DISABLE.getValue().equals(routeBO.getVisible()));
// 【菜单】是否开启页面缓存
if (MenuTypeEnum.MENU.getValue().equals(routeBO.getType())
&& routeBO.getKeepAlive() != null && 1 == routeBO.getKeepAlive()) {
meta.setKeepAlive(true);
}
// 【目录】只有一个子路由是否始终显示
if (MenuTypeEnum.CATALOG.getValue().equals(routeBO.getType())
&& routeBO.getAlwaysShow() != null && 1 == routeBO.getAlwaysShow()) {
meta.setAlwaysShow(true);
}
routeVO.setMeta(meta);
return routeVO;
}
五、源码下载
前端项目地址:https://gitee.com/dwp1216/boot4j_ui.git
后台项目地址:https://gitee.com/dwp1216/boot4j.git