若依— — 快速入门
1 什么是若依
官网地址:http://www.ruoyi.vip/
- 若依是一款优秀的开源项目,涉及到企业开发中大部分的管理系统,我们依此为模板进行二次开发,可以快速开发出符合大部分公司中的后台管理系统。
2 使用若依
使用开源项目流程:
- 下载并运行
- 看懂其中的业务流程
- 进行二次开发
-
下载
-
修改配置文件,配置数据源和Redis
-
下载前端依赖,然后运行
同时别忘记启动本机redis
- Redis官网地址:https://redis.io/
- 如果没有redis的可以去官网下载一个
3 若依源码分析
3.1 登录流程分析
基本思路:
后端生成一个表达式,然后根据@分割,将表达式转为图片传给前端,将表达式结果存入Redis,最后用户通过表单提交,判断结果是否正确,以此来达到区分人机的效果。
详细流程:
①前端通过vue反向代理请求到后端接口http://localhost/dev-api/captchaImage
【避免跨域问题】
②后端判断是否开启验证码开关captchaEnabled
,同时根据captchaType验证码类型,生成对应的表达式,同时生成一个UUID作为存入验证码的key
根据@
符号分割,然后将8+1
通过IO操作生成图片传给前端,将结果9存入redis。redis的key为:用户每次请求时后端生成的UUID。
前缀+UUID作为redis的key:
将生成的UUID和验证码图片,传给前端。
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
③前端展示图片,同时保存后端返回的UUID
④用户输入验证码后,前端携带UUID+用户输入的验证码结果请求后端/login
进行登录
- 根据前端传入的UUID取对应redis的value,然后将value与前端传入的code做比较,如果正确,则进行用户名密码校验,校验成功则生成token,并存入redis
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
...
//最后生成token返回给前端
return tokenService.createToken(loginUser);
⑤用户后面直接携带token访问后端不用重复登录
前端将后端返回的token存入浏览器的cookie中,并且让每次请求都携带上token
- 浏览器的cookie中存入token:
- 后端每次请求携带上token:
例如:
①后端生成一个表达式,1+1=2
1+1=?@2
②1+1=? 转成图片,传到前端进行展示
③表达式结果2存入redis
前端获取验证码的请求URL:http://localhost/dev-api/captchaImage
- 若依的后端端口是8080,前端是80,为什么上面的URl直接通过80端口就能访问到后台呢?
- 答案就是:反向代理
/dev-api 替换成'' 再映射到
http://localhost:8080
# 最后的URL被替换为:http://localhost:8080/captchaImage
3.2 登录具体流程
若依的安全框架之前使用的是shiro,后来转换为了SpringSecurity
①校验验证码
②校验用户名和密码
③生成Token
④异步任务管理器记录日志
使用异步任务管理器,结合线程池,实现了异步记录日志的操作,成功实现和业务逻辑异步解耦合。
⑤getInfo
获取当前用户的角色和权限信息,存储到Vuex中
权限匹配:*:*:*
请求后端/getInfo
获取当前用户信息,包括拥有哪些权限等:
当前用户所拥有的权限:
⑥GetRouters
根据当前用户的权限获取动态路由
- 因为若依是一个权限管理的系统,因此不同用户对应的左侧菜单栏是不同的
- 若依底层维护了几张表:
包括:sys_menu,其中借助了parent_id来进行层级的管理
3.3 用户管理
流程:加载Vue页面 - - 请求后台数据
例如:getList
- startPage():
public class PageUtils extends PageHelper{
/**
* 设置请求分页数据
*/
public static void startPage()
{
//如果请求中没有携带请求分页参数,则自动设置
PageDomain pageDomain = TableSupport.buildPageRequest();
//默认为:1
Integer pageNum = pageDomain.getPageNum();
//默认为:10
Integer pageSize = pageDomain.getPageSize();
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
...
}
PageHelper中的reasonable(合理的)对参数进行逻辑处理,保证参数的正确性。
- 例如:pageNum= 0/ -1, 则直接将pageNum设置为1
- userService.selectUserList(user);
注解
@DataScope(deptAlias="d", userAlias = "u")
是给表设置别名的
- sys_dept d, sys_user u
/**
* 根据条件分页查询用户列表
*
* @param user 用户信息
* @return 用户信息集合信息
*/
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}
- treeselect
- 查出所有的部门数据
- 组装成树状结构
buildDeptTreeSelect:将10条记录组装成一个树状图
recursionFn(depts, dept);
1、先找到顶级节点,再找到它的子节点
2、遍历顶级节点的子节点,再找到它的子节点
后端:
前端:
封装VO:
- 封装前:
- 封装后:
点击树状图中的数据:
3.4 添加数据
- reset:表单重置
- getTreeselct:获取部门树状图
- getUser:获取角色和部门信息
后端User业务:
3.5 修改数据
以修改用户信息为例
①先根据userId查询用户数据【请求的路径中有userId就是修改,没有就是查询】–GET请求
②根据userId进行修改【若依中的修改是:先删除再新增】–PUT请求
- 根据userId获取用户信息
- 如果有id就是修改,如果没有就是查询操作
- 修改用户信息的前端操作:
这里的getUser方法除了要获取所有岗位和角色信息之外,还要获取当前用户已经拥有的岗位和角色
- 根据userId发起PUT请求,修改用户信息
- 后端进行修改逻辑处理
/**
* 修改保存用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
@Transactional
public int updateUser(SysUser user)
{
Long userId = user.getUserId();
// 删除用户与角色关联
userRoleMapper.deleteUserRoleByUserId(userId);
// 新增用户与角色管理
insertUserRole(user);
// 删除用户与岗位关联
userPostMapper.deleteUserPostByUserId(userId);
// 新增用户与岗位管理
insertUserPost(user);
return userMapper.updateUser(user);
}
数据库:
- 修改user
- 重新维护user_post和user_role【用户-岗位,多对多关系,维护中间表】
根据是否有userId来区分是新增还是修改
- 修改操作:直接先删除,然后再新增记录关系
3.6 删除数据
3.7 异步任务管理器
AsyncManager.me().execute(AsyncFactory.recordLogininfor(
username, Constants.LOGIN_FAIL,
MessageUtils.message("user.password.not.match")));
通过异步任务管理器记录登录日志:
- AsyncManager.me()获取一个AsyncManager对象
- 执行execute方法,执行任务,传入的是一个Task对象,实现了Runnable接口,是一个任务,由线程Thread去执行。
若依框架中维护了一个sys_oper_log
表,用于记录操作日志:
①通过请求方法中的@Log日志,直接记录操作
/**
* 删除用户
*/
@PreAuthorize("@ss.hasPermi('system:user:remove')")
@Log(title = "用户管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds){
...
}
②直接异步调用日志记录
public String login(String username, String password, String code, String uuid)
{
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
//异步记录日志打印
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
打印日志实现:
- 创建线程池,配置核心参数等
- 创建任务
- 记录日志
/**
* 记录登录信息
*
* @param username 用户名
* @param status 状态
* @param message 消息
* @param args 列表
* @return 任务task
*/
public static TimerTask recordLogininfor(final String username, final String status, final String message,
final Object... args)
{
final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
final String ip = IpUtils.getIpAddr();
return new TimerTask()
{
@Override
public void run()
{
String address = AddressUtils.getRealAddressByIP(ip);
StringBuilder s = new StringBuilder();
s.append(LogUtils.getBlock(ip));
s.append(address);
s.append(LogUtils.getBlock(username));
s.append(LogUtils.getBlock(status));
s.append(LogUtils.getBlock(message));
// 打印信息到日志
sys_user_logger.info(s.toString(), args);
// 获取客户端操作系统
String os = userAgent.getOperatingSystem().getName();
// 获取客户端浏览器
String browser = userAgent.getBrowser().getName();
// 封装对象
SysLogininfor logininfor = new SysLogininfor();
logininfor.setUserName(username);
logininfor.setIpaddr(ip);
logininfor.setLoginLocation(address);
logininfor.setBrowser(browser);
logininfor.setOs(os);
logininfor.setMsg(message);
// 日志状态
if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
{
logininfor.setStatus(Constants.SUCCESS);
}
else if (Constants.LOGIN_FAIL.equals(status))
{
logininfor.setStatus(Constants.FAIL);
}
// 插入数据
SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
}
};
封装了登录用户的信息,执行添加操作,这里不会执行,而是将任务交给线程对象来执行。
- 异步任务管理器,内部定义了一个线程池,然后根据业务创建添加日志的任务,交给线程池来处理,这样做到日志和业务的抽象,解耦合,日志全部统一处理。
3.8 代码自动生成(menu菜单)
- 创建数据表
use ruoyi_vue;
create table test_user(
id int primary key auto_increment,
name varchar(11),
password varchar(11)
);
- 系统工具 - 代码生成
完整步骤:
①在数据库中创建一个表
create table test_user(
id int primary key ,
name varchar(11),
age int
)
②导入表
系统工具-代码生成
③导入之后,编辑我们导入的test_user表
需要新增表的字段描述和表的生成信息、表的描述等,否则无法成功生成正确的代码
④点击生成代码
3. 解压,使用代码
- main(Java后端代码)
- Vue(Vue前端代码)
- SQL(菜单SQL)
对应复制或直接执行使用即可。
注意:
如果后端抛出404异常,点击Idea的rebuild project,重新启动即可