若依框架:前端登录组件与图像验证码

文章详细分析了若依框架前端项目的登录组件Login.vue的初始渲染逻辑,包括获取和展示图像验证码,以及用户登录验证的过程。同时,介绍了后端生成验证码的逻辑,使用了SpringDataRedis进行缓存,并讲解了SpringBoot中@ConfigurationProperties注解、AjaxResult统一响应结果封装和HttpStatus状态码的应用。
摘要由CSDN通过智能技术生成

        在上一篇《若依框架:前端项目结构与初始页面渲染流程》中,我们探讨了与“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实体类的属性注入的。

被@Component+@ConfigurationProperties注解修改的配置类RuoYiConfig
被读取的项目配置信息-application.yml

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请求发出之前、收到后端响应结果之后,做一些过滤拦截处理操作,实现一些权限控制等操作。

请求拦截器的请求头配置-附加Token认证信息

        还记得之前我们提到过的后端自定义的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二次封装相关的内容介绍的比较粗略,之后会继续对这部分内容进行细化探讨。当然若有介绍不到位或者出错的地方,还请道友们海涵,我亦静候指正。

  • 5
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是席木木啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值