Supporting video at station b: [Season 2] The simplest but practical SpringBoot+Vue front-end and back-end separation project in the whole network
The second season of SpringBoot+Vue project actual combat
1. Some optimizations
Refresh lost other tabs
-
Cache open tabs
tagsViewCache() { window.addEventListener("beforeunload", () => { let tabViews = this.visitedViews.map(item => { return { fullPath: item.fullPath, hash: item.hash, meta: { ...item.meta }, name: item.name, params: { ...item.params }, path: item.path, query: { ...item.query }, title: item.title }; }); sessionStorage.setItem("tabViews", JSON.stringify(tabViews)); }); let oldViews = JSON.parse(sessionStorage.getItem("tabViews")) || []; if (oldViews.length > 0) { this.$store.state.tagsView.visitedViews = oldViews; } },
-
remove all tagviews on logout
// 注销时删除所有tagview await this.$store.dispatch('tagsView/delAllViews') sessionStorage.removeItem('tabViews')
Two, Swagger integration
Swagger-UI can dynamically generate online API documents based on annotations.
Common Notes
- @Api: Used to modify the Controller class and generate Controller-related document information
- @ApiOperation: Used to modify the methods in the Controller class and generate document information related to interface methods
- @ApiParam: Used to modify the parameters in the interface and generate document information related to interface parameters
- @ApiModelProperty: Used to modify the attributes of the entity class, when the entity class is a request parameter or return result, directly generate relevant document information
Integration steps:
-
add dependencies
<!--Swagger文档工具--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>
-
swagger configuration class
@Configuration @EnableOpenApi @EnableWebMvc public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.OAS_30) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.lantu")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("神盾局特工管理系统接口文档") .description("全网最简单的SpringBoot+Vue前后端分离项目实战") .version("1.0") .contact(new Contact("qqcn", "http://www.qqcn.cn", "[email protected]")) .build(); } }
-
The controller adds swagger annotations as needed
-
Test: http://localhost:9999/swagger-ui/index.html
3. Jwt integration
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact, self-contained way to securely transmit information between parties as JSON objects. This information can be verified and trusted because it is digitally signed.
Example of jwt format:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5MjAzOThjZi1hYThiLTQzNWUtOTIxYS1iNGQ3MDNmYmZiZGQiLCJzdWIiOiJ7XCJwaG9uZVwiOlwiMTIzNDIzNFwiLFwidXNlcm5hbWVcIjpcInpoYW5nc2FuXCJ9IiwiaXNzIjoic3lzdGVtIiwiaWF0IjoxNjc3MTE4Njc2LCJleHAiOjE2NzcxMjA0NzZ9.acc7H6-6ACqcgNu5waqain7th7zJciP-41z-qgWeaSY
⑴ Integration steps
-
pom
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
-
Tools
@Component public class JwtUtil { // 有效期 private static final long JWT_EXPIRE = 30*60*1000L; //半小时 // 令牌秘钥 private static final String JWT_KEY = "123456"; public String createToken(Object data){ // 当前时间 long currentTime = System.currentTimeMillis(); // 过期时间 long expTime = currentTime+JWT_EXPIRE; // 构建jwt JwtBuilder builder = Jwts.builder() .setId(UUID.randomUUID()+"") .setSubject(JSON.toJSONString(data)) .setIssuer("system") .setIssuedAt(new Date(currentTime)) .signWith(SignatureAlgorithm.HS256, encodeSecret(JWT_KEY)) .setExpiration(new Date(expTime)); return builder.compact(); } private SecretKey encodeSecret(String key){ byte[] encode = Base64.getEncoder().encode(key.getBytes()); SecretKeySpec aes = new SecretKeySpec(encode, 0, encode.length, "AES"); return aes; } public Claims parseToken(String token){ Claims body = Jwts.parser() .setSigningKey(encodeSecret(JWT_KEY)) .parseClaimsJws(token) .getBody(); return body; } public <T> T parseToken(String token,Class<T> clazz){ Claims body = Jwts.parser() .setSigningKey(encodeSecret(JWT_KEY)) .parseClaimsJws(token) .getBody(); return JSON.parseObject(body.getSubject(),clazz); } }
-
Test tools
-
Modify login logic
-
test login
Questions to think about:
How does the login subsequent request validate the jwt?
⑵ JWT verification interceptor
define interceptor
@Component
@Slf4j
public class JwtValidateInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("X-Token");
System.out.println(request.getRequestURI() +" 待验证:"+token);
if(token != null){
try {
jwtUtil.parseToken(token);
log.debug(request.getRequestURI() + " 放行...");
return true;
} catch (Exception e) {
e.printStackTrace();
}
}
log.debug(request.getRequestURI() + " 禁止访问...");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(Result.fail(20003,"jwt令牌无效,请重新登录")));
return false;
}
}
register interceptor
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
@Autowired
private JwtValidateInterceptor jwtValidateInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(jwtValidateInterceptor);
registration.addPathPatterns("/**")
.excludePathPatterns(
"/user/login",
"/user/info",
"/user/logout",
"/error",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/**");
}
}
⑶ Swagger authorization configuration
@Configuration
@EnableOpenApi
@EnableWebMvc
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.lantu"))
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securityScheme()))
.securityContexts(Collections.singletonList(securityContext()));
}
private SecurityScheme securityScheme() {
//return new ApiKey("Authorization", "Authorization", "header");
return new ApiKey("X-Token", "X-Token", "header");
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("^(?!auth).*$"))
.build();
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Collections.singletonList(
new SecurityReference("X-Token", authorizationScopes));
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("神盾局特工管理系统接口文档")
.description("全网最简单的SpringBoot+Vue前后端分离项目实战")
.version("1.0")
.contact(new Contact("老蔡", "https://space.bilibili.com/431588578", "[email protected]"))
.build();
}
}
4. Role Management
1. Basic functions
⑴ Preview effect
⑵ front end
role.vue
<template>
<div>
<!-- 搜索栏 -->
<el-card id="search">
<el-row>
<el-col :span="18">
<el-input placeholder="角色名" v-model="searchModel.roleName" clearable> </el-input>
<el-button @click="getRoleList" type="primary" icon="el-icon-search" round>查询</el-button>
</el-col>
<el-col :span="6" align="right">
<el-button @click="openEditUI(null)" type="primary" icon="el-icon-plus" circle></el-button>
</el-col>
</el-row>
</el-card>
<!-- 结果列表 -->
<el-card>
<el-table :data="roleList" stripe style="width: 100%">
<el-table-column label="#" width="80">
<template slot-scope="scope">
{
{(searchModel.pageNo-1) * searchModel.pageSize + scope.$index + 1}}
</template>
</el-table-column>
<el-table-column prop="roleId" label="角色编号" width="180">
</el-table-column>
<el-table-column prop="roleName" label="角色名称" width="180">
</el-table-column>
<el-table-column prop="roleDesc" label="角色描述" >
</el-table-column>
<el-table-column label="操作" width="180">
<template slot-scope="scope">
<el-button @click="openEditUI(scope.row.roleId)" type="primary" icon="el-icon-edit" circle size="mini"></el-button>
<el-button @click="deleteRole(scope.row)" type="danger" icon="el-icon-delete" circle size="mini"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="searchModel.pageNo"
:page-sizes="[5, 10, 20, 50]"
:page-size="searchModel.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
<!-- 对话框 -->
<el-dialog @close="clearForm" :title="title" :visible.sync="dialogFormVisible" :close-on-click-modal="false">
<el-form :model="roleForm" ref="roleFormRef" :rules="rules">
<el-form-item prop="roleName" label="角色名称" :label-width="formLabelWidth">
<el-input v-model="roleForm.roleName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="roleDesc" label="角色描述" :label-width="formLabelWidth">
<el-input v-model="roleForm.roleDesc" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveRole">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import roleApi from '@/api/roleManage'
export default {
data(){
return{
formLabelWidth: '130px',
roleForm: {},
dialogFormVisible: false,
title: '',
searchModel: {
pageNo: 1,
pageSize: 10
},
roleList: [],
total: 0,
rules:{
roleName: [
{ required: true, message: '请输入角色名称', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
]
}
}
},
methods:{
deleteRole(role){
this.$confirm(`您确定删除角色 ${role.roleName} ?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
roleApi.deleteRoleById(role.roleId).then(response => {
this.$message({
type: 'success',
message: response.message
});
this.dialogFormVisible = false;
this.getRoleList();
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
saveRole(){
// 触发表单验证
this.$refs.roleFormRef.validate((valid) => {
if (valid) {
// 提交保存请求
roleApi.saveRole(this.roleForm).then(response => {
// 成功提示
this.$message({
message: response.message,
type: 'success'
});
// 关闭对话框
this.dialogFormVisible = false;
// 刷新表格数据
this.getRoleList();
});
} else {
console.log('error submit!!');
return false;
}
});
},
clearForm(){
this.roleForm = {};
this.$refs.roleFormRef.clearValidate();
},
openEditUI(id){
if(id == null){
this.title = '新增角色';
}else{
this.title = '修改角色';
roleApi.getRoleById(id).then(response => {
this.roleForm = response.data;
});
}
this.dialogFormVisible = true;
},
handleSizeChange(pageSize){
this.searchModel.pageSize = pageSize;
this.getRoleList();
},
handleCurrentChange(pageNo){
this.searchModel.pageNo = pageNo;
this.getRoleList();
},
getRoleList(){
roleApi.getRoleList(this.searchModel).then(response => {
this.roleList = response.data.rows;
this.total = response.data.total;
});
}
},
created(){
this.getRoleList();
}
};
</script>
<style>
#search .el-input {
width: 200px;
margin-right: 10px;
}
.el-dialog .el-input{
width: 85%;
}
</style>
roleManage.js
import request from '@/utils/request'
export default{
// 分页查询角色列表
getRoleList(searchModel){
return request({
url: '/role/list',
method: 'get',
params: {
roleName: searchModel.roleName,
pageNo: searchModel.pageNo,
pageSize: searchModel.pageSize
}
});
},
// 新增
addRole(role){
return request({
url: '/role',
method: 'post',
data: role
});
},
// 修改
updateRole(role){
return request({
url: '/role',
method: 'put',
data: role
});
},
// 保存角色数据
saveRole(role){
if(role.roleId == null || role.roleId == undefined){
return this.addRole(role);
}
return this.updateRole(role);
},
// 根据id查询
getRoleById(id){
return request({
url: `/role/${
id}`,
method: 'get'
});
},
// 根据id删除
deleteRoleById(id){
return request({
url: `/role/${
id}`,
method: 'delete'
});
},
}
⑶ Backend
RoleController
@RestController
@RequestMapping("/role")
public class RoleController {
@Autowired
private IRoleService roleService;
@GetMapping("/list")
public Result<Map<String,Object>> getUserList(@RequestParam(value = "roleName",required = false) String roleName,
@RequestParam(value = "pageNo") Long pageNo,
@RequestParam(value = "pageSize") Long pageSize){
LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasLength(roleName),Role::getRoleName,roleName);
wrapper.orderByDesc(Role::getRoleId);
Page<Role> page = new Page<>(pageNo,pageSize);
roleService.page(page, wrapper);
Map<String,Object> data = new HashMap<>();
data.put("total",page.getTotal());
data.put("rows",page.getRecords());
return Result.success(data);
}
@PostMapping
public Result<?> addRole(@RequestBody Role role){
roleService.save(role);
return Result.success("新增角色成功");
}
@PutMapping
public Result<?> updateRole(@RequestBody Role role){
roleService.updateById(role);
return Result.success("修改角色成功");
}
@GetMapping("/{id}")
public Result<Role> getRoleById(@PathVariable("id") Integer id){
Role role = roleService.getById(id);
return Result.success(role);
}
@DeleteMapping("/{id}")
public Result<Role> deleteRoleById(@PathVariable("id") Integer id){
roleService.removeById(id);
return Result.success("删除角色成功");
}
}
2. Role permission setting display
⑴ front end
menuManage.js
import request from '@/utils/request'
export default{
// 查询所有菜单数据
getAllMenu(){
return request({
url: '/menu',
method: 'get',
});
},
}
role.vue
<el-form-item
prop="roleDesc"
label="权限设置"
:label-width="formLabelWidth"
>
<el-tree
:data="menuList"
:props="menuProps"
node-key="menuId"
show-checkbox
style="width:85%"
default-expand-all
></el-tree>
</el-form-item>
⑵ database
New data in x_menu table
delete from x_menu;
insert into `x_menu` (`menu_id`, `component`, `path`, `redirect`, `name`, `title`, `icon`, `parent_id`, `is_leaf`, `hidden`) values('1','Layout','/sys','/sys/user','sysManage','系统管理','userManage','0','N','0');
insert into `x_menu` (`menu_id`, `component`, `path`, `redirect`, `name`, `title`, `icon`, `parent_id`, `is_leaf`, `hidden`) values('2','sys/user','user',NULL,'userList','用户列表','user','1','Y','0');
insert into `x_menu` (`menu_id`, `component`, `path`, `redirect`, `name`, `title`, `icon`, `parent_id`, `is_leaf`, `hidden`) values('3','sys/role','role',NULL,'roleList','角色列表','roleManage','1','Y','0');
insert into `x_menu` (`menu_id`, `component`, `path`, `redirect`, `name`, `title`, `icon`, `parent_id`, `is_leaf`, `hidden`) values('4','Layout','/test','/test/test1','test','功能测试','form','0','N','0');
insert into `x_menu` (`menu_id`, `component`, `path`, `redirect`, `name`, `title`, `icon`, `parent_id`, `is_leaf`, `hidden`) values('5','test/test1','test1','','test1','测试点一','form','4','Y','0');
insert into `x_menu` (`menu_id`, `component`, `path`, `redirect`, `name`, `title`, `icon`, `parent_id`, `is_leaf`, `hidden`) values('6','test/test2','test2','','test2','测试点二','form','4','Y','0');
insert into `x_menu` (`menu_id`, `component`, `path`, `redirect`, `name`, `title`, `icon`, `parent_id`, `is_leaf`, `hidden`) values('7','test/test3','test3','','test3','测试点三','form','4','Y','0');
⑶ Backend
Added in the Menu class
@TableField(exist = false)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<Menu> children;
@TableField(exist = false)
private Map<String,Object> meta = new HashMap<>();
public Map<String,Object> getMeta(){
meta.put("title",this.title);
meta.put("icon",this.icon);
return this.meta;
}
MenuController
@RestController
@RequestMapping("/menu")
public class MenuController {
@Autowired
private IMenuService menuService;
@GetMapping
public Result<?> getAllMenu(){
List<Menu> menuList = menuService.getAllMenu();
return Result.success(menuList);
}
}
MenuSeviceImpl
@Override
public List<Menu> getAllMenu() {
// 一级菜单
LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper();
wrapper.eq(Menu::getParentId,0);
List<Menu> menuList = this.list(wrapper);
// 子菜单
setMenuChildren(menuList);
return menuList;
}
private void setMenuChildren(List<Menu> menuList) {
if(menuList != null) {
for (Menu menu:menuList) {
LambdaQueryWrapper<Menu> subWrapper = new LambdaQueryWrapper();
subWrapper.eq(Menu::getParentId, menu.getMenuId());
List<Menu> subMenuList = this.list(subWrapper);
menu.setChildren(subMenuList);
// 递归
setMenuChildren(subMenuList);
}
}
}
3. Submission of new role permissions
⑴ front end
⑵ Backend
4. Role permission echo
⑴ front end
⑵ Backend
RoleMenuMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lantu.sys.mapper.RoleMenuMapper">
<select id="getMenuIdListByRoleId" parameterType="Integer" resultType="Integer">
select
a.`menu_id`
from x_role_menu a, x_menu b
where a.`menu_id` = b.`menu_id`
and b.`is_leaf` = 'Y'
and a.`role_id` = #{roleId}
</select>
</mapper>
5. Role permission modification submission
⑴ Backend
Added RoleServiceImpl
@Override
@Transactional
public void updateRole(Role role) {
// 更新role表
this.updateById(role);
// 清除原有权限
LambdaQueryWrapper<RoleMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(RoleMenu::getRoleId,role.getRoleId());
roleMenuMapper.delete(wrapper);
//新增权限
for (Integer menuId : role.getMenuIdList()) {
roleMenuMapper.insert(new RoleMenu(null,role.getRoleId(),menuId));
}
}
6. Delete related permissions when role is deleted
⑴ Backend
5. User role setting
1. Role display
⑴ front end
roleManage.js
// 查询所有角色列表
getAllRole(){
return request({
url: '/role/all',
method: 'get'
});
},
user.vue
⑵ Backend
RoleController
2. Submit roles when adding new users
⑴ Backend
UserServiceImpl
3. Character echo
⑴ Backend
UserServiceImpl
4. Submit roles when modifying users
⑴ Backend
UserServiceImpl
5. Delete related roles when users are deleted
⑴ Backend
UserServiceImpl
Six, dynamic routing
1. Query the menu according to the user
⑴ Backend
MenuMapper.xml
<select id="getMenuListByUserId" resultType="Menu">
SELECT *
FROM x_menu a,
x_role_menu b,
x_user_role c
WHERE a.`menu_id` = b.`menu_id`
AND b.`role_id` = c.`role_id`
AND a.`parent_id` = #{pid}
AND c.`user_id` = #{userId}
</select>
yml
type-aliases-package: com.lantu.*.entity
MenuMapper.java
public interface MenuMapper extends BaseMapper<Menu> {
public List<Menu> getMenuListByUserId(@Param("userId") Integer userId,
@Param("pid") Integer pid);
}
MenuServiceImpl
@Override
public List<Menu> getMenuListByUserId(Integer userId) {
// 一级菜单
List<Menu> menuList = this.getBaseMapper().getMenuListByUserId(userId, 0);
// 子菜单
setMenuChildrenByUserId(userId, menuList);
return menuList;
}
private void setMenuChildrenByUserId(Integer userId, List<Menu> menuList) {
if (menuList != null) {
for (Menu menu : menuList) {
List<Menu> subMenuList = this.getBaseMapper().getMenuListByUserId(userId, menu.getMenuId());
menu.setChildren(subMenuList);
// 递归
setMenuChildrenByUserId(userId,subMenuList);
}
}
}
Return data through the user/info interface
UserServiceImpl
2. Front-end dynamic routing processing
⑴ Modify the original routing configuration
src\router\index.js, keep the basic routing, delete or comment other
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: {
title: '首页', icon: 'dashboard', affix:true ,noCache: false}
}]
},
]
⑵ Get menu data and save it to Vuex
src\store\modules\user.js
src\store\getters.js
⑶ Routing conversion
Modify permission.js in the src directory
import Layout from '@/layout'
// 路由转换
let myRoutes = myFilterAsyncRoutes(store.getters.menuList);
// 404
myRoutes.push({
path: '*',
redirect: '/404',
hidden: true
});
// 动态添加路由
router.addRoutes(myRoutes);
// 存至全局变量
global.myRoutes = myRoutes;
next({
...to,replace:true}) // 防止刷新后页面空白
function myFilterAsyncRoutes(menuList) {
menuList.filter(menu => {
if (menu.component === 'Layout') {
menu.component = Layout
console.log(menu.component);
} else {
menu.component = require(`@/views/${
menu.component}.vue`).default
}
// 递归处理子菜单
if (menu.children && menu.children.length) {
menu.children = myFilterAsyncRoutes(menu.children)
}
return true
})
return menuList;
}
⑷ route merge
src\layout\components\Sidebar\index.vue
Test the expected results. Users with different roles will display different menu lists after logging in.
So far, although the dynamic menu function has been realized, the security problem has not been solved. You can think about what problems exist?