若依
什么是若依
开源项目
官网: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”