【开源风云】从若依系列脚手架汲取编程之道(四)
📕开源风云系列
- 🍊本系列将从开源名将若依出发,探究优质开源项目脚手架汲取编程之道。
- 🍉从不分离版本开写到前后端分离版,再到微服务版本,乃至其中好玩的一系列增强Plus操作。
- 🍈希望你具备如下技术栈:
- 🍎Spring
- 🍍SpringMVC
- 🍐Mybatis/Mybatis-plus
- 🍅Thymeleaf
- 🥝SpringBoot
- 🍓Shiro
- 🍏SpringSecurity
- 🍌SpringCloud
- 🍒云服务器相关知识
- 本篇是分离版第一篇
- 文章同时同步到我的个人站点🍊欢迎来访!:
- 🍎国内:https://www.linxiaoqin.netlify.app
- 🍏国际:https://www.linxiaoqin.vercel.app
目录
- 📕开源风云系列
- 1、若依践行SpringSecurity
- 1.1、Apifox如何调接口
- 2、RBAC
- 2.1、权限
- 2.2、何时获得权限
- 2.3、页面权限的获取
- 2.4、按钮权限
- 3、第三方登录
- 4、新建模块
- 5、前端通用方法
- 5.1、$tab
- 5.1.1、打开新标签页
- 5.1.2、修改标签页
- 5.1.3、关闭标签
- 5.1.4、刷新标签
- 5.1.5、关闭所有页签
- 5.1.6、关闭左侧页签
- 5.2、$modal
- 5.3、$auth
- 5.4、$download
- 5.4.1、根据名称下载`upload`路径下的文件
- 6、Https实现
1、若依践行SpringSecurity
若依前后端分离采用SpringSecurity来做认证和授权操作,使用SpringSecurity,我们只需要authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password))
即:
- 认证管理器调用认证方法,将我们的用户名和密码封装成
UsernamePasswordAuthenticationToken
传入认证方法 - 认证管理器调用认证方法执行后会自动调用
UserDetailsServiceImpl.loadUserByUsername()
方法,我们自己写的类实现了UserDetailsService
接口,并重写loadUserByUsername
方法
我们简单 debug 认证方法一下看看,可以发现的是:
- 在用户登录的时候,对于任何异常都会记录进日志
- 然后获取当前的登录对象,通过它来生成token
其中recordLoginInfo(loginUser.getUserId())
是记录登录的信息的,包括登录的 ip、登录时间,然后将信息更新到数据库里面,我们可以进去方法里面查看
那么最后在tokenService.createToken(loginUser)
生成token是怎么生成的呢?
首先生成一个uuid,将uuid设置给LoginUser
的token字段,这个uuid其实也就是tokenid,setUserAgent(loginUser)
设置用户代理,这个用户代理干了什么呢?
这个设置用户代理方法其实就是获得了很多信息,包括操作系统、浏览器信息、ip等,然后将其更新进数据库中。
最后执行refreshToken(loginUser)
刷新令牌有效期,首先设置登录时间为系统当前时间,设置过期时间,expireTime
时间是读取yml配置的信息,然后将键值存入 redis 中,键是userKey
,值是loginUser
。
再回头来看我们创建令牌的代码,创建一个HashMap,将键值对login_user_key:token
存入HashMap,然后创建token,并将HashMap传入。
我们进入createToken
方法,设置主体信息为claims
,也就是我们上面HashMap的键值对,然后设置签名方式,加密方式为HS512,密钥secret通过yml配置文件读取,密钥随便填即可。
1.1、Apifox如何调接口
现在有一个这样的问题,遇到认证失败,但是自己想用postman、apifox、apipost等软件就想要调通API,怎么办?可以使用超级管理员的token,当我们登录超级管理员,随便请求一个页面,就可以在两个地方发现token:
- 一个是请求头的
Authorization
- 另一个是Cookie里面
包括Swagger,也需要添加token才能调试的通
2、RBAC
RBAC是一种基于角色权限控制,进而控制用户的权限。也就是给用户角色,给角色权限
具体而言,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。
这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。
2.1、权限
- 页面权限,就是进入某个页面所需要的权限
- 按钮权限则是能点击某个按钮所需要的权限
对于权限,在程序中往往表现为一个字符串,比如说system:user:add
表示对用户具有增加权限,当然也可以说dsadsaddsadsadsa
表示对用户有增加权限,字符串是随意定的,但是为了符合人类的想法,要做到尽量的见名知意。
对于若依而言,在系统管理→菜单管理对每一个菜单对应了某种权限,如下图所示:
下面我们以岗位管理为例子,说明岗位管理的每个字符串的作用,岗位管理的权限字符串如上:上图的岗位管理对应的字符串为system:post:list
,该字符串的意思是,有了这个字符串才能查看岗位管理的菜单。
system:post:query
字符串表示对单个岗位详情的查询权限system:post:add
表示增权限system:post:edit
表示修改权限system:post:remove
表示删岗位权限system:post:export
表示导出岗位数据Excel文件的权限
岗位管理的system:post:list
是页面权限,如果没有该权限,连菜单都看不到,自然页面也看不到。其他的都是按钮权限,
按钮权限在前端页面的表现就是能不能看到按钮,在后端的表现就是能不能执行按钮对应的controller的增删改查方法。
在若依中,页面菜单、目录菜单和按钮菜单基本上都是存在数据库中(首页、404等特殊页面除外),在系统管理→菜单管理可以填写每一个菜单对应的权限字符串,不过目前目录菜单不能添加权限字符串。
当我们在菜单管理写好权限字符串之后,在角色管理我们可以添加角色,并把相关的权限赋予给角色,然后给用户分配角色。
[!NOTE]
给用户赋予角色,给角色赋予权限。
2.2、何时获得权限
若依用户是在 登录的时候就在数据库得到了属于自己的权限,并将其存在了 redis 中。
我们来看一下代码:在用户验证处理UserDetailsServiceImpl
类中,创建了LoginUser
,并且传入了对应的权限。
permissionService.getMenuPermission(user)
则是根据当前登录的用户查询该用户的权限,转到方法进去看看:
从上面的代码可以看到,如果是超级管理员就给权限字符串为*:*:*
,那么什么样的人是超级管理员呢?
userId=1
的用户就是超级管理员,也就是说超级管理员是代码写死的,前端判断字符串也要先看看权限字符串是不是*:*:*
。如果不是超级管理员,代码是执行了perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()))
,也就是获得当前用户的userId,拿着userId去数据库查询权限。
查看Service层的代码,可以发现,调用Mapper去数据库查询权限,迭代查询的每个权限并且split分割了一下,通过,
逗号分隔,所以我们写权限的时候可以这样写system:user:add,system:user:edit
,可以在一个菜单的权限标识写好几个权限字符串,用逗号分割。
话说回来,去数据库怎么查询的权限呢?我们看到传入的只有userId,查看SQL语句,并手动在 navicat 运行userid=2的用户权限SQL:
可以看到SQL实际上就是不断在左连接,所谓左连接通俗而言就是左边表展示全部数据,右边表若匹配成功就连接,没有匹配成功就右边表空着。
那么这里就涉及到一个问题:当我用超级管理员修改了角色的权限,对于已经登录的和该角色相关联的用户,是不是立刻生效呢?
- 其实对于页面权限是立刻生效的,但是对于按钮权限并不是立刻生效的,而是要重新登录重新从数据库获取权限,然后再存入Redis
2.3、页面权限的获取
当用户登录完毕之后,左侧菜单栏展示出来了大量的菜单,对于不同权限的用户,展示的菜单不尽相同。那么,左侧菜单是怎么来的呢?这就需要打开Layout
组件,然后找到侧边栏组件,如下:
Layout组件的菜单栏组件是Sidebar,里面有一个ElMenu组件(对应整个菜单整体),SidebarItem全部都是一级菜单,ElSubmenu 对应我们的二级菜单整体,里面的SidebarItem组件为具体的每一个二级菜单。
ElMenu # 整体菜单-- SidebarItem # 一级菜单-- ElSubmenu # 二级菜单
如下图,我们可以发现,ElMenu中的菜单项的迭代数据来自变量sidebarRouters
,
sidebarRouters
变量的数据一定是后端查询来的菜单数据。我们继续深入,它是vuex的getters:
查看Vuex的目录下的 store/getters.js
sidebarRouters:state => state.permission.sidebarRouters,
可以发现,sidebarRouters 的数据是从 permission.js
中管理的,我们进去permission.js
中查看:发现sidebarRouters
初始化是空的,是被SET_SIDEBAR_ROUTERS
方法赋值的
我们继续深入SET_SIDEBAR_ROUTERS
方法,会发现这么一句代码:
constantRoutes.concat(sidebarRoutes)
也就是我们的sidebarRouters
的数据,是由constantRoutes
拼接了sidebarRoutes
其中constantRoutes
点进去发现其实它就是公共路由,不存在数据库中,任何角色都可以有:
从数据库中取的sidebarRoutes
数据其实是请求后端返回的data
数据
![NOTE]
sidebarRouters的数据是来自两部分:
- 一部分来自getRouters方法向后端发请求,返回的res的data经过filterAsyncRouter方法过滤后的路由
- 另一部分来自constantRoutes,constantRoutes是写死的,来自router文件夹下的index.js文件
前端点击岗位管理的时候,调用了岗位管理的system/post/list
接口,调用该接口是需要权限的,并且权限为system/post/list
,每一个接口的调用需要什么权限,我们都可以在上面的注解可以看到
![NOTE]
@PreAuthorize注解是SpringSecurity框架的注解,注解的value值所需要填写一个表达式,如果表达式计算的结果是true,那么就允许访问对应的接口,如果表达式计算的结果是false,则表示没有权限。
当我们需要用到@PreAuthorize注解,先需要写另一个注解来让@PreAuthorize注解能生效,ry程序在SecurityConfig类已经写了
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
表示**@PreAuthorize和@PostAuthorize注解要不要启用**@PreAuthorize表示在执行方法前验证权限,@PostAuthorize表示在执行方法后验证权限。一般用@PreAuthorize就够了。
刚刚我们说到,@PreAuthorize的value值写SpEL表达式,并且返回值为true则可以执行方法,返回值为false则表示不能执行方法。现在我们来分析SpEL表达式。SpEL表达式能写方法调用,ry程序就是写的方法表达式。岗位管理的表达式如下:
@PreAuthorize("@ss.hasPermi('system:post:list')")
SpEL表达式可以通过@
来引用bean,所以@ss
在引用名为ss的Bean,然后调用该Bean的hasPermi方法,并传入了参数。@ss
的bean为:
综上所述,表达式@ss.hasPermi('system:post:list')
的意思是调用下面的方法,并传参数为system:post:list
,如下:
2.4、按钮权限
按钮权限在前端的表现为vue组件、HTML标签是否显示。在ry程序中,最常用的方式就是控制按钮显示还是不显示,如下:
也就是说,当用户具有system:post:add
权限时,新增的<el-button>
组件才会显示。
v-hasPermi
是一个新指令,vue本身自身没有的,那么只能是ry自己写的一个新指令。ry所有自定义指令在src/directive
下,在main.js引入了src/directive/index.js
import directive from './directive' // directive
Vue.use(directive)
自定义指令是以插件的形式包装的,Vue.use方法会自动执行插件directive的install方法:
install方法中,我们看到v-hasPermi被注册。
可以看出,对象里面只有一个方法inserted,也就是在被绑定元素插入父节点时调用,这里正好可以判断有没有权限,有就插入,没有就直接移除dom即可。
3、第三方登录
当今社会,微信登录、QQ登录、抖音登录等等三方登录已经层出不穷,学会三方登录势在必行。微信登录要认证开发者,必须为企业,个人不行,而且还要交300块钱。一听到钱,白嫖党的钱是不可能被赚的,不学微信登录。QQ登录也要申请、微博登录也要申请。
还好Gitee给力,申请轻轻松松,谁都能轻松让Gitee作为第三方登录,JustAuth
能让我们第三方登录写少一些代码,它包装了国内外30多种三方登录。
使用教程:这应该是史上最全的整合第三方登录的开源库
使用JustAuth总共分三步(这三步也适合于JustAuth支持的任何一个平台):
- 申请注册第三方平台的开发者账号 - 设置 - 第三方应用
- 创建应用
-
应用名称
一般填写自己的网站名称即可 -
应用描述
一般填写自己的应用描述即可 -
应用主页
填写自己的网站首页地址 -
应用回调地址
重点,应用回调不能乱填,当我们gitee登录成功之后,gitee会自动跳转到应用回调地址,并且gitee会带上code,利用code可以得到所登录gitee用户信息。 -
权限
根据页面提示操作,默认勾选第一个就行。
以上信息输入完成后,点击确定按钮创建应用。创建完成后,点击进入应用详情页,可以看到应用的密钥等信息
- 引入依赖
<!--JustAuth第三方登录-->
<dependency><groupId>me.zhyd.oauth</groupId><artifactId>JustAuth</artifactId><version>${justAuth.version}</version>
</dependency>
- 我们可以在根的pom.xml引入
- 必须要在framework包下继续引入,但是不用带版本号,会自动继承根的pom.xml,否则Maven一直报错
- 接下来我们改
login.vue
<!-- Gitee登录 -->
<div style="width: 32px;height: 32px;margin-top: 5px;cursor: pointer;" title="利用Gitee登录" @click="giteeLogin"><img style="height: 100%;width: 100%;" src="../assets/logo/profile.jpg">
</div>
我们添加的代码如图,@click="giteeLogin"
点击事件的方法是giteeLogin
页面呈现的样子是:
我们看一下点击事件giteeLogin
,将其补充在 methods 下:
// Gitee登录
giteeLogin() {PreLoginByGitee().then(res => {Cookies.set("user-uuid", res.uuid)window.location = res.authorizeUrl})
},
我们在src/api/login.js
里面写入PreLoginByGitee
方法
// 获得跳转到Gitee登录的地址
export function PreLoginByGitee() {return request({url: '/PreLoginByGitee',headers: {isToken: false},method: 'get',})
}
对应的后端接口:
@RestController
public class GiteeLoginController {// 创建授权request@GetMapping("/PreLoginByGitee")public AjaxResult PreLoginByGitee() {AjaxResult ajax = AjaxResult.success();AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder().clientId("clientId").clientSecret("clientSecret").redirectUri("redirectUri").build());String uuid = IdUtils.fastUUID();String authorizeUrl = authRequest.authorize(uuid);//返回给前端一个跳转路径url和uuidajax.put("authorizeUrl", authorizeUrl);ajax.put("uuid", uuid);return ajax;}
}
这样就OK了,但是后端接口还得放行:
我们点击同意授权,就会调用我们的回调地址:http://localhost/callback
,并且带上code和state,也就是会跳转到http://localhost/callback&code=xxx&state=xxx
,这个 state 其实就是 uuid,我们在前端创建路由,让前端回调到我们的loginByGitee.vue
组件
// Gitee
{path: '/callback',component: () => import('@/views/loginByGitee'),hidden: true
},
同时我们也准备好了loginByGitee.vue
, 里面做的操作就是先从 cookie 中取出 uuid,然后将 uuid 和 路由中的 code封装成一个请求体,去调用LoginByGitee
方法登录,一旦登录成功后就重定向到/
首页。
[!NOTE]
其实 uuid 也可以不从cookie中拿到,可以从路由中拿到:
this.$route.query.state
<template><div v-loading="loading" style="height: 100%;width: 100%;">正在加载中...</div>
</template><script>import Cookies from "js-cookie";export default {name: "loginByGitee",data() {return {loading: true}},// 立即执行的钩子函数mounted() {this.loading = true;// 从 cookie 中取出 uuidconsole.log("uuid", Cookies.get("user-uuid"))// 封装请求体const formBody = {uuid: Cookies.get("user-uuid"),code: this.$route.query.code}// vuex中调用actions的方法this.$store.dispatch("LoginByGitee", formBody).then(() => {this.$router.push({path: this.redirect || "/"}).catch(() => {});}).catch(() => {this.loading = false;});}
}
</script><style scoped></style>
记得前端放行/callback
路由
所以需要在 actions 中定义 LoginByGitee 方法,LoginByGitee
通过提交 commit 来更新Vuex中store的状态,并且传递 body 上方封装的请求体,将操作放在一个 Promise 中,并且在成功或者失败之后,调用对应的 resolve 或 reject。
// Gitee登录
LoginByGitee({commit}, body) {return new Promise((resolve, reject) => {loginByGitee(body.code, body.uuid).then(res => {setToken(res.token)commit('SET_TOKEN', res.token)resolve()}).catch(error => {reject(error)})})
},
里面才真正调用了loginByGitee
方法,所以我们在 src/api/login.js
补充方法
// Gitee登录
export function loginByGitee(code, uuid) {const data = {code,source: "Gitee",uuid}return request({url: '/loginByGitee',headers: {isToken: false},method: 'post',data: data})
}
前端调用的后端的/loginByGitee
方法,所以我们需要在后端放行/loginByGitee
方法
/*** 真正登录逻辑* @param loginByOtherSourceBody* @return*/
@PostMapping("/loginByGitee")
public AjaxResult loginByGitee(@RequestBody LoginByOtherSourceBody loginByOtherSourceBody) {AjaxResult ajax = AjaxResult.success();String token = sysLoginService.loginByOtherSource(loginByOtherSourceBody.getCode(), loginByOtherSourceBody.getSource(), loginByOtherSourceBody.getUuid());ajax.put(Constants.TOKEN, token);return ajax;
}
这里需要创建一个LoginByOtherSourceBody
类,并且加上getter和setter方法
public class LoginByOtherSourceBody {private String code;private String source;private String uuid;
}
并且sysLoginService.loginByOtherSource
Service层是没有这个方法的,加上。
public String loginByOtherSource(String code, String source, String uuid) {//先到数据库查询这个人曾经有没有登录过,没有就注册// 创建授权requestAuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder().clientId("clientId").clientSecret("clientSecret").redirectUri("http://localhost/callback").build());// Gitee登录成功AuthResponse<AuthUser> login = authRequest.login(AuthCallback.builder().state(uuid).code(code).build());System.out.println(login);//先查询数据库有没有该用户(从登录成功的login中拿到登录的信息)AuthUser authUser = login.getData();SysUser sysUser = new SysUser();sysUser.setUserName(authUser.getUsername());sysUser.setSource(authUser.getSource());List<SysUser> sysUsers = userService.selectUserListNoDataScope(sysUser);if (sysUsers.size() > 1) {throw new ServiceException("第三方登录异常,账号重叠");} else if (sysUsers.size() == 0) {//相当于注册sysUser.setNickName(authUser.getNickname());sysUser.setAvatar(authUser.getAvatar());sysUser.setEmail(authUser.getEmail());sysUser.setRemark(authUser.getRemark());userService.registerUserAndGetUserId(sysUser);AsyncManager.me().execute(AsyncFactory.recordLogininfor(sysUser.getUserName(), Constants.REGISTER,MessageUtils.message("user.register.success")));} else {sysUser = sysUsers.get(0);}AsyncManager.me().execute(AsyncFactory.recordLogininfor(sysUser.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));//注册成功或者是已经存在的用户LoginUser loginUser =new LoginUser(sysUser.getUserId(), sysUser.getDeptId(), sysUser, permissionService.getMenuPermission(sysUser));recordLoginInfo(loginUser.getUserId());// 生成tokenreturn tokenService.createToken(loginUser);
}
同时我们在三方登录的时候,还需要维护一个source字段,表示你是通过QQ、Github、Gitee还是哪个来登录的。
所以我们在sysUser
类里面加上source
字段,并且生成对应的 getter 和 setter,数据库表sys_user
表中也需要加 source 字段。
/** 登录源 */
private String source;
同时 userService.selectUserListNoDataScope
Service层没有这个方法 ,所以需要写一个查询方法在ISysUserService
,SysUserServiceImpl
中,
同时Mapper.xml里面也需要加上我们的source
字段,涉及到的查询sql语句我们都可以进行加啦
我们再来看我们三方登录的逻辑,根据用户名 username 和 源source 在数据库进行查询,如果查到1条以上,那么说明第三方登录异常,账号重叠了,如果1条也查不到,那么相当于是在注册了,这里重新写了一个注册方法userService.registerUserAndGetUserId
因为我们需要拿到注册的用户的ID
其中我们自己的注册用户信息返回用户ID如下
我们来看看效果,登录进去后头像不显示,很明显,我们的头像是一个 http请求,它自动加上了dev-api
前缀
所以我们修改一下获取用户信息的头像逻辑:
[!NOTE]
至此,我们三方登录正式完成!梳理一下
- 首先去前端写一个 Gitee 的按钮,给按钮点击事件
- 点击按钮就执行
PreLoginByGitee
方法,这个方法会去请求后端/PreLoginByGitee
接口,后端会生成 url,也就是跳转到Gitee登录的那个 url,这样点击按钮就可以跳转到 Gitee 的登录认证的窗口了- 认证成功会回调到路由
/callback
,这个路由又跳转到LoginByGitee.vue
组件,这个组件开始加载中转圈圈,然后准备code、uuid直接去请求后端LoginByGitee
接口。- 登录成功后我们可以拿到登录用户的所有数据,比如 Gitee 的名字、Gitee的头像等,然后我们将这些数据与数据库进行比对,通过用户名username和source源进行比对,如果没有比对成功的,那么就直接使用这个用户数据进行注册。如果有1条比对成功的,我们直接拿出来进行登录即可。
4、新建模块
- 对准项目根文件右键新建选择模块:
然后新建模块,注意父模块要选ruoyi
,我这里是因为更改了包名,新建模块我的叫KuangStudy_Vue-core
[!NOTE]
需要注意的是:
- 我们新建的模块需要在根的
pom.xml
下引用
- 并且需要在
admin
模块下引入,这样的话是直接继承根的依赖,就不用写版本号了
现在已经有了新的模块了,那么,我需要添加我自己的代码有什么注意事项吗?
-
因为RuoYiApplication在com.ruoyi包下,所以我们也需要手动建立com.ruoyi包,不然自己写的类无法注入Spring容器。
-
当我们自己新建的模块需要依赖ruoyi-system和ruoyi-framework的时候,只需要引入
ruoyi-framework
即可,因为ruoyi-framework里面引入了ruoyi-system。 -
创建的新模块没有引入spring,而spring都在
common
包下,所以必须引入common
包
接下来就可以写自己的方法了,我这里写了一个控制层接口:
这是后端的接口,所以得访问http://localhost:8080/kuang
,如果通过前端访问的话,需要走前端的代理,也就是需要访问的url是http://localhost:80/dev-api/kuang
。
[!NOTE]
思考:我们为什么在控制层方法上添加了
@Anonymous
注解,不用之前的token就可以访问到方法了?回答:
@Anonymous
注解放到类上,该类所有方法都可以匿名访问;放到方法上,那么就该方法可以被匿名访问。
5、前端通用方法
所谓通用方法,就是随时能调用的方法。例如我们的Vue的methods中随时可写:
this.$tab.openPage("用户管理", "/system/user");
this.$modal.msg("默认反馈");
this.$auth.hasPermi("system:user:add");
this.$auth.hasRole("admin");
this.$cache.local.set('key', 'local value')
this.$download.name(name);
这些方法为什么可以随时调用呢?是因为若依准备了插件,在main.js
中引入了plugins
,并且使用Vue.use(plugins)
Vue.use 会去调用 plugins 的 install
方法,给Vue原型 prototype 加上了 $xxx
,所以在任意的组件实例里面都可以取到了。
5.1、$tab
5.1.1、打开新标签页
可以在方法中调用this.$tab.openPage(title,url)
来打开新的标签页,标签页的标题是title
,要打开的标签url是url
<template><div><h1>打开标签页</h1><el-button type="primary" @click="openNotice">通知公告1</el-button><el-button type="primary" @click="openNoticeAndWindows">通知公告2</el-button></div>
</template><script>
export default {name: "kuang",methods: {// 单纯打开新页面openNotice() {this.$tab.openPage("打开的通知公告", "/system/notice");},// 打开页面之后,并且做一点其他事情。openNoticeAndWindows() {this.$tab.openPage("打开的通知公告并且弹窗", "/system/notice").then(() => {this.$modal.msg("我是弹窗")})}}}
</script><style></style>
如上代码,我们给两个按钮加了两个方法,分别是单纯打开一个新页面,新标签页的标题是 打开的通知公告,另一个是打开标签页并且提示弹框
我们看看若依封装的openPage
方法
从上面的源码可以看出,做了两件事:
- 调用Vuex的
tagsView/addView
方法,并传参数obj。**该方法是为了把新打开的页签添加到Vuex维护的页签数组visitedViews。**当添加成功,组件TagsView的计算属性visitedViews变化,页面重新渲染,页签自然多一个。 - 第二件事就是将路由做简单的跳转。
5.1.2、修改标签页
this.$tab.updatePage
可以修改标签
<template><div><h1>打开标签页</h1><el-button type="primary" @click="openNotice">通知公告1</el-button><el-button type="primary" @click="openNoticeAndWindows">通知公告2</el-button><el-divider></el-divider><h1>修改标签页</h1><el-button type="info" @click="updateSelf">修改自己标签</el-button><el-button type="info" @click="updateOther">修改其他标签</el-button></div>
</template><script>
export default {name: "kuang",methods: {// 单纯打开新页面openNotice() {this.$tab.openPage("打开的通知公告", "/system/notice");},// 打开页面之后,并且做一点其他事情。openNoticeAndWindows() {this.$tab.openPage("打开的通知公告并且弹窗", "/system/notice").then(() => {this.$modal.msg("我是弹窗")})},updateSelf() {//修改自己const obj = Object.assign({}, this.$route, {title: "自定义标题"})this.$tab.updatePage(obj);},updateOther() {// 修改别的标签页const obj = Object.assign({}, {path: "/system/notice"}, {title: "自定义标题222"})this.$tab.updatePage(obj).then(() => {this.$modal.msg("修改页签完毕")})},}}
</script><style></style>
5.1.3、关闭标签
this.$tab.closeOpenPage
:关闭当前页签并且打开新页面this.$tab.closePage
: 关闭当前页签并回到首页
//关闭当前并且打开新页面
closeAndOpen() {const obj = {path: "/system/user"};this.$tab.closeOpenPage(obj);
},
//关闭当前并回到首页
closeAndGoIndex() {this.$tab.closePage();
},
//关闭指定的页面
closeSpecifyPage() {const obj = {path: "/system/user"};this.$tab.closePage(obj);
}
5.1.4、刷新标签
this.$tab.refreshPage()
刷新标签
//刷新当前页签
refreshPage() {this.$tab.refreshPage();
}
刷新页签相当于页面关闭,重新打开。
5.1.5、关闭所有页签
this.$tab.closeAllPage()
: 关闭所有页签
//关闭所有页签
closeAll() {this.$tab.closeAllPage();
},
5.1.6、关闭左侧页签
this.$tab.closeLeftPage();const obj = { path: "/system/user", name: "User" };
this.$tab.closeLeftPage(obj);this.$tab.closeLeftPage(obj).then(() => {// 执行结束的逻辑
})
[!NOTE]
抛砖引玉,更多请查看官方文档,文档的东西更详细!
5.2、$modal
$modal
对象用于做消息提示、通知提示、对话框提醒、二次确认、遮罩等。
<template><div><h1>打开标签页</h1><el-button type="primary" @click="openNotice">通知公告1</el-button><el-button type="primary" @click="openNoticeAndWindows">通知公告2</el-button><el-divider></el-divider><h1>修改标签页</h1><el-button type="info" @click="updateSelf">修改自己标签</el-button><el-button type="info" @click="updateOther">修改其他标签</el-button><el-divider></el-divider><h1>消息提示弹窗</h1><el-button type="text" @click="msg">反馈信息</el-button><el-button type="text" @click="alert">提示信息</el-button><el-button type="text" @click="notify">通知信息</el-button></div>
</template><script>
export default {name: "kuang",methods: {// 单纯打开新页面openNotice() {this.$tab.openPage("打开的通知公告", "/system/notice");},// 打开页面之后,并且做一点其他事情。openNoticeAndWindows() {this.$tab.openPage("打开的通知公告并且弹窗", "/system/notice").then(() => {this.$modal.msg("我是弹窗")})},updateSelf() {//修改自己const obj = Object.assign({}, this.$route, {title: "自定义标题"})this.$tab.updatePage(obj);},updateOther() {// 修改别的标签页const obj = Object.assign({}, {path: "/system/notice"}, {title: "自定义标题222"})this.$tab.updatePage(obj).then(() => {this.$modal.msg("修改页签完毕")})},// 提供成功、警告和错误等反馈信息msg() {this.$modal.msg("默认反馈");// this.$modal.msgError("错误反馈");// this.$modal.msgSuccess("成功反馈");// this.$modal.msgWarning("警告反馈");},// 提供成功、警告和错误等提示信息alert() {this.$modal.alert("默认提示");// this.$modal.alertError("错误提示");// this.$modal.alertSuccess("成功提示");// this.$modal.alertWarning("警告提示");},// 提供成功、警告和错误等通知信息notify() {this.$modal.notify("默认通知");// this.$modal.notifyError("错误通知");// this.$modal.notifySuccess("成功通知");// this.$modal.notifyWarning("警告通知");},}}
</script><style></style>
增加一个确认窗口信息
confirm() {this.$modal.confirm('你确定要确定吗').then(function () {}).then(() => {this.$modal.notify("点了确定")}).catch(() => {this.$modal.notify("点了取消")});
},
[!NOTE]
玩个好玩的,开启遮罩层和关闭遮罩层
//遮罩层 startLoading() {// 打开遮罩层this.$modal.loading("正在Loading,请稍后...");wait5Second().then(res => {console.log(res)this.$modal.closeLoading();}) }, //如若已经loading则只能程序自己调用 stopLoading() {this.$modal.closeLoading(); }
前端点击按钮开启遮罩层,调用前端的
wait5Second
请求,等待5s后关闭遮罩层。
- 去
src/api/kuang/kuang.js
下新增请求import request from "../../utils/request";// 等5s export function wait5Second() {return request({url: '/wait5Second',headers: {isToken: false},method: 'get',}) }
- 在后端新增接口
wait5Second
5.3、$auth
$auth
对象用于验证用户是否拥有某(些)权限或角色,它定义在plugins/auth.js
文件中,它有如下方法
- 验证用户权限
// 验证用户是否具备某权限
this.$auth.hasPermi("system:user:add");
// 验证用户是否含有指定权限,只需包含其中一个
this.$auth.hasPermiOr(["system:user:add", "system:user:update"]);
// 验证用户是否含有指定权限,必须全部拥有
this.$auth.hasPermiAnd(["system:user:add", "system:user:update"]);
- 验证用户角色
// 验证用户是否具备某角色
this.$auth.hasRole("admin");
// 验证用户是否含有指定角色,只需包含其中一个
this.$auth.hasRoleOr(["admin", "common"]);
// 验证用户是否含有指定角色,必须全部拥有
this.$auth.hasRoleAnd(["admin", "common"]);
我们来使用一下:增加一个按钮,点击按钮判断用户是否具有system:user:list
权限
<template><div><h1>验证权限</h1><el-button type="text" @click="validMyPermission">验证权限</el-button></div>
</template><script>export default {name: "kuang",methods: {//验证权限validMyPermission() {console.log(this.$auth.hasPermi("system:user:list"));}}}
</script><style></style>
我们看一下hasPermi
的源码,它调用了authPermission
方法,可以发现,它是从Vuex的 store 中获取权限,而Vuex的store中的 permission 又是后端传过来的
5.4、$download
$download对象用于文件下载,它定义在plugins/download.js文件中。
对于图片,用户上传头像,若依的处理是将用户上传的头像存放在D:/ruoyi/upload
目录下,这个目录可以在yml
中配置。
[!NOTE]
用户上传头像,我们可以将头像的URI存在数据库中,这样当使用用户头像或者下载用户头像的时候,我们可以通过数据库中的URI进行下载。
5.4.1、根据名称下载upload
路径下的文件
[!NOTE]
后期更新
6、Https实现
因为http自身是明码传输,我们通过网卡抓包可以轻而易举得到token。甚至包括登录的账号密码,所以要上https!
[!NOTE]
- 参考视频:腾讯云实现HTTPS
相关文章:

【开源风云】从若依系列脚手架汲取编程之道(四)
📕开源风云系列 🍊本系列将从开源名将若依出发,探究优质开源项目脚手架汲取编程之道。 🍉从不分离版本开写到前后端分离版,再到微服务版本,乃至其中好玩的一系列增强Plus操作。 🍈希望你具备如下…...

华为 HCIP-Datacom H12-821 题库 (15)
有需要题库的可以看主页置顶 1.以下关于 OSPF 路由聚合的描述,错误的是哪一项? A、OSPF 中任意一台路由器都可以进行路由聚合的操作 B、OSPF 有两种路由聚合方式:ABR 聚合和ASBR 聚合 C、路由聚合是指将相同前缀的路由信息聚合一起…...

MT6895(天玑8100)处理器规格参数_MTK联发科平台方案
MT6895平台 采用台积电5nm工艺,与天玑 8000 相比性能提升 20% ,搭载4 个 2.85GHz A78 核心 4 个 2.0GHz A55 核心,CPU能效比上一代提高 25% 。GPU 采用了第三代的Valhall Arm Mali-G610 MC6架构,拥有6核心,搭配天玑81…...

从 0 开始搞定 RAG 应用系列(第一篇):构建简单 RAG
从 0 开始搞定 RAG 应用系列(第一篇):构建简单 RAG 前言 LLM 已经从最初的研究性转变为实际应用性,尤其在今年各大 LLM 厂商都在研究 LLM 的商业化落地方案(包括我司)。而在各种商业化场景中个人觉得最具…...

接口(Interface)和端点(Endpoint)的区别
在软件开发和相关的文档中,我们经常会看到两个专有名词:接口(Interface)和端点(Endpoint)。而它们的使用场景有很大的重合部分,让人有些分不清到底用哪个。那么,这两者到底有什么区别…...

小米汽车再陷“抄袭”争议,上汽高管直言“真不要脸”
小米SU7在上市初期就曾面临来自各方的争议与质疑,不少人将其戏称为“米时捷”或“保时米”。 转载科技新知 原创 作者丨杰瑞 编辑丨影蕨 近日,在成都车展上,上汽乘用车常务副总经理俞经民对小米汽车提出了尖锐批评,指责其“抄袭”…...

VS C++ 加入dump实现崩溃日志 可以再崩溃的时候使用VS调试
调用 // 加入崩溃dump文件功能SetUnhandledExceptionFilter(ExceptionFilter); 实现 #include "DbgHelp.h"//生成dump int GenerateMiniDump(PEXCEPTION_POINTERS pExceptionPointers) {// 定义函数指针typedef BOOL(WINAPI * MiniDumpWriteDumpT)(HANDLE,DWORD,H…...

Ubuntu22.04版本左右,开机自动启动脚本
Ubuntu22.04版本左右,开机自动启动脚本 1. 新增/lib/systemd/system/rc-local.service中[Install]内容 vim /lib/systemd/system/rc-local.service 按 i 进入插入模式后,新增内容如下: [Install] WantedBymulti-user.target Aliasrc-local.…...

中秋之美——html5+css+js制作中秋网页
中秋之美——html5cssjs制作中秋网页 一、前言二、功能展示三、系统实现四、其它五、源码下载 一、前言 八月十五,秋已过半,是为中秋。 “但愿人长久,千里共婵娟”,中秋时节,气温已凉未寒,天高气爽&#x…...

java设计模式day03--(结构型模式:代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式)
5,结构型模式 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“…...

Golang path/filepath包详解:高效路径操作与实战案例
Golang path/filepath包详解:高效路径操作与实战案例 引言基础用法Abs 函数Base 函数Clean 函数Dir 函数Ext 函数FromSlash 和 ToSlash 函数 基础用法Abs 函数Base 函数Clean 函数Dir 函数Ext 函数FromSlash 和 ToSlash 函数 路径操作Join 函数Split 函数Rel 函数Ma…...

【Shiro】Shiro 的学习教程(四)之 SpringBoot 集成 Shiro 原理
目录 1、第一阶段:启动服务,构建类的功能2、第二阶段:正式请求 1、第一阶段:启动服务,构建类的功能 查看 Shiro 配置类 ShiroConfiguration: Configuration public class ShiroConfiguration {// 创建 sh…...

多线程篇(阻塞队列- PriorityBlockingQueue)(持续更新迭代)
目录 一、简介 二、类图 三、源码解析 1. 字段讲解 2. 构造方法 3. 入队方法 put 浮调整比较器方法的实现 入队图解 4. 出队方法 take dequeue 下沉调整比较器方法的实现 出队图解 四、总结 一、简介 PriorityBlockingQueue队列是 JDK1.5 的时候出来的一个阻塞…...

strstr函数的使用和模拟实现
目录 1.头文件 2.strstr函数的使用 3.strstr函数模拟实现 小心!VS2022不可直接接触,否则!没这个必要,方源面色淡然一把抓住!顷刻炼化! 1.头文件 strstr函数的使用需要头文件 #include<string.h>…...

使用Selenium与WebDriver实现跨浏览器自动化数据抓取
背景/引言 在数据驱动的时代,网络爬虫成为了收集和分析海量数据的关键工具。为了应对不同浏览器环境下的兼容性问题,Selenium与WebDriver成为了开发者实现跨浏览器自动化数据抓取的首选工具。本文将深入探讨如何利用Selenium和WebDriver实现跨浏览器的数…...

信创实践(3):基于x2openEuler将CentOS升级成openEuler,享受其带来的创新和安全特性
引言: 在当前的 IT 行业中,创新和安全性是两大关键趋势。随着 CentOS 停止维护,许多用户正在寻找替代方案,以保持其系统的更新和安全。openEuler 作为一个强大的开源操作系统,成为了理想的迁移目标。本教程将指导您如…...

LEAN 类型理论之注解(Annotations of LEAN Type Theory)-- 相等类型(Equality Type)
《何谓相等 (Equality),在类型理论(Type Theory)语境下》 与 《转化(conversion and reduction)后的相等(Equality)》,两文中,已对相等(Equality)的概念进行了描述&#…...

Idea 创建 Maven项目的时候卡死
文章目录 一、Archetype 和 Catalog1.1 Archetype(原型)1.2 Catalog(目录) 二、可能遇到的问题2.1 问题描述2.2 原因分析2.3 解决方案 参考资料 一、Archetype 和 Catalog 1.1 Archetype(原型) Archetype…...

C++入门(02)简单了解C++应用程序的开发部署
文章目录 1. 开发C应用程序2. 简单示例计算器程序3. 需求分析4. 设计5. 编码6. 编译7. 调试8. 测试9. 部署10. 部署示例10.1 使用Visual Studio Installer Projects创建安装程序10.2 安装VisualStudio Installer Projects扩展10.3 在calculator解决方案中创建安装项目10.3.1 添…...

有了室内外一体化人行导航,你还怕迷路吗?
在快节奏的现代生活中,无论是穿梭于繁华的都市丛林,还是漫步于错综复杂的购物中心,迷路似乎成了不少人的“小确丧”。然而,随着科技的飞速发展,一项革命性的创新——室内外一体化人行导航系统,正悄然改变着…...

Python虚拟环境包迁移
1. 激活源虚拟环境 首先,激活你想要导出包的源虚拟环境。在命令行中输入: Windows: path\to\your\source_env\Scripts\activatemacOS/Linux: source path/to/your/source_env/bin/activate 2. 导出已安装包的列表 使用以下命令生成一个requirements…...

利用分布式锁在ASP.NET Core中实现防抖
前言 在 Web 应用开发过程中,防抖(Debounce) 是确保同一操作在短时间内不会被重复触发的一种有效手段。常见的场景包括防止用户在短时间内重复提交表单,或者避免多次点击按钮导致后台服务执行多次相同的操作。无论在单机环境中&a…...

Django+Vue3前后端分离学习(二)(重写User类)
一、重写User类: 1、首先导入User类: from django.contrib.auth.models import User 2、然后点在User上,按住ctrl 点进去,发现 User类继承AbstractUser Ctrl点进去AbstractUser,然后将此方法全部复制到自己APP的mo…...

兔英语语法体系——观后笔记
目录 一、视频链接 二、视频前言 三、简单句(Simple Sentences) 1. 可独立完成的动作 2. 有1个动作的承受者 3. 有两个动作承受者 4. 只有一个动作承受者(但需补充) 5. 非 “动作” 6. 总结 四、五大基本句型 五、句子成分 6. 定语 7. 状语 8. 同位语 9. 总结 …...

哈希表如何避免冲突
系列文章: 1. 先导片--Map&Set之二叉搜索树 2. Map&Set之相关概念 3. 哈希表如何避免冲突 目录 1.概念 2. 冲突-概念 3. 冲突-避免 3.1 冲突-避免-哈希函数设计 3.2 冲突-避免-负载因子调节 4. 冲突-解决 4.1 冲突-解决-闭散列 4.1.1 线性探…...

内核模块驱动开发
内核模块开始学习前,一定是最先接触到内核模块三要素(面试),驱动入口、驱动出口和协议的遵循。 1.内核模块三要素(面试)//修饰模块化驱动的入口函数module_init(demo_init);//修饰模块化驱动的出口函数module_eixt(demo_exit);//遵循GPL开源协议MODULE_…...

Linux 下 alsa 库录音并保存为 WAV 格式
麦克风列表: [jnjn build]$ arecord -l **** List of CAPTURE Hardware Devices **** card 0: AudioPCI [Ensoniq AudioPCI], device 0: ES1371/1 [ES1371 DAC2/ADC]Subdevices: 1/1Subdevice #0: subdevice #0 card 1: Camera [2K USB Camera], device 0: USB Aud…...

使用stripe进行在线支付、退款、订阅、取消订阅功能(uniapp+h5)
stripe官网:Stripe 登录 | 登录 Stripe 管理平台 然后在首页当中打开测试模式,使用测试的公钥跟私钥进行开发 测试卡号 4242 4242 4242 4242 1234 567 在线支付 stripe的在线支付有两种,第一种就是无代码,第二中就是使用api进行自定义,一般来说推荐第二种进行开发 无…...

深度学习中常见的损失函数
关注B站可以观看更多实战教学视频:hallo128的个人空间 深度学习中常见的损失函数 损失函数的作用 损失函数是衡量神经网络输出与真实标签之间差距的指标。在训练过程中,神经网络的目标是最小化损失函数的值。常见的损失函数包括均方误差(MS…...

认识Linux及Linux的环境搭建
目录 1、什么是Linux2、Linux环境搭建2.1 下载安装 Xshell2.2 下载安装 VMware Workstation Pro2.3 选择适合自己系统 1、什么是Linux Linux,一般指GNU/Linux(单独的Linux内核并不可直接使用,一般搭配GNU套件,故得此称呼ÿ…...