在上一篇《若依框架:前端项目结构与初始页面渲染流程》中,我们探讨了与“vue.config.js文件配置、.env模式和环境变量配置、vue-router全局导航守卫配置、vue-router路由配置简介”相关的内容,书接上回,我们继续探讨若依前端项目的初始页面组件Login.vue的初始渲染逻辑,以及图像验证码实现逻辑、用户登录逻辑的解析。
目录
登录组件初始渲染逻辑
图像验证码逻辑
前端实现逻辑
后端验证码图像生成逻辑
@Resource注解
AjaxResult统一响应结果封装
HttpStatus响应状态码封装
@ConfigurationProperties注解
Redis缓存工具
springframework提供的FastByteArrayOutputStream类
前端Cookie信息读取和存储逻辑
用户登录逻辑
Vuex:全局状态值的异步更新操作
Axios:二次封装与拦截器配置
登录组件初始渲染逻辑
登录组件为Login.vue,借助Element-UI的表单组件el-form实现页面布局。初始渲染时,在组件的created生命周期阶段, 主要做了两件事情:
①调用后端接口http://localhost/dev-api/captchaImage,获取以Base64字符串形式表示的验证码图像,以及其它参数;
②借助js-cookie第三方依赖库从本地Cookie中获取已经缓存过的用户登录信息。
有关具体的业务处理逻辑,将在后面部分进行介绍。
此外,对应单击“登录”按钮,实现用户登录验证功能,Login.vue组件也为按钮注册了点击事件,并提供了handleLogin()回调函数,用于执行用户登录验证、登录成功后的用户信息缓存(缓存至Cookie和Vuex中),以及路由切换至“/index”主页页面的处理逻辑。
图像验证码逻辑
前端实现逻辑
图像验证码的前端逻辑实现,主要是:Login.vue组件在created()生命周期阶段,调用后端接口http://localhost/dev-api/captchaImage,获取包含了Base64字符串形式的图片序列,然后将其转换为Base64形式的图片链接,将其设置到<img/>标签的src属性(codeUrl)上。具体代码实现被封装在Login.vue组件的getCode()方法中。
/**
* 获取验证码
*/
getCode() {
getCodeImg().then(res => {
console.log(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;
}
});
},
调用后端接口时,返回的基本信息如下,
{
"msg": "操作成功",
"img": "Base64字符串形式的image图片资源",
"code": 200,
"captchaEnabled": true,
"uuid": "0f8c9fab3ce8485e9779ef9515852c74"
}
其中:①captchaEnabled字段表示后端接口是否可以返回一个验证码图像,如果为false,则无法返回,对应的Login.vue组件就不会显示验证码这一项;反之为true时,则后端接口会返回一个Base64字符串形式的验证码图像。这个逻辑是基于Vue的v-if条件渲染实现的。
②uuid字段,对应的是后端存储在Redis缓存中的图像验证码表达式的正确计算结果,当用户点击登录时,会在loginForm属性中随着用户名、密码、是否记住密码、用户输入的验证码值一起被提交给后端接口,凭借这个uuid,后端接口在进行用户登录验证时,可以与Redis中存储的正确计算结果进行对比,以此判断用户登录信息是否有误。
后端验证码图像生成逻辑
图像验证码的前端处理逻辑其实比较简单,仅仅涉及后端接口请求,与Base64字符串形式的图像资源的处理与<img/>标签的src属性动态值的绑定操作。
至于后端部分,处理逻辑则较为复杂。
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();//AjaxResult-统一返回结果的封装-[返回成功的消息]
boolean captchaEnabled = configService.selectCaptchaEnabled();
ajax.put("captchaEnabled", captchaEnabled);//根据返回的布尔值判断是否允许使用图片验证码
if (!captchaEnabled)
{
//不允许-直接返回ajax响应结果
return ajax; //msg:操作成功,code:200,data:null
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();//生成uuid-[简化的UUID,去掉了横线]
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; // 'captcha_codes'+uuid
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType(); //math
if ("math".equals(captchaType))
{
//创建一个表达式 x operator y = ? @ result
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@")); //获取表达式 x operator y = ?
code = capText.substring(capText.lastIndexOf("@") + 1); //获取结果 result
image = captchaProducerMath.createImage(capStr); // 创建一个BufferedImage对象
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
//将表达式的结果result存入redis缓存中,并设置过期时间-[以分钟为单位]
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出-【java.io.ByteArrayOutputStream类的替代品,OutputStream的直接子类,由 org.springframework.util包提供】
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os); //将图片数据写入FastByteArrayOutputStream对象中
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray())); //图片转化为Base64编码
return ajax; //返回对象
}
首先,这个处理前端http://localhost/dev-api/captchaImage请求的后端接口被放在ruoyi-admin模块下的controller/common/CaptchaController控制器中;其次,该接口对应的类成员方法如上,每一句代码的含义已通过注释进行标注;最后,该接口中涉及到了如下几个知识点:
①@Resource自动装配注解;
②AjaxResult统一响应结果封装;
③@ConfigurationProperties注解读取application.yml的配置属性信息;
④Redis缓存工具类com.ruoyi.common.core.redis.RedisCache;
⑤springframework提供的FastByteArrayOutputStream,作为java.io.ByteArrayOutputStream字节数组流的替代类使用;
⑥HttpStatus响应状态码封装。
以下对上述6点内容进行逐一介绍。
@Resource注解
@Resource在Spring/SpringBoot框架中,可用于实现类的成员属性的自动装配,该注解源码如下,主要包含7个属性,其中最重要的两个参数是:name 和 type 。
package javax.annotation;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
/**
* 此注解用于标识应用程序所需要的资源.,可以用于修饰组件类、以及类的字段、方法。
当注解被应用到字段或者方法上,组件初始化时,container容器就会注入一个资源对象对应的的实例;
当注解被应用到class类上,就声明了一个应用程序将在运行时查找的资源。
*
* @since Common Annotations 1.0
*/
@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
/**
* 资源的JNDI名称
*/
String name() default "";
/**
* 引用所指向的资源的名称
*/
String lookup() default "";
/**
* 资源对应的Java数据类型,默认是Object类型
*/
Class<?> type() default java.lang.Object.class;
/**
* The two possible authentication types for a resource.
*/
enum AuthenticationType {
CONTAINER,
APPLICATION
}
/**
* 使用资源时的验证类型,枚举类型
enum AuthenticationType {
CONTAINER,
APPLICATION
}
*/
AuthenticationType authenticationType() default AuthenticationType.CONTAINER;
/**
* 用于判断当前资源是否可以在不同的Bean实例中被共享
*/
boolean shareable() default true;
String mappedName() default "";
/**
* 资源的描述信息
*/
String description() default "";
}
同时,由于@Resources注解是jdk原生提供的,因此该注解可以应用在任何Java后端框架中。关于Spring中@Autowired和@Resource的区别,可以查看参看博客。
AjaxResult统一响应结果封装
若依框架后端部分,对于接口响应结果进行了统一地封装,对应com.ruoyi.common.core.domain.AjaxResult实体类,该类作为HashMap<String, Object>的子类,源码如下,对success成功消息、warn警告消息、error错误消息进行了区分,并提供了对应的静态方法可供直接调用。
/**
* 操作消息提醒
*
* @author ruoyi
*/
public class AjaxResult extends HashMap<String, Object>
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult()
{
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
*/
public AjaxResult(int code, String msg)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (StringUtils.isNotNull(data))
{
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success()
{
return AjaxResult.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data)
{
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static AjaxResult success(String msg)
{
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data)
{
return new AjaxResult(HttpStatus.SUCCESS, msg, data);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult warn(String msg)
{
return AjaxResult.warn(msg, null);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult warn(String msg, Object data)
{
return new AjaxResult(HttpStatus.WARN, msg, data);
}
/**
* 返回错误消息
*
* @return 错误消息
*/
public static AjaxResult error()
{
return AjaxResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 错误消息
*/
public static AjaxResult error(String msg)
{
return AjaxResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 错误消息
*/
public static AjaxResult error(String msg, Object data)
{
return new AjaxResult(HttpStatus.ERROR, msg, data);
}
/**
* 返回错误消息
*
* @param code 状态码
* @param msg 返回内容
* @return 错误消息
*/
public static AjaxResult error(int code, String msg)
{
return new AjaxResult(code, msg, null);
}
/**
* 方便链式调用
*
* @param key 键
* @param value 值
* @return 数据对象
*/
@Override
public AjaxResult put(String key, Object value)
{
super.put(key, value);
return this;
}
}
HttpStatus响应状态码封装
在查看AjaxResult类的源码时,我们发现若依框架其实内部对接口响应时的状态码也进行了封装,对应于com.ruoyi.common.constant.HttpStatus类,源码如下,
/**
* 返回状态码
*
* @author ruoyi
*/
public class HttpStatus
{
/**
* 操作成功
*/
public static final int SUCCESS = 200;
/**
* 对象创建成功
*/
public static final int CREATED = 201;
/**
* 请求已经被接受
*/
public static final int ACCEPTED = 202;
/**
* 操作已经执行成功,但是没有返回数据
*/
public static final int NO_CONTENT = 204;
/**
* 资源已被移除
*/
public static final int MOVED_PERM = 301;
/**
* 重定向
*/
public static final int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
public static final int NOT_MODIFIED = 304;
/**
* 参数列表错误(缺少,格式不匹配)
*/
public static final int BAD_REQUEST = 400;
/**
* 未授权
*/
public static final int UNAUTHORIZED = 401;
/**
* 访问受限,授权过期
*/
public static final int FORBIDDEN = 403;
/**
* 资源,服务未找到
*/
public static final int NOT_FOUND = 404;
/**
* 不允许的http方法
*/
public static final int BAD_METHOD = 405;
/**
* 资源冲突,或者资源被锁
*/
public static final int CONFLICT = 409;
/**
* 不支持的数据,媒体类型
*/
public static final int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
public static final int ERROR = 500;
/**
* 接口未实现
*/
public static final int NOT_IMPLEMENTED = 501;
/**
* 系统警告消息
*/
public static final int WARN = 601;
}
既然若依框架内部对接口响应时的状态码进行了封装,那么在前端项目中,对于axios必定也进行了对应的响应拦截器的配置,这部分内容在后边部分进行介绍。
@ConfigurationProperties注解
@ConfigurationProperties注解,可以用于读取application.yml的配置属性信息,并将其转换为Class类的属性、或者直接转换为Class实体类的属性值使用。
①可以配合@Bean注解使用,用于在某个配置类中完成Bean实例的自动装配;
②将属性转换成bean对象,配合@component注解使用。例如:若依后端项目中com.ruoyi.common.config.RuoYiConfig类就是通过@component+ @ConfigurationProperties注解,基于application.yml配置文件中的属性信息,自动完成RuoYiConfig实体类的属性注入的。
Redis缓存工具
SpringBoot如何整合Redis呢?这有赖于Spring Data子项目:Spring Data Redis成员的支持。如下依赖项在ruoyi-common模块中被引入。
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
并且,若依框架为了方便Redis的缓存操作,也封装了内部工具类com.ruoyi.common.core.redis.RedisCache,提供了设置、删除、查询缓存列表等的基本方法,并通过@Component注解交由Spring容器进行管理。
package com.ruoyi.common.core.redis;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
/**
* spring redis 工具类
*
* @author ruoyi
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
springframework提供的FastByteArrayOutputStream类
在后端接口返回Image二进制图像资源时,是以二进制流的形式返回的。jdk原生API提供了java.io.ByteArrayOutputStream字节数组流,可以实现bye数组数据的传输。而springframework通过继承OutputStream父类,内置定义并提供了FastByteArrayOutputStream类,相比原生API,性能更优。
提及此处,我们可以尝试基于此类,编写自己的验证码图形接口。后端接口示例代码如下,
package com.xwd.controller;
import com.xwd.common.AjaxResult;
import com.xwd.common.Base64;
import org.springframework.stereotype.Controller;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.nio.Buffer;
/**
* @className ImageController
* @description: com.xwd.controller
* @auther: xiwd
* @date: 2023-01-01 - 01 - 01 - 00:16
* @version: 1.0
* @jdk: 1.8
*/
@Controller(value = "com.xwd.controller.ImageController")
@RequestMapping(value = "/image")
public class ImageController {
//properties
//methods
@ResponseBody
@RequestMapping(value = "/verify")
public AjaxResult getVerifyImage(){
AjaxResult ajaxResult = AjaxResult.success();
//创建图片
BufferedImage image = new BufferedImage(200,50, BufferedImage.TYPE_INT_RGB);
Graphics graphics = image.getGraphics();//获取画笔
graphics.setColor(Color.PINK);
graphics.fillRect(0,0,200,300);
//设置字体样式
Font font = new Font("gothic",Font.PLAIN,14);
graphics.setFont(font);
//设置字体颜色
graphics.setColor(Color.BLUE);
//写入文字
graphics.drawString("Hello",(200-14*5)/2,25);
//获取流数据
FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
try{
ImageIO.write(image,"jpg",outputStream);
}catch (Exception e){
return AjaxResult.error(e.getMessage());
}
//数据流转base64编码
ajaxResult.put("image", Base64.encode(outputStream.toByteArray()));
return ajaxResult;
}
}
相应结果如下,其中,image字段对应的就是FastByteArrayOutputStream实例转换过来的Base64编码字符串。
我们通过前端代码,将其展示到html页面中,
PS:此处为了展示效果,直接硬编码将Base64编码的字符串设置到img标签的src属性,最终显示结果如下图所示,这证明我们的思路是正确的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<img src="data:image/gif;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAyAMgDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD0eiiiuM+kCiiigAooooAKKKKACimJLHI0ipIrNG21wpyVOAcH0OCD9CKIpY54UmhkWSJ1DI6HIYHkEEdRTs0K4+ionureNykk8SOuzKs4BG9tq/mQQPU8VLQ01uFwooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDnLWLVH1jXDZXlnDF9sTKzWrSkn7PDzkSLx04x+NZGmR399aaZawJ5sMGjWcgX+0ZrTazhwTmJSWyEXr0xx1NdukUcbSMkaq0jbnKjBY4AyfU4AH0AqrcaNpd3DBDc6bZzRQLthSSBWEY4GFBHA4HT0FepTzCKTjJdI2aWqsrdLN39dvufNKg3qn3/ABfz/I5eZpnkVri4iuJmg0YyTREFJG+1PllIAGCeRxVvdL9j/tb7Tc/a/wC1fs//AB8P5fl/bPJx5edn3OM7c556810r2tvI5eSCJ3bZlmQEnY25fyJJHoeah/svT/7Q/tD7Dbfbf+fjyV8zpt+9jPTj6U/r8Gtrap6dVZLl6aO2vy0Yexa6/wBX39S3RRRXlHSFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//2Q==" alt="">
</body>
</html>
前端Cookie信息读取和存储逻辑
以上内容是对图像验证码的前后端实现逻辑的剖析,接下来我们探讨一下初始渲染时,Login登录组件的created生命周期阶段, 做的另一件事情:借助js-cookie第三方依赖库从本地Cookie中获取已经缓存过的用户登录信息。
这里js-cookie第三方依赖主要是提供了面向原生Cookie的增删改查API接口,具体的逻辑代码则被封装在Login.vue组件的getCookie()方法中,源码如下,
/**
* 从Cookie中读取信息
*/
getCookie() {
//从Cookie中获取值
const username = Cookies.get("username"); // undefined
const password = Cookies.get("password"); // undefined
const rememberMe = Cookies.get('rememberMe') // undefined
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),//密码解密
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
},
此处所实现的功能主要是:在Login登录组件初始化时,尝试从Cookie中获取被缓存的用户信息,并基于Vue表单组件的响应式特性将其填充到el-form表单组件的对应位置处。正因为这个逻辑,我们才可以看到登录组件显示了默认的用户名、密码信息。
当然,js-Cookie工具库在之后的用户登录逻辑中也有涉及到。
用户登录逻辑
前面分析了一大堆,现在终于来到用户的表单登录逻辑了,对应的代码被写在“登录”按钮的点击回调函数中,源码如下,其中也包含了我自己写的一些代码注释内容,
handleLogin() {
//表单验证-Element-ui的$refs.loginForm.validate()接口
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true; // 切换为显示登陆中
//是否记住密码
if (this.loginForm.rememberMe) {
//记住密码-设置到Cookie中-[过期时间为30天]
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 {
//不记住密码-将上一次设置到Cookie中的值移除
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
//提交用户信息到Vuex中
this.$store.dispatch("Login", this.loginForm).then(() => {
//路由跳转
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
//登陆失败时-取消loading显示,并尝试重新获取验证码图片资源
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}
至于表单验证规则,Element-UI的el-form表单组件是可配置的,配置信息如下,主要是面向用户名、密码、验证码的非空判定。
//表单验证规则配置
loginRules: {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" }
],
password: [
{ required: true, trigger: "blur", message: "请输入您的密码" }
],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
},
注意到:在用户登录逻辑中,涉及到了this.$store.dispatch("Login", this.loginForm)——Vuex全局状态管理、this.$router.push({ path: this.redirect || "/" })路由跳转相关的内容。
以下,我们将继续探讨此处针对全局状态管理的处理逻辑。
Vuex:全局状态值的异步更新操作
Vuex为Vue前端应用提供了全局变量共享的能力,以及同步/异步更新这些全局变量的接口。同时,应当认识到:Vuex的store仓库中存储的状态值是响应式的,这意味着状态值的更新会引起组件中的更新。面向同步/异步的状态值提交,Vuex提供了mutation的commit提交、actions的dispatch提交接口。
在Login组件的handleLogin()方法中,通过this.$store.dispatch()接口异步触发了store仓库中的Login方法,在异步执行流程中,对SET_TOKEN的值进行了同步更新,具体处理逻辑源码如下,
// 登录
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 => {
console.log(res);
//将token设置到Cookie中
setToken(res.token)
//存储token到Vuex中
commit('SET_TOKEN', res.token)
//修改Promise对象的状态
resolve()
}).catch(error => {
reject(error)
})
})
},
实质上,这里只是调用了一个login用户登录接口,然后根据接口响应结果,返回一个Promise对象,以便进行后续处理。登录接口定义如下,
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
url: '/login',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
Axios:二次封装与拦截器配置
再次注意到,上述login()方法调用接口时,是通过调用request()方法实现的,该方法其实是对Axios第三方依赖库的二次封装。
二次封装有什么好处呢?就在于它可以对Axios对象进行自定义化的配置,例如:请求拦截器、响应拦截器,在HTTP请求发出之前、收到后端响应结果之后,做一些过滤拦截处理操作,实现一些权限控制等操作。
还记得之前我们提到过的后端自定义的HTTP响应状态码吗?通过Axios二次封装,自定义响应拦截器,就可以实现针对不同的状态码的统一处理。
若依前端框架中对Axios的二次封装脚本request.js文件源码如下,
import axios from 'axios'
import { Notification, MessageBox, Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from "@/utils/ruoyi";
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API,
// 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器-拦截器设置
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if(res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer'){
return res.data
}
//判断状态码的值-非200的状态码会被拦截掉
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
Message({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
Message({ message: msg, type: 'warning' })
return Promise.reject('error')
} else if (code !== 200) {
Notification.error({ title: msg })
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
export default service
本篇内容涉及的知识点细节比较多,导致在最后介绍Vuex全局状态管理和Axios二次封装相关的内容介绍的比较粗略,之后会继续对这部分内容进行细化探讨。当然若有介绍不到位或者出错的地方,还请道友们海涵,我亦静候指正。