若依框架:前端登录组件与图像验证码|用户登录逻辑

news2024/12/25 14:28:12

        在上一篇《若依框架:前端项目结构与初始页面渲染流程》中,我们探讨了与“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二次封装相关的内容介绍的比较粗略,之后会继续对这部分内容进行细化探讨。当然若有介绍不到位或者出错的地方,还请道友们海涵,我亦静候指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/132606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言进阶】字符函数与字符串函数

目录 1、函数介绍 1.1 strlen 1.2 strcpy 1.3 strcat 1.4 strcmp 1.5 strncpy 1.6 strncat 1.7 strncmp 1.8 strstr 1.9 strtok 1.10 strerror 【补】字符分类函数&#xff1a; 1.11 memcpy 1.12 memmove 1.13 memcmp 1.14 memset 1、函数介绍 1.1 strlen siz…

基于卷积神经网络的高光谱分类(1D、2D、3D-CNN)

算法原理 卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;CNN&#xff09;是深度学习中最常见的一种 算法&#xff0c;它具有强大的特征学习能力。CNN 通过结合局部感知区域、共享权重、空间或者 时间上的降采样来充分利用数据本身包含的局部性等特征&…

绘图仪 与 示波器 Plotter Oscilloscope

【后台管理&#xff0c;这哪里是广告了&#xff1f;图都是百度搜的&#xff0c;又没有销售信息&#xff0c;就事论事而已&#xff01;】 Plotter &#xff1a; 对低频信号持续测量并绘制到一张很长的纸上&#xff0c;通常是卷纸。 常见的比如传统心电图机&#xff08;图左&am…

『分分钟玩转VueRouter●下』我对VueRouter在项目中如何使用的理解

路由的设置会根据系统中用户角色的数量而有所不同&#xff0c;大致分为三种单角色同权限、单角色不同权限、多角色。这里的角色均是只一种身份&#xff0c;而不是用户量。接下来的讲解纯属个人见解&#xff0c;大型项目会将不同权限的用户直接分开开发不同的系统。如果是小型多…

c++基础——for循环

for循环是循环的一种 以下是 for 循环的结构&#xff1a; for (初始化; 判断条件; 更新) {循环体; } 执行顺序&#xff1a; for 语句的三个部分中&#xff0c;任何一个部分都可以省略。其中&#xff0c;若省略了判断条件&#xff0c;相当于判断条件永远为真。 for (int i …

fpga实操训练(从模块到系统开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们学习了fpga的一些基本操作&#xff0c;熟悉了这些操作&#xff0c;基本上说fpga已经入门了。但是距离我们用fpga开发产品&#xff0c;这中…

Faster RCNN网络源码解读(Ⅷ) --- RPN网络代码解析(下)RegionProposalNetwork类解析

目录 一、代码作用&#xff08;rpn_function.py&#xff09; 二、代码解析 2.1 RegionProposalNetwork类 2.1.1 正向传播过程forward 接着上篇博客的2.1.2节 2.1.2 assign_targets_to_anchors 2.1.3 det_utils.Matcher传入参数 2.1.4 compute_loss 2.1.5 smooth_l1_lo…

你真的会正确使用wait和notify么?

目录 wait和notify原理 API wait 与 sleep的区别 wait 和 notify的正确使用 step1 step2 step3 step4 step5 总结waitnotify wait和notify原理 当我们线程获取某个对象的monitor锁的时候就会成为owner线程,当owner线程条件不满足的时候,就会调用wait方法,该线程就会进…

惠州市政企信息化(互联网)市场调研报告

1.引言 1.1.编写目的 据广东省惠州市惠东县的政企信息化市场调研的客观性数据&#xff0c;分析相关数据&#xff0c;确定市场规模、市场潜力、市场需求&#xff0c;以及需求价值&#xff0c;为后续的市场决策、服务组合决策提供依据&#xff0c;也作为未来根据市场变化而调整…

Nacos 注册中心

Nacos 注册中心 目录概述需求&#xff1a;设计思路实现思路分析1.增加 Maven 依赖2.Client端配置注册中心3.Server端配置注册中心4.Nacos 注册中心参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c…

Java--Map接口详解

目录 Map接口的特点 代码实现 代码实现 Map的常用方法 代码实现 Map接口的4种遍历方法 代码实现 第一种方式 第二种方式 第三种方式 第四种方式 Map接口的特点 1)Map与Collection并列存在。用于保存具有映射关系的数据&#xff1a;Key-Value 2)Map中的key和value可以…

