ruoyi前后端不分离版本4.4.0

对一个开源web项目要学习什么?

  • 需求分析
  • 权限管理
    • 数据权限
    • 操作权限
  • 数据库设计
  • UI设计
  • 日志系统
  • 文档书写

properties

ruoyi是后缀为Properties的类,用于读取配置文件中的配置,而不是java中的properties类
这种类加上@Configuration、@ConfigurationProperties注解,用@Value注解配合spel(或在类上指定配置中的前缀)获取配置文件中的属性

  • 跨模块的配置文件也可以读取,因为依赖?还是因为读取classpath下的文件?
  • maven依赖是单向的(毕竟会报循环依赖)
    在配置类用@EnableConfigurationProperties({MySqlBinlogConnectJavaProperties.class})注解读取获取到的属性

动态数据源配置

  • 什么是“动态数据源”?
  • 似乎要先系统学学mysql主从,,话说单纯mysql主从不需要改代码吧。
public class DruidConfig
{
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

日志学习

sys_oper_log 操作日志记录
其他日志由logback保存在日志文件中,操作日志和登录日志保存在数据库中

handleLog方法中,user,dept,deptName真是每一步都要判空啊
记录日志:用单例模式的异步管理类
异步任务工厂AsyncFactory生产异步任务,由异步任务管理类AsyncManager执行(管理)
在 RuoYi-Vue 脚手架中,使用了 Java JUC 中的 ScheduledExecutorService 去完成这个延时任务,AsyncManager.me().execute 方法实际上就是去执行一个延时任务,这个延时任务就是一个往数据库中写入一条记录。
如果请求类型是 GET 或者 DELETE,则请求参数就直接从请求对象提取了。是如果请求类型是 PUT 或者 POST,就意味着请求参数是在请求体中,请求参数有可能是二进制数据(例如上传的文件),二进制数据就不好保存了,所以对于 POST 和 PUT 还是从接口参数中提取,然后过滤掉二进制数据即可。

操作日志

LogAspect

ServletUtils.getRequest().getParameterMap()不为空
否则 使用joinPoint的参数
根据Druid,每次更新Dept的时候应该时把状态单独执行的

业务类型

0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据

授权角色:先删除user_role再插入user_role

  • controller参数: id,ids
    删除都是状态改变,可能有定时任务清理?businessType还是删除

实践:改造操作日志->记录数据变化

思路:
  • 提前取出老数据再上下文中?
  • 比较同一个类两个对象?
  • 删除/导出可以多选,怎么办?
    • 传递参数时逗号分隔的字符串
    • 导出的数据是什么?导出是导出全部,传入一个空数据
binlog学习

client.connect() is blocking (meaning that client will listen for events in the current thread).
因为阻塞所以在前台吗
mysql的binlog有三种模式(级别),Row(输出变化)、Statement(默认,输出Sql语句)、Mixed
show global variables like '%binlog_format%';
BinlogMiner:离线挖掘模式,将日志发到其他机器上分析(脱机?)

系统日志

# 日志配置
logging:
  level:
    com.ruoyi: debug
    org.springframework: warn

这个日志级别配置和ems_vue一样么?

权限学习

shiro使用

知乎上一篇教程
Realm负责Autherication和Authorizztion
shiro和slf4j的关系?
shiro认证
认真和授权流程相似
注:该实例中的类全部来自Shiro

//新建Realm
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加账户,并使他具有admin和user两个角色(可以不设置角色)
//在自定义的Realm中,应该由Subject传过来的信息中提取用户名,再根据用户名从数据库中提取权限、角色、凭证等信息
simpleAccountRealm.addAccount("wmyskxz", "123456", "admin", "user");

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体
		//将用户输入的账号密码包装成token,并尝试登录看看是否能被认证
        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
        subject.logout(); // 登出
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出false
        
        // 判断subject是否具有admin和user两个角色权限,如没有则会报错
        subject.checkRoles("admin","user");
    }

自定义Realm

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.*;

public class MyRealm extends AuthorizingRealm {

    /**
     * 模拟从数据库中取得所有需要的用户名、密码
     */
    Map<String, String> userMap = new HashMap<>(16);
    {
        userMap.put("wmyskxz", "123456");
        super.setName("myRealm"); // 设置自定义Realm的名称,取什么无所谓..
    }

