版本3.5.0
ruoyi-cloud仓库自带vue2版本的前端模块ruoyi-ui,我们可以学习开源的Rouyi-cloud-Vue3版本

建议阅读官方文档来学习,一些常见问题如部署、微服务组件介绍、添加模块、获取当前用户,都可以在文档中搜索到教程,
微服务技术互相穿插,记笔记时应记得写清楚,那种技术和哪个组件相关,方便读者查阅

项目部署

提前安装nacos等中间件,建议尝试docker安装。
一些软件如果下载太慢可以用ruoyi文档里的网盘地址下载。
不建议完全按照文档的说明部署环境,不然容易被夹带私货(狗头),如nacos的数据库没必要叫ry-config,这不是若依专用的数据库,而是给中间件nacos用的。要注意边看文档边思考。

项目结构

同前后端不分离的ruoyi相比,没有framework模块,多了module模块来完成业务

  • ruoyi-common 该模块没有bootstrap.yml,也不用启动
    • ruoyi-common-core
      • src/main/java/com/ruoyi/common/core
        • annotation 注解包,包含自定义的Excel注解
        • constant 常量包
          • CacheConstants
      • context
        • SecurityContextHolder
      • domain 只有R
      • enums 所以和常量啥区别啊 UserStatus
      • exception 很多,有些模块分个文件夹
      • text
      • utils 很多 ,别想着敲了
      • xss
      • web
    • 其他common模块结类似构

每个模块下单独配置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学习-登录态判断
后端部分,有两种认证场景

  1. 自定义的Gateway全局过滤器AuthFilter
  2. 对被注解标注的方法进行校验。通过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
  • 这切面类怎么有了@Aspect注解还要@Component注解啊(恼
  • 前端收到“登录状态已过期”之类的异常后会不会清本地缓存?

模块依赖

auth依赖security,security依赖redis和api

jwt

JwtUtils

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等模组中的方法。

  • 超时单位?
  • 加密原理?

redis应用

  • 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,自己auth模块的。用于sysLoginService.login(form.getUsername(), form.getPassword());用用户密码直接登录,返回登录用户信息(LoginUser)
  • TokenService,通过登录的用户信息创建token。

SysLoginService

remoteLogService
remoteUserService
根据用户名,调用system模块服务查询用户信息。查到了再回来和密码比较

TokenService生成Token和JWT

USER__key:独特的UUID,估计是识别用的
userid
userName
然后把这仨放进map里当作claims,用JWTUtils生成access_token,放JWT里
返回access_token和expireTime

获取当前登录用户

        // 获取当前的用户
        LoginUser loginUser = SecurityUtils.getLoginUser();

LoginUser在api.system.model包下,
在这里插入图片描述

springSecurity应用

securityUtil
securityContextLoader:上下文,存放用户信息等;(common-core)
其实底层原理似乎还是用ThreadLocal

动手实验:添加websocket踢出功能

设计功能时,先考虑场景:发生甚么事了
然后考虑在该场景下,需要哪些功能,这些功能的权重;权重高的功能,即使是牺牲权重低的功能也要实现
比如,管理员踢出用户功能,用户收得到收不到踢出通知不打紧,但一定要保证踢出成功。这样就行了,不要求消息一定传递
但在消息推送的场景下,消息就一定要传达到。

  • 怎么保持连接?
  • 如何将在线用户的唯一依赖tokenId和websocket的sessionId组合在一起?

方案一:
在现有的tokenid保存的内容里带一个websocketId。似乎也需要前端带过去
方案二:
再在缓存里关联一下.这需要设置缓存的时候查一下当前的token。或许是前端带过去?

原本的强制登出登出在system模块,调用redisService删除缓存中的tokenid。然后每次登录需要查询。tokenid?

  • 那么在线用户保存的只有token?
  • 那怎么查询的在线用户信息?
    前端utils中的auth
  • import Cookies from ‘js-cookie’?
  • nprogress?
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 java.util.concurrent.Semaphore;

cookies

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();
  • 大括号是什么?
  • 得到的是什么?
    SysLoginService为什么没接口?和动态代理有关吗

util/ruoyi.js

/**
 * 构造树型结构数据
 * @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;
}
  • //用了||的写法来设定默认值,巧妙。看来默认是左边?
  • nodeIds默认是对象也可以数组,直接用[ ] ?
  • [ ]

前后端通信

ruoyi后端返回数据的方式是封装HashMap

public class AjaxResult extends HashMap<String, Object>

java值传递

java方法传递的是变量中的值;
如果传递对象的引用变量(变量中存储对象的引用),也是传递值(既一个存储同样引用的副本变量)。如果令副本变量指向新的对象,则不会影响原变量;若调用getter/setter方法,则还是原对象使用这些方法,会改变其中的值;

引用和指针的区别:

  • 输出:引用输出引用的字符串表示(包含类型等信息,有没有专门的类?),指针只输出变量地址(一串数字)
  • 变量:
    • 引用变量:存储引用的变量;
    • 指针变量:存贮地址的变量;
    • 他们隶属变量这个仓管部门的,只是把负责的仓库里放的是引用和地址;变量和引用、地址不是一个编制
    • (指针就是变量,但引用和存储他的变量不同;引用和地址是一个级别的。都是被存储的东西;)

变量最原始纯真的功能:存储。什么指向对象是存储的东西干的。

ioc

配置是独立于程序的只读变量
配置对于程序是只读的,程序通过读取配置来改变自己的行为,但是程序不应该去改变配置

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模块的动态数据源数据库配置和不用动态数据源的不一样。
可以阅读官方文档对于添加新模块的教程。

报错整理

maven编译报错:java:找不到符号

按报错点进文件,加载下就行

菜单管理

前端路由和组件其实没关联关系,自己在表单中设置

common模块

用枚举包装返回信息,包括code和message、data
code承载业务,http status code不承载业务

Logo

快速构建 Web 应用程序

更多推荐