如何在星巴克连接家中Windows台式机?(安卓,iOS, Windows, macOS配合frp穿透公网IP实现)

zhaoolee 最近热衷于和海外热心老哥们交换硬盘中的单机游戏资源&#xff08;BT下载&#xff09;&#xff0c;家中有Windows台式机&#xff0c; 适合长时间挂机下载BT资源&#xff0c;zhaoolee希望能随时连接到Windows台式机新增下载任务&#xff0c;安装体积超大的主机游戏。 …

End-to-End Object Detection with Transformers论文阅读笔记

End-to-End Object Detection with Transformers 端到端&#xff0c;不需要NMS后处理了&#xff0c;直接出结果。 1、Abstract 将目标检测作为一个集合预测问题来解决。简化了检测的整体流程&#xff0c;有效的消除了许多人工设计的部分&#xff0c;比如NMS&#xff0c;anch…

数据库连接池(C++11实现)

目的&#xff1a; 因为对数据库的操作实质上是对磁盘的IO操作&#xff0c;所以如果对数据库访问次数过多&#xff0c;就会到导致大量的磁盘IO&#xff0c;为了提高MySQL数据库&#xff08;基于C/S设计&#xff09;的访问瓶颈&#xff0c;除了在服务器端增加缓存服务器缓存常用的…

还在用BERT做文本分类?分享一套基于预训练模型ERNIR3.0的文本多分类全流程实例【文本分类】

目录&#x1f340;一、前言&#x1f331;二、多分类场景简介&#x1f343;三、前期准备阶段&#x1f7e5;3.1 运行环境准备&#x1f7e7;3.2 文心ERNIE系列模型介绍&#x1f7e8;3.3 预训练模型加载⬜3.4 加载项目代码&#x1f490;四、数据准备阶段&#x1f7e9;4.1 数据处理流…

变不可能为可能——记房产推销员佟鑫海

有勤奋&#xff0c;就会有所收获。傲人的成绩和背后的努力密切相关。俗话说得好&#xff0c;没卖不掉的房子&#xff0c;仅有卖不掉房子的艺人经纪人。关键是你是否有恒心。 在明升&#xff0c;总会有这样一群影子&#xff0c;他们每天精力旺盛&#xff0c;衣着光鲜&#xff0…

【C/C++ SOCKET编程】基于TCP协议实现服务器客户端的简单通信

什么是SOCKET Socket又称"套接字"&#xff0c;应用程序通常通过"套接字"向网络发出请求或者应答网络请求&#xff0c;使主机间或者一台计算机上的进程间可以通讯。 TCP/IP协议 从字面意义上讲&#xff0c;有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议…

unsafe.Pointer和uintptr的区别

unsafe 包 func Alignof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Sizeof(x ArbitraryType) uintptr type ArbitraryType int type Pointer *ArbitraryType在unsafe包中&#xff0c;只提供了3个函数&#xff0c;两个类型。就这么少的量&#xf…

【数据结构进阶】布隆(Bloom Filter)过滤器【哈希+位图的整合】

布隆(Bloom Filter)过滤器【哈希位图的整合】 1、什么是布隆过滤器&#xff1f; 布隆过滤器&#xff08;Bloom Filter&#xff09;是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空…

物联网与射频识别技术,课程实验(四)

实验4—— 基于帧的时隙ALOHA(FSA)算法的实现与性能分析 实验说明&#xff1a; 利用Python或Matlab模拟基于帧的时隙ALOHA算法&#xff1b; 分析标签数量k、帧中所含时隙个数n对信道利用率的影响&#xff0c;其中&#xff0c; 信道利用率发送数据的时间/(发送数据的时间信道空…