ruoyi若依框架学习笔记-01
ruoyi若依框架分页实现分析
ruoyi若依框架SpringSecurity实现分析
JDK >= 1.8 (推荐1.8版本)
Mysql >= 5.7.0 (推荐5.7版本)
Redis >= 3.0
Maven >= 3.0
Node >= 12
将ruoyi项目从gitee上克隆到本地。
配置数据库
在数据库工具中新建数据库,并运行项目下的sql目录中的两个sql文件
在项目中修改相关配置
配置redis
先确定redis服务已启动,在进入项目文件中进行配置
# 进入项目目录
cd ruoyi-ui
# 安装依赖
npm install
# 强烈建议不要用直接使用 cnpm 安装,会有各种诡异的 bug,可以通过重新指定 registry 来解决 npm 安装速度慢的问题。
npm install --registry=https://registry.npmmirror.com
# 本地开发 启动项目
npm run dev
记录一下我前端项目启动时的一个报错:
95% emitting CompressionPlugin ERROR Error: error:0308010C:digital envelope routines::unsupported
Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:68:19)
at Object.createHash (node:crypto:138:10)
at D:\java\RuoYi-Vue\ruoyi-ui\node_modules\compression-webpack-plugin\dist\index.js:243:42
at CompressionPlugin.compress (D:\java\RuoYi-Vue\ruoyi-ui\node_modules\compression-webpack-plugin\dist\index.js:284:9)
at D:\java\RuoYi-Vue\ruoyi-ui\node_modules\compression-webpack-plugin\dist\index.js:305:12
at _next1 (eval at create (D:\java\RuoYi-Vue\ruoyi-ui\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:14:17)
at eval (eval at create (D:\java\RuoYi-Vue\ruoyi-ui\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:33:1)
at D:\java\RuoYi-Vue\ruoyi-ui\node_modules\copy-webpack-plugin\dist\index.js:91:9
错误原因:nodeJS V17 版本发布了 OpenSSL3.0 对算法和秘钥大小增加了更为严格的限制,nodeJS v17 之前版本没影响,但 V17 和之后版本会出现这个错误。
解决办法:在 package.json 的 scripts 中新增 SET NODE_OPTIONS=–openssl-legacy-provider
添加前:
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build"
},
添加后
"scripts": {
"dev": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build:prod": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build"
},
思路分析:
验证码由 1+1=?@2这种格式组成,后端生成验证码之后uuid和img返回给前端,并将答案存在redis中,其中uuid是指存在redis中的键名,而img是图片流。前端将uuid存在表单中的隐藏域中。当用户提交表单时,先判断验证码是否正确,如果正确,再判断用户名和密码。
首先,找到登录页面,因为在进入登录页面的时候,验证码图片已经被加载好了,所以可以去created方法中看一看执行了哪些方法。
很明显,获取验证码是通过getCode()方法,传统来说,是通过axios向后台发送请求,估计是做了多层封装,这里先分析一下方法内部的响应,先判断验证码开关是否打开,其次如果是开启状态(未定义即开启状态),则为codeUrl和uuid赋值。
我这里很好奇data:image/gif;base64, 是什么意思,百度一下发现:
Data URI scheme是在RFC2397中定义的,目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入。
data表示取得数据的协定名称,image/gif是数据类型名称,base64 是数据的编码方法,逗号后面就是这个image/gif文件base64编码后的数据。
目前,Data URI scheme支持的类型有:
data:,文本数据
data:text/plain,文本数据
data:text/html,HTML代码
data:text/html;base64,base64编码的HTML代码
data:text/css,CSS代码
data:text/css;base64,base64编码的CSS代码
data:text/javascript,Javascript代码
data:text/javascript;base64,base64编码的Javascript代码
data:image/gif;base64,base64编码的gif图片数据
data:image/png;base64,base64编码的png图片数据
data:image/jpeg;base64,base64编码的jpeg图片数据
data:image/x-icon;base64,base64编码的icon图片数据
使用base64图片的优点
使用base64图片的缺点
打开getCodeImg()方法,找到方法定义的文件login.js 首行就看到了import request from ‘@/utils/request’,这和vue-admin-template一样的封装模式
打开request.js文件后看了一下自定义的axios
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API,
// 超时
timeout: 10000
})
打开开发环境配置文件,可以看到
# 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
打开资源检测,可以看到请求地址的的路径中会有’/dev-api’这个字符串,据此可以知道配置文件中的该字符串会自动添加到客户端的请求地址之前,作为统一前缀。
但是后端接口是8080,我的前端的请求地址是80端口。原因:前端加了反向代理,url请求前端,进行代理,映射到后端,解决跨域问题
我们来看一下前端的代理,打开vue-config.js
devServer: {
host: '0.0.0.0',
port: port,
open: true,
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]: ''
}
}
},
disableHostCheck: true
},
dev-api换成’',再映射到http://localhost:8080
这里我们首先要找到对应的接口,但是我们现在只知道接口地址中包含’captchaImage‘,这个时候就可以使用idea的全局搜索了,连按两下shift键,打开全局搜索框。
瞬间就看到我们找的接口了。当然还有一种方式:利用Idea插件,RestfulTool
在接口内部,刚开始就实例化了一个AjaxResult对象,向内挖掘看到,AjaxResult是定义的前端统一返回结果类(VO),而其返回状态码定义在了com.ruoyi.common.constant.HttpStatus类中
boolean captchaEnabled = configService.selectCaptchaEnabled();
这里研究一下它是如何获取配置的,点开方法的实现类中
String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled");
可以看到这里又调用了实例方法:
//这里先从缓存中获取数据,获取对应键下的配置值
String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey)));
//如果存在,直接返回从reids中获取的数据
if (StringUtils.isNotEmpty(configValue))
{
return configValue;
}
//示例化了一个po对象,查询数据库中的配置
SysConfig config = new SysConfig();
config.setConfigKey(configKey);
SysConfig retConfig = configMapper.selectConfig(config);
//如果该配置信息对象不为null,将其直接存入redis缓存中,并返回值
if (StringUtils.isNotNull(retConfig))
{
redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());
return retConfig.getConfigValue();
}
return StringUtils.EMPTY;
这里我展示一下:
如图,为当前缓存,为开启状态
这里我们将true改成false:
发现前端的验证码功能确实消失了。修改数据库的结果我就不展示了,但前提一定要将缓存数据删掉。
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
这里的key其实就是redis中的键,CacheConstants.CAPTCHA_CODE_KEY是一个常量"captcha_codes:",这里冒号的意思是在redis单独生产一个目录。
而旁边的7其实就是生成的验证码答案。
接下来分析验证码实现的代码:
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
//问腿
capStr = capText.substring(0, capText.lastIndexOf("@"));
//答案
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
//将验证码存入redis缓存,并设置有效期
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
RuoYiConfig类是配置文件的映射类,对应的便是yml文件的这一块:
可以看到当前captchaType类型是math,这里captchaProducerMath和captchaProducer属性都是Producer类,其实是google验证码Api(k)提供的生产者类。
login.vue
handleLogin() {
//element-ui表单验证
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
//存入cookies
if (this.loginForm.rememberMe) {
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 {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
//调用vuex方法Login向前端反向代理发送请求
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}
user.js
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
流程分析:
- 校验验证码
- 校验用户名和密码
- 生成token
看一下校验验证码内的业务代码:
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
这段代码是一个异步线程记录日志的代码。暂时先不具体分析。这里抛出的异常也是会被异常处理器捕获。在业务层面不需要管业务结果是否正确,只需在业务逻辑部分抛出异常即可。
//记录用户登录信息
AsyncFactory.recordLogininfor();
//更新用户最近登录状态
recordLoginInfo(loginUser.getUserId());
这段代码就是将用户最新登录信息存入数据库
最后创建token的时候,看代码内部:
//这里存入redis的键是根据uuid生成的
String token = IdUtils.fastUUID();
loginUser.setToken(token);
//设置用户代理
setUserAgent(loginUser);
//刷新token,根据uuid将loginUser缓存
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
//封装uuid对应的值,并存入redis
return createToken(claims);