若依框架学习

若依是一个基于Spring Boot + Vue的开源项目,本文详细介绍了若依的部署过程,包括下载、配置数据库、启动前后端,以及可能出现的问题。此外,还讲解了相关功能,如验证码的前端和后端实现、登录功能、获取用户角色和权限、动态菜单路由等。
摘要由CSDN通过智能技术生成

若依

什么是若依

开源项目
官网:http://ruoyi.vip/
Spring Boot + Vue
环境要求:
1、JDK 1.8+
2、MySql
3、Redis
4、Maven
5、Vue
电脑没有安装这些,请先安装。

部署

下载

下载位置
在这里插入图片描述
大概功能
在这里插入图片描述

配置数据库

配置mysql和Redis
1、在MySQL中运行sql中的文件sql脚本
在这里插入图片描述
2、修改MySQL配置(记得修改库名,问号前面的):
修改文件:ruoyi-admin/src/main/resources/application-druid.yml
在这里插入图片描述
3、修改Redis配置:
修改文件:ruoyi-admin/src/main/resources/application.yml
在这里插入图片描述
Redis的安装可参考:https://blog.csdn.net/Hiking_Tsang/article/details/127617085

启动后端

1、执行这个类的main
在这里插入图片描述
2、访问后台:http://localhost:8080/,如果出现下图所示,则表示启动成功。
在这里插入图片描述

启动前端

1、用idea打开 ruoyi-ui
在这里插入图片描述
2、终端运行安装依赖命令:

npm install

小知识:npm(node package manager):node.js 的包管理器,用于node插件管理(包括安装、卸载、管理依赖等) ,npm 是随同 node.js 一起安装的包管理工具,能解决 node.js 代码部署上的很多问题(安装 node.js 的时候会相应的安装 npm,node.js 已经集成了 npm,所以安装 node.js 后 npm 也安装好了)
3、终端运行启动服务命令:

npm run dev

4、访问连接
http://localhost:80
登录账户和密码默认是:admin/admin123
登录后的效果图:
在这里插入图片描述

可能会遇到的问题

在运行的时候本人出现了如下这个问题
在这里插入图片描述
处理方案package.json 新加 SET NODE_OPTIONS=–openssl-legacy-provider 如下:
在这里插入图片描述
问题描述和解决方案参考的是这个
https://blog.csdn.net/qq_54083224/article/details/131592017

相关功能

验证码前端实现

登录页面位置:ruoyi-ui/src/views/login.vue
1、基本思路:
后端生成一个表达式,3+4=7
3+4=?@7
3+4=?转成图片,传到前端进行展示,7存入redis,然后根据key从redis中获取到存入的value(7)和用户输入的值进行对比(key会传到前端,用户输入的值和key找到的值进行对比),如果相等则验证码校验通过
redis中存放如下图所示:
在这里插入图片描述
2、前端代码
页面都是在views中
在这里插入图片描述
2.1、登录页面一刷新,验证码就有了,说明验证码是登录页面初始化的时候生成的。
2.2、vue初始化的方法在created 里,所以这个登录页面初始化的方法是

created() {
    this.getCode();
    this.getCookie();
  }

getCode()就是获取验证码的方法

getCode() {
    getCodeImg().then(res => {
      this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
      if (this.captchaEnabled) {
        this.codeUrl = "data:image/gif;base64," + res.img;
        this.loginForm.uuid = res.uuid;
      }
    });
  }

getCodeImg()方法成功后的回调里面将 “data:image/gif;base64,” + res.img (图片)存到codeUrl,uuid存到logingForm.uuid中
2.3、getCodeImg 是src/api/login中引入的,@就相当于是src

import { getCodeImg } from "@/api/login";

最终调用的是login.js中的

// 获取验证码
export function getCodeImg() {
  return request({
    url: '/captchaImage',
    headers: {
      isToken: false
    },
    method: 'get',
    timeout: 20000
  })
}

这里请求的url是 /captchaImage,请求的方式是get
我们可以f12跟踪一下,点击页面中的验证码图片访问的请求是:
在这里插入图片描述
这里的请求是封装在request方法中的

import request from '@/utils/request'

引入的request.js中的request方法,这个方法用的Axios访问后端请求(Axios是vue中常用的访问后端服务器的方式)

// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: process.env.VUE_APP_BASE_API,
  // 超时
  timeout: 10000
})

baseUrl表示请求url公共部分,也就是会拼接这部分作为前缀,读取的是开发配置文件的参数:
在这里插入图片描述
验证码的请求:
在这里插入图片描述
http://localhost/dev-api/captchaImage
这里请求的是默认端口(80),也就是请求的是一个前端地址,我们生成验证码图片的是后端生成的,前端获取验证码图片应该是去访问后端地址,这里用到的就是跨域调用
反向代理,url请求前端,进行代理后映射到后端,解决跨域问题。
代理的代码如下:

 proxy: {
      // detail: https://cli.vuejs.org/config/#devserver-proxy
      [process.env.VUE_APP_BASE_API]: {
        target: `http://localhost:8080`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }
    }

代码位置:ruoyi-ui/vue.config.js
这段代码的作用是把 process.env.VUE_APP_BASE_API 的值(也就是/dev-api)替换成空,再映射到 http://localhost:8080
其实访问的地址就变成了 http://localhost:8080/captchaImage

验证码的后端实现

代码位置:ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java

/**
     * 生成验证码
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) throws IOException
    {
        AjaxResult ajax = AjaxResult.success();
        // lxj 获取是否开启验证码功能
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled)
        {
            return ajax;
        }

        // 保存验证码信息
        String uuid = IdUtils.simpleUUID();
        // lxj 验证码的key值 "captcha_codes:+uuid"
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;

        String capStr = null, code = null;
        BufferedImage image = null;

        // 生成验证码
        String captchaType = RuoYiConfig.getCaptchaType();
        if ("math".equals(captchaType))
        {
            String capText = captchaProducerMath.createText();
            // lxj 用@分割 capStr 是 8-5=?
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            // lxj 用@分割 code 是 3
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        }
        else if ("char".equals(captchaType))
        {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }
		// lxj 这里是把数据存到redis中
		// lxj verifyKey是captcha_codes:ef6d1bc28de84d0f99e47e17cad8c137
		// lxj Constants.CAPTCHA_EXPIRATION 是存在redis中的时间是两分钟,验证码两分钟后失效
		// lxj TimeUnit.MINUTES 是时间单位,min
        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return AjaxResult.error(e.getMessage());
        }
		
		// lxj 图片和uuid传到前端
        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }

这里的capText就是验证码,其中"8-5=?"会生成一个图片
在这里插入图片描述
redis数据存在的时间:
在这里插入图片描述

登录的实现

1、login.vue点击登录,调用 handleLogin方法
点击登录事件
@keyup.enter.native=“handleLogin”
登录方法代码如下

    handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true;
          if (this.loginForm.rememberMe) {
          // lxj 如果勾选了保存用户名和密码,那么就把用户名和密码保存到cookies中
            Cookies.set("username", this.loginForm.username, { expires: 30 });
            Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
            Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
          } else {
          // lxj 如果没有勾选,则从cookies中删除
            Cookies.remove("username");
            Cookies.remove("password");
            Cookies.remove('rememberMe');
          }
          // lxj 最终调用的是这个Login方法
          this.$store.dispatch("Login", this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
          }).catch(() => {
            this.loading = false;
            if (this.captchaEnabled) {
              this.getCode();
            }
          });
        }
      });
    }

2、Login 方法
路径:ruoyi-ui/src/store/modules/user.js
代码:

Login({ commit }, userInfo) {
     //拿到用户名、密码、验证码答案、验证码key这些信息
     const username = userInfo.username.trim()
     const password = userInfo.password
     const code = userInfo.code
     const uuid = userInfo.uuid
     return new Promise((resolve, reject) => {
       // 最终调用的是这个login
       login(username, password, code, uuid).then(res => {
         setToken(res.token)
         commit('SET_TOKEN', res.token)
         resolve()
       }).catch(error => {
         reject(error)
       })
     })
   }

该方法封装在action中
3、login方法
路径:ruoyi-ui/src/api/login.js
代码:

// 登录方法
export function login(username, password, code, uuid) {
//传递给后台的数据
 const data = {
   username,
   password,
   code,
   uuid
 }
 return request({
 //调用的后台地址
   url: '/login',
   headers: {
     isToken: false
   },
   //这是一个post请求
   method: 'post',
   data: data
 })
}

这里也是用到了反向代理实现跨域。
4、后台调用 login
位置:
代码:

	@PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

这里的loginBody传的值是:
在这里插入图片描述

String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());

这里的login方法其实做了三个事情
第一个:校验验证码
第二个:校验用户名和密码
第三个:生成token
5、后端生成的token返回到前端,前端拿到token后保存到cookies中。

获取用户角色和权限

在点击登录的时候,有另外两个请求和登录请求一起访问
在这里插入图片描述
这里的getInfo在任意的页面跳转的时候可能都会调用。
如下router.beforeEach()是全局路由管理器,页面跳转的时候都要进到这个方法,这里会去判断是否调用GetInfo方法
路径:src/permission.js
代码如下:

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        isRelogin.show = true
        // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(() => {
          isRelogin.show = false
          store.dispatch('GenerateRoutes').then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            router.addRoutes(accessRoutes) // 动态添加可访问路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch(err => {
            store.dispatch('LogOut').then(() => {
              Message.error(err)
              next({ path: '/' })
            })
          })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

进入到GetInfo
路径 ruoyi-ui/src/store/modules/user.js
代码如下:

// 获取用户信息
    GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        getInfo().then(res => {
          const user = res.user
          const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
          if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            //如果返回有角色,把角色存储到vuex中,做一个全局存储。
            commit('SET_ROLES', res.roles)
            commit('SET_PERMISSIONS', res.permissions)
          } else {
            commit('SET_ROLES', ['ROLE_DEFAULT'])
          }
          commit('SET_NAME', user.userName)
          commit('SET_AVATAR', avatar)
          resolve(res)
        }).catch(error => {
          reject(error)
        })
      })
    }

GetInfo调用getInfo
getInfo
路径:ruoyi-ui/src/api/login.js
代码如下:

// 获取用户详细信息
export function getInfo() {
  return request({
    url: '/getInfo',
    method: 'get'
  })
}

后台代码自行查看。

获取动态菜单路由

在 router.beforeEach 方法中,调用GetInfo,调用GetInfo成功后会调用GenerateRoutes 方法,
GenerateRoutes 代码如下
路径 ruoyi-ui/src/store/modules/permission.js

    // 生成路由
    GenerateRoutes({ commit }) {
      return new Promise(resolve => {
        // 向后端请求路由数据
        getRouters().then(res => {
          const sdata = JSON.parse(JSON.stringify(res.data))
          const rdata = JSON.parse(JSON.stringify(res.data))
          const sidebarRoutes = filterAsyncRouter(sdata)
          const rewriteRoutes = filterAsyncRouter(rdata, false, true)
          const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
          rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
          router.addRoutes(asyncRoutes);
          commit('SET_ROUTES', rewriteRoutes)
          commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
          commit('SET_DEFAULT_ROUTES', sidebarRoutes)
          commit('SET_TOPBAR_ROUTES', sidebarRoutes)
          resolve(rewriteRoutes)
        })
      })
    }

调用getRouters,请求后端,这个就是上一节截图中的另一个方法。

// 获取路由
export const getRouters = () => {
 return request({
   url: '/getRouters',
   method: 'get'
 })
}

后台做的是根据用户查到对应的角色,再去查对应的菜单,然后将对应的菜单路由返回。

首页数据加载

登录成功后跳转到首页

//最终调用的是这个login方法
         this.$store.dispatch("Login", this.loginForm).then(() => {
           //登录成功后,跳转到"/"中(这里在router 中有定义路径)
           this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
         }).catch(() => {
           this.loading = false;
           if (this.captchaEnabled) {
             this.getCode();
           }
         });

"/"对应的路径在路由中有定义
路径:ruoyi-ui/src/router/index.js

 {
 path: '',
 component: Layout,
 redirect: 'index',
 children: [
   {
     path: 'index',
     component: () => import('@/views/index'),
     name: 'Index',
     meta: { title: '首页', icon: 'dashboard', affix: true }
   }
 ]
},

最终跳转到首页页面
路径:ruoyi-ui/src/views/index.vue

用户管理,用户数据PageHelper分页

侧边栏代码路径:src/layout/components/Sidebar/index.vue
这里用了一个循环,把后台的菜单遍历出来了

<sidebar-item
   v-for="(route, index) in sidebarRouters"
    :key="route.path  + index"
    :item="route"
    :base-path="route.path"
/>

左侧菜单栏,点击跳转地址是在数据库中设定的
在这里插入图片描述
从数据库的路径可以找到对应的代码,
比如用户管理对应的数据库路径是:system/user/index
所以对应的代码路径是在:ruoyi-ui/src/views/system/user/index.vue
用户管理页面的代码中主要有getList()获取用户列表和getDeptTree(),这两个方法都是在页面初始化的时候加载的(在create()方法里面的方法会在页面初始化的时候调用)。

created() {
 this.getList();
  this.getDeptTree();
  this.getConfigKey("sys.user.initPassword").then(response => {
    this.initPassword = response.msg;
  });
},

getList(),方法如下
路径:当前页面
代码:

getList() {
      //加载效果,true表示开启加载效果
      this.loading = true;
      listUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
          this.userList = response.rows;
          this.total = response.total;
          //获取到数据后把加载效果关闭,不过这里可能因为请求数据很快,导致我们看不到这个效果
          this.loading = false;
        }
      );
    }

最终调用后台:

// 查询用户列表
export function listUser(query) {
  return request({
    url: '/system/user/list',
    method: 'get',
    params: query
  })
}

调用后台路径是/system/user/list,路径分了两层
在这里插入图片描述
具体的list方法如下:
路径:com/ruoyi/web/controller/system/SysUserController.java

/**
     * 获取用户列表
     */
     //这个注解是用来判断当前用户是否有system:user:list权限,如果有则可以访问这个方法。
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        //处理数据,把数据封装到dataTable中。
        return getDataTable(list);
    }