    /**
     * 授权
     * 根据用户名,获取用户的权限和角色。类似上例addAccount中的功能
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 从数据库获取角色和权限数据,
        Set<String> roles = dao.getRolesByUserName(userName);
        Set<String> permissions = dao.getPermissionsByUserName(userName);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 认证
     * @param authenticationToken 主体传过来的认证信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.从主体传过来的认证信息中,获得用户名
        String userName = (String) authenticationToken.getPrincipal();

        // 2.通过用户名到数据库中获取凭证(此处为密码,找得到就返回认证信息)
        String password = getPasswordByUserName(userName);
        if (password == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
        return authenticationInfo;
    }
}

然后将之前例子中的Realm改成自己的就好,也不用手动添加addAccount了

springboot配置-ruoyi示例

# Shiro
shiro:
  user:
    # 登录地址
    loginUrl: /login
    # 权限认证失败地址
    unauthorizedUrl: /unauth
    # 首页地址
    indexUrl: /index
    # 验证码开关
    captchaEnabled: true
    # 验证码类型 math 数组计算 char 字符
    captchaType: math
  cookie:
    # 设置Cookie的域名 默认空,即当前访问的域名
    domain: 
    # 设置cookie的有效访问路径
    path: /
    # 设置HttpOnly属性
    httpOnly: true
    # 设置Cookie的过期时间,天为单位
    maxAge: 30
    # 设置密钥,务必保持唯一性(生成方式,直接拷贝到main运行即可)KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecretKey deskey = keygen.generateKey(); System.out.println(Base64.encodeToString(deskey.getEncoded()));
    cipherKey: zSyK5Kp6PZAAjlT+eeNMlg==
  session:
    # Session超时时间,-1代表永不过期(默认30分钟)
    expireTime: 30
    # 同步session到数据库的周期(默认1分钟)
    dbSyncPeriod: 1
    # 相隔多久检查一次session的有效性,默认就是10分钟
    validationInterval: 10
    # 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
    maxSession: -1
    # 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
    kickoutAfter: false

操作权限

通过shiro,在controller层添加@RequiresPermissions,在访问前认证;通过shiro标签控制是否显示;
删除角色前需要查看角色操作权限?和数据权限,且如果角色已被分配则不能删除

            checkRoleAllowed(new SysRole(roleId));
            checkRoleDataScope(roleId);

自定义UserRealm类认证、授权,

认证

SysLoginController
    @PostMapping("/login")
    @ResponseBody
    public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe)
    {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
        Subject subject = SecurityUtils.getSubject();
        try
        {
            subject.login(token);
            return success();
        }
        catch (AuthenticationException e)
        {
            String msg = "用户或密码错误";
            if (StringUtils.isNotEmpty(e.getMessage()))
            {
                msg = e.getMessage();
            }
            return error(msg);
        }
    }

  • get的login是干什么的?
  • shiro配置login是哪个?还是说都是?
UserRealm

从token中取出password、username,tryloginService.login(username, password),逐渐catch认证;没出错则包装返回

授权

ShiroUtils中获得当前User,根据userId,从Sys_menu表中取出perms字段,从sys_role表中取出SysRole对象(当前用户的角色、权限),放在授权信息中。
doGetAuthorizationInfo方法触发机制
在需要进行认证、授权的时候,SecurityManager调用Realm中的方法

数据权限

按照部门分配
角色与部门关联表
通过注解、切面、拼接sql字符串来动态拼接查询语句。
只有角色和数据权限直接相关,用户通过自己的角色来获得权限

@DataScope

common模块中的annotion包中定义DataScope注解,两个属性用于指定别名;指定别名是为了在切面类拼接sql语句时和Mapper.xml中的别名一致
在Service层

使用该注解的方法:
  • 查询user(分页查询、未分配、已分配)
  • 查询role(只能看到自己数据权限内的角色)
  • 查询部门列表
  • 查询部门管理树
  • 查询部门管理树(排除下级)
    后两个方法就是把第三个全查出来再在Service中剔除、排列
    校验部门是否有数据权限(checkDeptDataScope)没用上?校验角色的倒是在删除角色用上了
   /**
     * 修改数据权限信息
     */
    @Override
    @Transactional
    public int authDataScope(SysRole role)
    {
        // 修改角色信息(role中有datascope字段?ORM一对多要求的么)
        roleMapper.updateRole(role);
        
        //先删后增而不是直接修改,因为修改前后条数不一定一样(多对多关联表修改都是这样么?)
        // 删除角色与部门关联
        roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId());
        // 新增角色和部门信息(数据权限)
        return insertRoleDept(role);
    }

