版本3.5.0
ruoyi-cloud仓库自带vue2版本的前端模块ruoyi-ui,我们可以学习开源的Rouyi-cloud-Vue3版本
建议阅读官方文档来学习,一些常见问题如部署、微服务组件介绍、添加模块、获取当前用户,都可以在文档中搜索到教程,
微服务技术互相穿插,记笔记时应记得写清楚,那种技术和哪个组件相关,方便读者查阅
提前安装nacos等中间件,建议尝试docker安装。
一些软件如果下载太慢可以用ruoyi文档里的网盘地址下载。
不建议完全按照文档的说明部署环境,不然容易被夹带私货(狗头),如nacos的数据库没必要叫ry-config,这不是若依专用的数据库,而是给中间件nacos用的。要注意边看文档边思考。
同前后端不分离的ruoyi相比,没有framework模块,多了module模块来完成业务
每个模块下单独配置logback.xml
- Q:logback是不用专门导入依赖么,他这个maven依赖怎么看?
- A:是的,springboot默认使用logback作为日志框架,包含在spring-boot-starter中,不需要显示声明依赖
在网上很多微服务入门课程中,两个服务远程调用的大致操作为:
接口
而在ruoyi-cloud中,接口
全部被定义在了ruoyi-api模块中,调用方则通过直接或间接在pom文件中添加该模块的依赖来获得这些接口
api模块相当于一个配置的地方,将可以被远程调用的接口根据url在api模块下映射一下,然后调用api模块中的方法会找到对应的接口
- ruoyi-api模块就成了专门定义远程公共接口的地方了
- 关于模块依赖:如ruoyi-auth依赖了ruoyi-sercurity,而ruoyi-sercurity又依赖了ruoyi-api模块
- 根据openFeign的用法,调用方入口类上都需要@EnableFeignClients注解;ruoyi对该注解进一步封装成了@EnableRyFeignClients注解,尽管在原生的ruoyi-cloud中并没有体现出特殊的用法(;´д`)ゞ
通过观察gateway在nacos上的配置文件可知,gateway对登录功能相关接口开放白名单。
跳转登录界面的功能由前端完成。前端路由设有拦截器,如果发现本地没有登录token,强制跳转到登录页面
前端部分见Ruoyi-cloud-vue3学习-登录态判断
后端部分,有两种认证场景
PreAuthorizeAspect
切面,对RequiresRoles,RequiresPermissions,RequiresLogin
三个注解进行校验,调用AuthUtil
类的方法比较当前用户的登录状态、角色、权限是否与被调用的方法匹配两种都是校验失败了直接抛异常,然后异常消息被?Catch后送到前端会返回给前端。
而AuthUtil是经过一连串调用链后通过SecurityUtils获取前端传来的token,并尝试获取Redis中保存的当前用户;如果没有拿到token或找不到对应的在线用户,就直接抛异常,
话说,因为有全局登录认证的过滤器了,搞得@RequireLogin注解不是完全没用吗(恼
PreAuthorizeAspect
有个同时对多个注解进行切面的技巧
//定义AOP签名 (切入所有使用鉴权注解的方法)
public static final String POINTCUT_SIGN = " @annotation(com.ruoyi.common.security.annotation.RequiresLogin) || "
+ "@annotation(com.ruoyi.common.security.annotation.RequiresPermissions) || "
+ "@annotation(com.ruoyi.common.security.annotation.RequiresRoles)";
@Pointcut(POINTCUT_SIGN)
public void pointcut(){
}
- Q:那gateway的白名单是不校验了什么呢
- A: Gateway 网关层的白名单实现原理是在过滤器内判断请求地址是否符合白名单,如果通过则跳过当前过滤器。在ruoyi中,白名单的路径可以跳过全局的AuthFilter
auth依赖security,security依赖redis和api
ruoyi-common-core模块
登陆失败直接抛异常,所以前端Catch登录失败
通过调用getCurrentInstance方法获得当前实例,通过当前实例的refs获取表单,通过elementUI的validate先校验再设置cookie(),直接从前端表单的值设置,超时时间30,密码用encrypt加密。
然后再通过store的dispatch方法请求登录接口,没异常就直接跳转路由
user.js中,调用login方法,然后将接收到的token通过store存储到本地
/api/login
/utils/auth 通过cookies获取和存放token
使用vuex做前端状态管理(前端本地存储)
将store挂载(mount)在vue应用上,再在store上注册模组(user),在调用dispatch方法时传入要调用的方法名和需要用的参数,就像分发器一样通过Store调用了user等模组中的方法。
- Q:redis什么时候存进去的?在登录接口没见啊
- A:RefreshToken的时候存token。token刚生成后直接调用refresh存一次
以CacheConstants.LOGIN_TOKEN_KEY + tokenId存储登录信息的key
getCacheObject(key)获取
好家伙,SysUserOnlineController也是一堆if else。
为什么?get请求哪里还会有ipaddr和userName参数?其他功能带过来的?
从Redis获取的是LoginUser,返回的数列是SysUserOnline,要转换一下
猜测,查询在线用户的唯一依赖是redis中的CacheConstants.LOGIN_TOKEN_KEY + tokenId,从持久层选出唯一依赖后再转化
为的是少io吗
访问/auth/login登录。该controller调用两个service:
sysLoginService.login(form.getUsername(), form.getPassword());
用用户密码直接登录,返回登录用户信息(LoginUser)remoteLogService
remoteUserService
根据用户名,调用system模块服务查询用户信息。查到了再回来和密码比较
USER__key:独特的UUID,估计是识别用的
userid
userName
然后把这仨放进map里当作claims,用JWTUtils生成access_token,放JWT里
返回access_token和expireTime
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
securityUtil
securityContextLoader:上下文,存放用户信息等;(common-core)
其实底层原理似乎还是用ThreadLocal
设计功能时,先考虑场景:发生甚么事了
然后考虑在该场景下,需要哪些功能,这些功能的权重;权重高的功能,即使是牺牲权重低的功能也要实现
比如,管理员踢出用户功能,用户收得到收不到踢出通知不打紧,但一定要保证踢出成功。这样就行了,不要求消息一定传递
但在消息推送的场景下,消息就一定要传达到。
方案一:
在现有的tokenid保存的内容里带一个websocketId。似乎也需要前端带过去
方案二:
再在缓存里关联一下.这需要设置缓存的时候查一下当前的token。或许是前端带过去?
原本的强制登出登出在system模块,调用redisService删除缓存中的tokenid。然后每次登录需要查询。tokenid?
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
const ExpiresInKey = 'Admin-Expires-In'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
export function getExpiresIn() {
return Cookies.get(ExpiresInKey) || -1
}
export function setExpiresIn(time) {
return Cookies.set(ExpiresInKey, time)
}
export function removeExpiresIn() {
return Cookies.remove(ExpiresInKey)
}
只要有cookie中的token就可以到index.vue?能看到东西吗?还是说index.vue加载的时候也会鉴权?
默认路由为layout,不再组件文件夹里,why?是侧边栏加appmain(主体部分)组件
引用方式也不一样
conputed是干什么的?还可以获取设备信息
v-slot?
{ ‘–current-color’: theme }"
·
innerlink.vue
<script>
export default {
setup() {
const route = useRoute();
const link = route.meta.link;?
if (link === "") {?===
return "404";
}
let url = link;
const height = document.documentElement.clientHeight - 94.5 + "px";?
const style = { height: height };
// 返回渲染函数
return () =>
h(
"div",
{
style: style,
},
h("iframe", {
src: url,
frameborder: "no",
width: "100%",
height: "100%",
scrolling: "auto",
})
);
},
};
script>
import { encrypt, decrypt } from “@/utils/jsencrypt”;
Cookies.set(“username”, loginForm.value.username, { expires: 30 });
Cookies.remove(“username”);
跳转页面
const redirect = ref(undefined);
// 调用action的登录方法
store.dispatch("Login", loginForm.value).then(() => {
router.push({ path: redirect.value || "/" });
}).catch(() => {
loading.value = false;
// 重新获取验证码
if (captchaOnOff.value) {
getCode();
}
});
const store = useStore();
const router = useRouter();
const { proxy } = getCurrentInstance();
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
*/
export function handleTree(data, id, parentId, children) {
let config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
};
var childrenListMap = {};
var nodeIds = {};
var tree = [];
for (let d of data) {
let parentId = d[config.parentId];
if (childrenListMap[parentId] == null) {
childrenListMap[parentId] = [];
}
nodeIds[d[config.id]] = d;
childrenListMap[parentId].push(d);
}
for (let d of data) {
let parentId = d[config.parentId];
if (nodeIds[parentId] == null) {
tree.push(d);
}
}
for (let t of tree) {
adaptToChildrenList(t);
}
function adaptToChildrenList(o) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]];
}
if (o[config.childrenList]) {
for (let c of o[config.childrenList]) {
adaptToChildrenList(c);
}
}
}
return tree;
}
ruoyi后端返回数据的方式是封装HashMap
public class AjaxResult extends HashMap<String, Object>
java方法传递的是变量中的值;
如果传递对象的引用变量(变量中存储对象的引用),也是传递值(既一个存储同样引用的副本变量)。如果令副本变量指向新的对象,则不会影响原变量;若调用getter/setter方法,则还是原对象使用这些方法,会改变其中的值;
引用和指针的区别:
变量最原始纯真的功能:存储。什么指向对象是存储的东西干的。
配置是独立于程序的只读变量
配置对于程序是只读的,程序通过读取配置来改变自己的行为,但是程序不应该去改变配置
idea能用的maven在命令行识别不了,是没添加到环境变量么
找不到实体类的错误可能有很多,接下来列举几个地方
启动类位置不对,启动类应该在你的service和dao 的上一层,因为Spring是从启动类所在目录的同级目录开始扫描的,当然你也可以放在其他地方,但需要配置,具体配置可以参考网上的其他文章!
引入其他模块的实体类,要clean install一下
测试:
service本地的单元测试?
controller需要网络,怎么单测,用API?
python测试脚本是什么?
controller层 调用业务,匹配路径,封装数据。That’s why Service层不封装数据
写mapper时方法名字中的类就不需要Sys前缀了,,
idea endpoint mapping中可以看接口
对包右键->refactor
对项目右键->replace in path 替换ruoyi字符串
在nacos中修改配置
在idea中对类/注解/接口右键来寻找引用该类的代码
system模块的动态数据源数据库配置和不用动态数据源的不一样。
可以阅读官方文档对于添加新模块的教程。
按报错点进文件,加载下就行
前端路由和组件其实没关联关系,自己在表单中设置
用枚举包装返回信息,包括code和message、data
code承载业务,http status code不承载业务