项目介绍
VueAdmin - 前后端分离后台管理系统
前端VueCli3,Vue2.x
后台SpringBoot2.4
Markerhub:
线上演示:https://www.markerhub.com/vueadmin
登录密码:1234567
前端笔记:https://shimo.im/docs/pxwyJHgqcWjWkTKX/
后端笔记:https://shimo.im/docs/OnZDwoxFFL8bnP1c/
源码分享:
https://github.com/markerhub/vueadmin
https://gitee.com/markerhub/VueAdmin
视频讲解:https://www.bilibili.com/video/BV1af4y1s7Wh/
Spring Security做JWT认证和授权
好文:https://www.jianshu.com/p/d5ce890c67f7
但是本项目和他的不一样,我们没有采用让AuthenticationManager去调用具体自定义的Provider类处理,我们是自己在JwtAuthenticationFilter处理jwt的逻辑。
问题
路由
1.动态加载路由的时候
import('...')
- 我在动态路有点的时候直接写出完整的文件加载路径导致出错
- ...中路径不能直接用变量替换,必须动态拼接,'@/views'+路径变量+'.vue'
- addRoute推荐使用,是添加路由的方法,直接往路由表的某个children数组push没用,必须最后走addRoute
全局守卫
- 无限跳转,判断url'/login'出错了,导致了无限循环
- 确保全局守卫中只调用1次next()
async await
- 带url刷新页面的时候,例如/sys/user,无法进入页面
可能是因为全局路由守卫,在你去获取路由数组的时候已经调用了next,导致找不到路由。使用了async await,等待异步获取路由并且加载路由之后再next
router.beforeEach(async (to, from, next) => { const hasRoutes = store.state.hasRoutes; if (!hasRoutes) { //参数的next是留给操作完再调用 if (await addRoutes(next)) { } else { next(); } } else { next(); } });
Vuex
登出重置VUEX STATE
在mutation中使用Object.assign(state,getDefaultState())
- 直接
state=getDefaiState()
不起作用不知道为什么
- 直接
- getDefaultState()是一个定义返回state初始对象的函数
- action中异步操作登出api,在调用mutation赋值为初始state
- 清除state详见:https://stackoverflow.com/questions/42295340/how-to-clear-state-in-vuex-store
菜单页面 MENU
菜单修改页面
提交表单的时候需要清空表单内容不能直接调
this.refs[formRef].resetFields()
//并不是将表单中的props直接清空 //将所有字段值重置为开始修改之前的初始值并移除校验结果 //在这可调可不调 this.resetForm(formName); //需要手动清空 this.editForm = {};
- 消息气泡官方文档点击之后的确认事件写的有问题,文档写的是
confirm
查看源码实际上是onConfirm
用户和角色页面
对话框不展示的情况下无法获取到其组件
- 必须先设置其展示
而且要放在axios外层,可能是初始化需要时间
//1.展示角色表单对话框 //必须放在axios外层,可能是初始化需要时间 this.roleDialogVisible = true; //2.获取该用户的角色 this.$axios.get('/sys/user/info/' + userId).then(res => { console.log(res); //2.1将其有的角色渲染到角色树上 this.$refs.roleTree.setCheckedKeys(res.data.data.roles) //2.2 把当前角色的角色数据赋值给角色表单保存 this.roleForm.personalData = res.data.data.roles; });
跨域问题
前端访问验证码的时候老是出现下面的错误
import axios from 'axios' import router from "../router"; import el from 'element-ui'; //错误在这里没有加http axios.defaults.baseURL = 'http://localhost:8081' // create an axios instance const request = axios.create({ timeout: 5000, // axios timeout, headers: { 'Content-Type': "application/json; charset=utf-8", 'Access-Control-Allow-Origin': '*' //cors错误是因为请求头没加Access-Control-Allow-XXX信息 } })
在给axios设置baseURL的时候没有加http://
前缀,导致出错
HttpServletRequest获取post方法的参数
Spring Security采用的是默认form登录,我们前端是用的json提交的body不能这样做,只能把参数放到url上。不然get不到。
post方法不能直接getParameter(key),要使用request.getReader()
读取成字符串。
后面获取参数我使用的是fastjson转成map再读取参数
//校验验证码逻辑
private void validate(HttpServletRequest request) throws IOException {
//1.获取验证码和key
//key=验证码的唯一标识
//post方法不能直接getParameter
//把读取出来的json字符串参数再用fastjson转成map获取参数
BufferedReader br = request.getReader();
String str = "";
String paramString = "";
while ((str = br.readLine()) != null) {
paramString += str;
}
HashMap<String,String> paramMap = JSON.parseObject(paramString, HashMap.class);
System.out.println(paramMap );
String key = paramMap.get("key");
String captchaCode = paramMap.get("captchaCode");
//...
}
后台返回给前端的动态子菜单列表和权限接口
JwtAuthenticationFilter
,父类是BasicAuthenticationFilter
每次访问非登录接口的时候,我们都会对header
中的jwt
进行校验,我们在20行开始获取了用户的信息,并在方法中通过redis查找了用户的权限,设置在了Authentication
的实现类UsernamePasswordAuthenticationToken
这个类中存放了主体Principal
和权限GrantedAuthority
集合
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//请求头中获取jwt,header中的jwt参数是Authentication
String jwt = request.getHeader(jwtUtils.getTokenName());
//没有jwt交给后面过滤器处理
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
//解析jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token过期,重新登录");
}
//jwt验证成功取出主体,主体中存放的是用户名和用户id的json字符串
String userInfo = claim.getSubject();
SysUser sysUser = JSON.parseObject(userInfo, SysUser.class);
//根据用户id获取用户拥有的权限
List<GrantedAuthority> authInfo = userDetailService.getUserAuthorities(sysUser.getId());
//把用户的id和权限放置在Authentication中,UsernamePasswordAuthenticationToken是1个Authentication实现类
//用户id是放在principal主体中的
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(sysUser.getId(), null, authInfo);
//不加这句代码,会进入到AuthenticationEntryPoint,代表了jwt认证失败
SecurityContextHolder.getContext().setAuthentication(token);
//进入下一个过滤器
chain.doFilter(request, response);
}
SysUserController
通过Security传过来的Principal主体可以获取到在JwtAuthenticationFilter extends BasicAuthenticationFilter
这个类中设置的用户信息,我们在这设置的是用户id,方便service调用接口。
/**
*获取前端子菜单列表和其拥有权限
* @return
*/
@GetMapping("/menu")
public Result menu(Principal principal){
String id = principal.getName();
Long userId = Long.valueOf(id);
//1.获取权限信息
String authInfo = sysUserService.getUserAuthInfo(userId);
String[] authArr = StringUtils.tokenizeToStringArray(authInfo, ",");
//2.获取导航栏信息
List<SubMenuDto> subMenuDtos = sysUserService.getCurrentUserSubMenuList(userId);
//3.返回结果
return Result.success(MapUtil.builder()
.put("permList", authArr)
.put("subMenuList", subMenuDtos).build());
}
SysUserServiceImpl
这个类有3个方法通过用户id转化成用户的前端子菜单列表
getCurrentUserSubMenuList()
在
sys_menu
表中获取到用户的List<SysMenu>
(sys_menu
表中存储的是用户的权限信息:type
字段包括 0:目录 1:菜单 2:按钮)buildMenuTree()
因为我们需要的是树结构(目录->菜单->按钮的父子链)我们在
SysMenu
中添加了一个children
属性,所以可以把查出来的List<SysMenu>
通过遍历查找每个SysMenu
的子节点构建成树结构convert()
因为
SysMenu
的属性和前端不一致所以我们需要把第2步构建的树结构集合转成前端的树结构集合,新建了个SubMenuDto
映射对象,这一步就是List<SysMenu>
到List<SubMenuDto>
/**
* 获取用户的子菜单列表
* @param userId 用户id
* @return SubMenuDto,对应前端子菜单列表的集合
*/
@Override
public List<SubMenuDto> getCurrentUserSubMenuList(Long userId) {
//1.获取用户对应的权限列表
List<Long> menusIds = sysUserMapper.getMenusIds(userId);
List<SysMenu> menus = sysMenuService.listByIds(menusIds);
//2.转成DTO对象
//2.1 SysMenu 转成 SysMenuTree对象
List<SysMenu> sysMenuTree = buildMenuTree(menus);
//2.2转化成Dto集合也就是对应前端数据的树返回
return convert(sysMenuTree);
}
/**
* 把SysMenu实体类列表转化成树状结构
* @param menus SysMenu集合
* @return SysMenu树结构集合,和SysMenu一样只是children属性形成了一条父子链一样被注入了,因为chilren属性也是List<SysMenu>
*/
private List<SysMenu> buildMenuTree(List<SysMenu> menus) {
//用于存放SysMenu树的集合
List<SysMenu> sysMenuTree = new ArrayList<>();
//1.遍历每一个SysMenu
for (SysMenu menu : menus) {
//2.继续第二次遍历,找到父循环遍历元素的子节点
//根据id判断
for (SysMenu m : menus) {
//不能用==因为是包装类,有缓存127以后就不行了
if (m.getParentId().equals(menu.getId())){
menu.getChildren().add(m);
}
}
//3.每次父循环遍历完成判断是不是根节点,是就放入树集合中
if (menu.getParentId() == 0 )
sysMenuTree.add(menu);
}
return sysMenuTree;
}
/**
* 把SysMenu的树状结构对应到前端属性
* @param sysMenuTree SysMenu树结构,被填充了children属性的SysMenu
* @return
*/
private List<SubMenuDto> convert(List<SysMenu> sysMenuTree) {
//新建用于存储前端接收的集合菜单树subMenuDtos
List<SubMenuDto> subMenuDtoTree = new ArrayList<>();
//1.把sysMenuTree转成subMenuDtos
sysMenuTree.forEach(menuTree -> {
SubMenuDto subMenuDto = new SubMenuDto()
.setId(menuTree.getId())
.setName(menuTree.getPerms())
.setTitle(menuTree.getName())
.setIcon(menuTree.getIcon())
.setPath(menuTree.getPath())
.setComponent(menuTree.getComponent());
//2.如果menuTree有子节点,递归调用当前方法
if (menuTree.getChildren().size() > 0){
subMenuDto.setChildren(convert(menuTree.getChildren()));
}
//3.每次遍历完成添加到前端的dto集合菜单树
subMenuDtoTree.add(subMenuDto);
});
return subMenuDtoTree;
}
后台字段校验
SpringBott2.4之后必须添加,spring-boot-validation依赖。
给实体类属性添加校验注解如String类型可以添加@NotBlank,不能为空。引用类型例如@NotNull不能为空。
在接口中给实体参数前添加@Validate注解代表需要验证该实体类
@PreAuthorize("hasAuthority('sys:menu:save')")
@PostMapping("/save")
public Result save(@Validated @RequestBody SysMenu sysMenu){
//设置创建事件
sysMenu.setCreated(LocalDateTime.now());
sysMenuService.save(sysMenu);
return Result.success(sysMenu);
}
权限校验
前端Vue
注册全局校验方法,把获取的权限存在store中,页面动态渲染根据该方法
import Vue from 'vue';
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
methods: {
hasPerm(perm){
let perms = this.$store.state.user.permList;
return perms.indexOf(perm) > -1;
}
}
})
页面渲染,v-if判断方法:
<el-form-item v-if="hasPerm('sys:user:save')">
<el-button type="primary" @click="userDialogVisible=true">新增</el-button>
</el-form-item>
后台Spring Security
每次访问非白名单接口的时候,都会在jwt认证类中通过redis获取其权限,再和接口上的权限注解比对,如果没有权限注解代表可以直接访问。
需要接口校验就在接口中加上@PreAuthorize(xxx)
@PreAuthorize("hasAuthority('xxx')")
:对权限校验@PreAuthorize("hasRole('xxx')")
:对角色校验
@PreAuthorize("hasAuthority('sys:menu:save')")
@PostMapping("/save")
public Result save(@Validated @RequestBody SysMenu sysMenu){
//设置创建事件
sysMenu.setCreated(LoHcalDateTime.now());
sysMenuService.save(sysMenu);
return Result.success(sysMenu);
}
@PreAuthorize("hasRole('admin')")
@GetMapping("/test")
public Object test() {
List<SysUser> sysUsers = userService.list();
return Result.success(sysUsers);
}
包装类型
SysUserServiceImpl
中的buildMenuTree()
权限转化成前端Dto所需的树结构用于渲染权限树角色树,包装类型Id是Long,在比较是不是父子关系的时候用了==可能会出现问题,因为包装类型的缓存是[-127,128]必须用equals不能用\==
异常处理
GlobalExceptionHandler
字段校验,业务处理抛出异常都在这个类中处理,AOP。
Mybtais-plus
不用再写基本的增删改查,提高效率。
集合的stream方法
集合的stream方法非常方便和js的map、filter、reduce如出一辙非常方便和js的map、filter、reduce如出一辙,代码写起来简洁高效。
直接collection.stream()可以转化成流,然后有对应的流方法可以快速完成我们想实现的效果,最后将流可以转化成集合collect方法
整合Swagger2
一个框架,直接在接口上写注释,可以直接生成接口文档。
项目部署
单项目和多项目的部署不一样,因为一直无法实现像多项目那样通过路径去访问,所以最后项目vueadmin是部署在单项目中的。参考的是markerhub的部署方式
以下写的部署方式是不正确的,尝试多项目部署的方式,但是在具体的uri下刷新会404,仅作记录使用,复习单项目部署直接去看marerhub的视频。
单项目
直接部署在nginx的html文件夹中,没有单独一个文件夹去包裹vue代码
多项目
每个项目在html文件夹下以一个文件夹去包裹项目代码,通过项目路径访问
可以搜索"Vue多项目部署"
https://blog.csdn.net/weixin_38023551/article/details/88640939
nginx的alias和base实际访问路径不一样
- alias:替换掉uri
- base:uri+base
docker nginx容器部署vueadmin项目
我们想通过服务器ip+项目路径的方式访问网页
- 所以我们需要在根目录添加
vue.config.js
文件指定静态资源的访问路径
module.exports = {
publicPath:'/vueadmin'
}
在路由
index.js
中添加base
属性- 修改请求路径
打包项目
到根目录dist文件夹
npm run build
修改名称为dist文件夹名称为vueadmin,并压缩成zip
使用sftp上传zip文件
解压在挂在目录下的html文件夹下
添加配置文件
在挂在目录下的conf.d中添加vueadmin.conf配置文件
server {
listen 80;
server_name localhost;
#charset koi8-r;
access_log /var/log/nginx/host.access.log main;
error_log /var/log/nginx/error.log error;
#ip+/vueadmin可以访问项目
location /vueadmin {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location /api/ { # 匹配api接口,进行转发配置
rewrite /api/(.*) /$1 break;
proxy_pass http://45.647.88.99:8000; #这是重点,转发到你的后端接口
}
}
:information_source:注意: docker 容器无法解析域名,访问接口须使用ip或者去搜索解决域名解析问题。
docker部署springboot后台
1.编写dockerfile文件
# Docker image for springboot file run
# 基础镜像使用java
FROM java:8
# 作者信息
MAINTAINER whyat <whyat@foxmail.com>
# 暴露8081端口,可以被映射到8081
EXPOSE 8081
# 添加jar包到容器
ADD vueadmin-java-0.0.1-SNAPSHOT.jar vueadmin.jar
# 运行jar包
RUN bash -c 'touch /vueadmin.jar'
ENTRYPOINT ["java","-jar","/vueadmin.jar"]
2.制作镜像
-t 参数是指定此镜像的tag名
制作完成可以通过docker images
查看镜像名称
docker build -t 镜像名称 .
3.运行容器
外部映射内部
- -d参数是让容器后台运行
- -p 是做端口映射,此时将服务器中的8081端口映射到容器中的8081(项目中端口配置的是881)端口\
- --name别名
docker run --name vueadmin-springboot -d -p 8081:8081 镜像名称
访问项目
http://www.whyat.top/vueadmin/