文档

  • 设置了数据权限后,角色、用户、部门树等只能看到自己数据权限给的部门内的。
    SysUser类的orm对象包含着roles的对象引用列表,而不是像数据库一样只有外键;在从数据库查询用户的时候就已经将roles读取到内存,而不是读取sys_user_role中的roles_id,在内存中不是通过id查找的。
    此时类和数据库不是一一对应的,SysUser类中的roles需要在其他表中查找

DataScopeAspect

framework模块中的aspectj包实现切面、注解
@Before匹配的注解是下面自己的参数的

  • 没有配置切入点

在执行@DataScope标注的方法之前,用ShiroUtils.getSysUser()获取当前用户,for循环处理用户的roles,获取每个role唯一的dataScope,然后if-else判断拼接哪句sql;
然后用joinPoint.getArgs()[0]获取参数(SysUser或SysDept),获取为Object,判断后转换为BaseEntity类,放入params并开始查询

SysUser类继承自BaseEntity类,父类中有Map类型的params属性,在mapper中调用,用于获取dataScope。虽然是私有属性,但是可以通过继承的共有方法调用??还是说,其实是SysUser->Object->BaseEntity?
为什么是subString(4)?

  • 全部权限就直接为空,不用筛选
  • 自定义数据权限的角色从sys_role_dept表里查dept_id,获得这个role能看的部门id
  • 只看自己部门就d.depi_id = user的deptId
  • 能看子部门就从部门表中选择自己部门 or 祖先有自己部门的
  • 只看自己就筛选按user_id,没有别名就为空(什么时候有这种情况呢?)
    基本是查看d.dept_id是否in一个子查询中

aop中指定切点为某个包?则该包下只能由接口,实现类要转移至业务层

  • 切点只能是接口吗?

相关表(sys_前缀)

理解
user用户
role角色
menu菜单权限表菜单是前端出现的选项,有些菜单需要权限才渲染
role_menu角色权限多对多
user_role角色用户多对多

菜单里有权限字段,user通过连接表查询自己的所有权限,有权限则显示菜单、访问controller接口

不需要用户-权限直接配置?因为用户的权限通过给角色配置过了吧(被范式优化掉了?因为shiro需要“角色”?spring security还需要角色不,数据库还这么搞不?)

  • 没分配角色侧边栏什么也看不到;
  • 父子联动:
    • 关掉父子联动,有父权限才能在任务栏看到父标签,有子权限父权限标签下才有东西
    • 选父会全选子,选一个子会自动选父;子没了自动取消

在角色中有这个权限才可以看到这个选项

相关类

  • role中这个属性干嘛的?
    /** 部门组(数据权限) */
    private Long[] deptIds;
  • @RequiresRoles写在哪里?

controller

  • system/dept/treeData 公司部门树
  • /system/user/list

数据库设计

表名加上前缀以区分
用户和岗位关联表,角色和部门关联

ancestors字段保存树形结构的父节点,然后用find_in_set(#{deptId}, ancestors) 查找子节点
当前时间用sysdate()

接口的概念:编程语言中和业务中不一样,做范围区分图

  • Fernflower decompiler / x jad

判断方法:
if (table == null? false: table.startsWith(“sys”))
从canal学到的

Java中定义Map常量,List常量
一般的方式的使用静态代码块。比如:

public final static Map map = new HashMap();  
static {  
    map.put("key1", "value1");  
    map.put("key2", "value2");  
}

下面为一种简单定义Map常量的方式

public final static Map<String, Fragment> NAV_ITEM_ADPTER = new HashMap<String, Fragment>() {
    {
        put("拍录传", new CameraFragment());
        put("集群对讲", new GroupTalkFragment());
        put("视通", new VideoCallFragment());
        put("位置", new PositionFragment());
        put("浏览", new BrowseFragment());
        put("消息", new MsgFragment());
        put("群组", new GroupFragment());
        put("设置", null);
        put("退出", null);
    }
};
        mv.addObject("pageInfo",pageInfo);
        mv.setViewName("orders-page-list");

modelView是把页面和页面需要的数据当成一个对象来操作,这种思想可以借鉴。如果需要的数据是一个对象,就addObject,把需要的数据包装成对象。似乎前后端分离也可以这么封装。

  • 这点儿可以看Spring的解释证明一下
  • 字典数据是干嘛的?
  • 内连接的on只能写主键与外键?
Logo

快速构建 Web 应用程序

更多推荐