在redis中可以看到,当前用户的权限是
在这里插入图片描述
所以他是管理员权限,所有方法都可以访问。
startPage();

/**
    * 设置请求分页数据
    */
   public static void startPage()
   {
       //获取分页对象
       PageDomain pageDomain = TableSupport.buildPageRequest();
       Integer pageNum = pageDomain.getPageNum();
       Integer pageSize = pageDomain.getPageSize();
       String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
       //这个参数是用来判断我们传入的参数正确性的,在分页工具类中需要用到,比如传的pageNum值是0,reasonable会变成false,那么在调用了setReasonable(reasonable)方法后会自动设置成 pageNum = 1
       Boolean reasonable = pageDomain.getReasonable();
       // PageHelper 一个分页的工具类,调用工具类的startPage方法传递pageNum(第几页),pageSize(每页数据条数),orderBy(根据哪个字段排序)
       //我们在实现分页功能的时候,只需要把这些值传入进去就可以了
       PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
   }

TableSupport.buildPageRequest();这个方法代码如下

    /**
   * 封装分页对象
   */
  public static PageDomain getPageDomain()
  {
  	//创建分页对象
      PageDomain pageDomain = new PageDomain();
      //给分页对象赋值,这里把前端传过来的 page_num放到pageDomain对象中,
      // ServletUtils.getParameter(PAGE_NUM), 1) 获取前端请求携带的参数,如果没有传值默认给1
      pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));
      pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));
      pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
      pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
      pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
      return pageDomain;
  }

  public static PageDomain buildPageRequest()
  {
      return getPageDomain();
  }

查找用户列表方法
userService.selectUserList(user);
实现类中的代码:

    /**
    * 根据条件分页查询用户列表
    * 
    * @param user 用户信息
    * @return 用户信息集合信息
    */
   @Override
   //给表设置别名,dep的表别名是“d”,user的别名是“u”
   @DataScope(deptAlias = "d", userAlias = "u")
   public List<SysUser> selectUserList(SysUser user)
   {
       return userMapper.selectUserList(user);
   }

部门树状图

/** 查询部门下拉树结构 */
   getDeptTree() {
     deptTreeSelect().then(response => {
       this.deptOptions = response.data;
     });
   },

最终调用user.js中的

// 查询部门下拉树结构
export function deptTreeSelect() {
 return request({
   url: '/system/user/deptTree',
   method: 'get'
 })
}

调用到后台:

/**
     * 获取部门树列表
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/deptTree")
    public AjaxResult deptTree(SysDept dept)
    {
        return success(deptService.selectDeptTreeList(dept));
    }

这里的类中有一部分路径“/system/user”

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值