李成笔记网

专注域名、站长SEO知识分享与实战技巧

Vue + SpringBoot 项目实战(五):我的助手之前端路由与登录拦截器

在上篇文章末尾,提到了登录功能还有一些缺陷,就是如何避免绕过登录功能直接登录,这篇文章主要解决这个问题,因此引入了一个新的内容,来协助我们完成这个功能。

前言:

在前面文章当中,当时登录时,我们需要加上 # ,来完成页面的访问。但是在实际工作中,大多数会通过,直接访问一个网址路径,并不携带这个 #, 下面就说说这两种方式。

1.前端路由

前端中的路由模式有两种,一种是hash模式,一种是history模式。下面说下,我查询到的一些介绍,这部分感觉枯燥的话,可以跳过。不会太长篇幅。

①.hash模式的由来

  • 缘由:hash,即地址栏URL中的#符号(此hash不是密码学里的散列运算)。hash路由模式的出现,主要是利用了浏览器对hash值的处理特性,即当hash值发生改变时,浏览器不会向服务器发送请求,而是会触发onhashchange事件。
  • 背景:在早期的Web开发中,由于前端技术相对落后,页面跳转通常由后端控制。但随着Ajax等技术的出现,前端开始有能力在不重新加载页面的情况下更新视图,这就催生了前端路由的需求。hash路由作为前端路由的一种实现方式,因其简单、兼容性好而被广泛使用。
  • 补充在hash模式下,URL的路径会以 # 符号作为分隔符,后面跟随一个不会提交到服务器的片段标识符(hash)。例如,http://example.com/#/home。hash模式的原理是监听浏览器的 hashchange事件,当URL的hash发生变化时,Vue会根据相应的配置加载对应的组件。hash模式的特点如下:
    • 兼容性好。Hash模式支持现代所有的浏览器,并且在不支持HTML5 History API 的旧版浏览器上也能正常工作。
    • 简单配置。在Vue路由中,默认使用Hash模式,不需要额外的配置。
    • 易于部署,由于使用了Hash模式,URL发生变化时不会触发页面刷新,因此部署时,只需要将静态文件部署到服务器即可。

②. history模式的由来

  • 缘由:history路由模式利用 HTML5 History Interface 中新增的pushState()replaceState()方法。这两个方法允许开发者在不重新加载页面的情况下,修改浏览器的历史记录和当前URL。由于通过history的API进行路由跳转不会向服务器发起请求,因此也达到了前端路由的目的。
  • 背景:随着HTML5标准的推广和浏览器对HTML5特性的支持不断完善,history路由模式逐渐成为前端路由的主流实现方式之一。与hash路由相比,history路由可以提供更加美观的URL(没有#符号),并且可以更好地与后端路由和服务器端渲染配合使用。
  • 补充在history模式下,URL中没有 #,使用的是传统的路由分发模式。例如:http://abc.com/user/id。特点如下:
    • 兼容性不好。对于不支持HTML5 History API的旧版浏览器上不能正常工作。
    • 需要服务端支持。对于history模式,需要后台配置支持,用来处理路径 '/' 请求,否则会导致路由不生效。
    • 对于前端路由来说,没有浏览器的hash机制,不能使用浏览器的前进后退功能。

③.两者的优点与区别


2.使用 History 模式

首先我们把 Vue 中配置的路由从默认的 hash 模式切换为 histroy 模式。打开我们的前端项目 wj-vue,修改 router\index.js,加入 mode: 'history 这句话。整体代码如下:

import Vue from 'vue'
import Router from 'vue-router'
import HomePage from '@/components/commonManager/HomePage.vue'
import Login from '@/components/LoginManager/LoginIn.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: LoginIn
    },
    {
      path: '/index',
      name: 'HomePage',
      component: HomePage
    }
  ]
})

运行项目,访问不加 # 号的 http://localhost:8080/login ,成功加载页面。

3.Vuex 与前端登录拦截器

在前面有说过,现在的登录,其实是不完善的,如果直接访问路径,它是可以绕过登录页面的,接下来我们要做的,就是在前端去做这么一个事情,不管任何请求,在访问路由之前,都先经过一个状态值去判断,下面要说的就是VueX。

Vuex 是专门为Vue开发的一个状态管理方案,对于一个全局属性,我们可以使用它进行管理。接着我们去引入和使用它。

3.1 引入VueX

在我们的项目文件夹中,运行 npm install vuex --save ,之后,在 src 目录下新建一个文件夹 store,并在该目录下新建 index.js 文件,在该文件中引入 vue 和 vuex,代码如下:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

之后,我们在 index.js 里设置我们需要的状态变量和方法。为了实现登录拦截器,我们需要一个记录用户信息的变量。为了方便日后的扩展(权限认证等),我们使用一个用户对象而不是仅仅使用一个布尔变量。同时,设置一个方法,触发这个方法时可以为我们的用户对象赋值。完整的代码如下:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: window.localStorage.getItem('token' || '[]') == null? '': JSON.parse(window.localStorage.getItem('token' || '[]')).token,
  },
  mutations: {
        login (state, user) {
            state.token = user;
            window.localStorage.setItem('token',JSON.stringify(state.token));
        },
  }
})

这里我们还用到了 localStorage,即本地存储,在项目打开的时候会判断本地存储中是否有 user 这个对象存在,如果存在就取出来并获得 token 的值,否则则把 token 设置为空。这样我们只要不清除缓存,登录的状态就会一直保存。

3.2 修改路由配置

为了区分页面是否需要拦截,我们需要修改一下 src\router\index.js,在需要拦截的路由中加一条元数据,设置一个 requireAuth 字段如下:

    {
      path: '/index',
      name: 'AppIndex',
      component: HomePage,
      meta: {
        requireAuth: true
      }
    }

完整的 index.js 代码如下:

import Vue from 'vue'
import Router from 'vue-router'
import HomePage from '@/components/commonManager/HomePage.vue'
import Login from '@/components/LoginManager/LoginIn.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '/index',
      name: 'AppIndex',
      component: HomePage,
      meta: {
        requireAuth: true
      }
    }
  ]
})

3.3 使用钩子函数判断是否拦截

钩子函数及在某些时机会被调用的函数。这里我们使用 router.beforeEach(),意思是在访问每一个路由前调用。

打开 src\main.js ,首先添加对 store 的引用。完整版代码如下:

// 导入 store
import store from './store';

new Vue({
  router,
  // 添加到这里
  store,
  render: h => h(App)
}).$mount('#app')

接着写 beforeEach() 函数。下面是我查询之后,并补充的注释信息。

/**  
*  beforeEach()方法各个字段解释说明:
*  to: 到什么地方
*  from:from:从哪个路由离开
*  next:路由控制参数
*/
router.beforeEach((to,from,next) =>{
  if (to.meta.requireAuth) {
    if(store.state.token){
      next()
    }else {
      next({
        path: 'login',
        query: {redirect: to.fullPath}
      })
    }
  }else {
    next()
  }
})

上面的代码含义可以理解为:当我们想要直接去访问需要认证的 /index 路由时,我们会先判断这个路由的 meta.require 信息,也就是 src\router\index.js 中的代码,如下图

    {
      path: '/index',
      name: 'AppIndex',
      component: HomePage,
      meta: {
        requireAuth: true
      }
    }

只有取到的结果为 true,才能进行访问,否则返回到 /login 登录页面。这样就解决了,未登录用户禁止通过直接访问路由进行跳转。

到了上面所说内容,前端部分的登录拦截就告一段落了。

4.后端状态校验值编写

在上面的介绍中,反复提到了一个状态值,在这里的话,我使用的是 token 来表示这个状态值。并会进行介绍它。

  1. Token:指的是任何形式的令牌,用于在客户端和服务器之间传递身份验证授权信息。它通常由服务器生成并返回给客户端,客户端在每次请求时都会携带这个Token以证明其身份和权限。
  2. JWT(JSON Web Token):JWT是Token的一种具体实现方式,它遵循RFC 7519标准,定义了一种紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。JWT由头部(Header) 载荷(Payload) 签名(Signature)三个部分组成,通过这三个部分可以确保信息的完整性和发送者的身份。下面是它的一张介绍图,并附上一个 token 进行说明。

下面是举例说明一下JWT, 它以 "." 为分隔,分为三部分

String token  =
"eyJhbGciOiJIUzI1NiJ9." +
"eyJqdGkiOiI2ZDM1ZDBhMjUwN2U0MmIyYmU0ZDQyNmZiYzdjZjZlMSIsInN1YiI6IjE0MCIsImlzcyI6ImJhaUh1aSIsImlhdCI6MTcxODI5MTgwMiwiZXhwIjoxNzE4MjkxODEyfQ."
 +
"VQ0bPR56uUFclWq59XvPUfUISwWB-Ty7v0M07TTPxRU";

该如何使用这两个功能?

首先,我们先在后端项目中引入需要的依赖信息。

<!--jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

接着,我们在后端项目中,新建一个包,叫做 utils,译为 工具包。这个包,在之后的工作中,会非常常用。之后,在这个包中,新建一个JwtUtil 类,这个就是我们要进行开发的内容。代码如下:在这里,我只保留一种方法,就是根据数据库查询到的结果对象,作为加密内容。

package com.eh.lbh.utils;

import com.eh.lbh.pojo.SysUser;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * @ClassName JwtUtil
 * @Description 令牌静态方法
 * @Author liuBaiHui
 * @Date 2024/6/13 21:41
 * @Version 1.0
 */
@Slf4j
public class JwtUtil {

    // 设置有效期为3分钟, 以毫秒为单位
    public static final Long JWT_TTL = 60 * 3 * 1000L;

    // 设置密钥密文,注意长度必须大于等于6位
    public static final String SECRET_KEY = "baiHui";

    /*
     * @description: 根据 user对象 生成一个token
     * @return: java.lang.String
     * @author liuBaiHui
     * @date 2024/6/13-21:48
     */
    public static String getTokenByUser(SysUser sysUser) {

        // 1.设置签名使用HMAC SHA-256算法,它的特点是:签名和验证过程使用相同的密钥,即对称加密算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 2.设置密钥签名
        SecretKey secretKey = generalKey();

        // 3.配置并返回 JWT 构造器
        String token = Jwts.builder()
                // 载荷
                .setId(getUUID())   // 为 JWT 设置一个唯一标识
                .setIssuer("baiHui")  // 设置签发人
                .setIssuedAt(new Date())  //设置签发时间
                .setSubject(String.valueOf(sysUser.getId())) // 设置一个主题
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TTL)) // 设置过期时间为 3分钟过期

                // 签名
                .signWith(signatureAlgorithm, secretKey) // 使用HS256对称加密算法,第二个参数为 密钥
                .compact();
        return token;
    }


    /*
    * @description 生成密钥
    * @return: javax.crypto.SecretKey
    * @author liuBaiHui
    * @date 2024/6/27-22:00
    */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.SECRET_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /*
     * @description 生成一个UUID
     * @return: java.lang.String
     * @author liuBaiHui
     * @date 2024/6/13-22:46
     */
    public static String getUUID() {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return uuid;
    }


    /*
    * @description 解析token时,判断token是否过期
    * @return: io.jsonwebtoken.Claims
    * @author liuBaiHui
    * @date 2024/6/13-22:53
    */
    public static Claims parseJwt(String token) throws ExpiredJwtException {
        SecretKey secretKey = generalKey();
        Claims payload = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();

        // payload相关的数据信息其实就是JWT中间部分的内容信息
        return payload;
    }




    public static void main(String[] args) {
        String token  =
        "eyJhbGciOiJIUzI1NiJ9." +
        "eyJqdGkiOiI2ZDM1ZDBhMjUwN2U0MmIyYmU0ZDQyNmZiYzdjZjZlMSIsInN1YiI6IjE0MCIsImlzcyI6ImJhaUh1aSIsImlhdCI6MTcxODI5MTgwMiwiZXhwIjoxNzE4MjkxODEyfQ." +
        "VQ0bPR56uUFclWq59XvPUfUISwWB-Ty7v0M07TTPxRU";

        Claims payload = parseJwt(token);
        String subject = payload.getSubject();

        System.out.println(subject);
    }
}

之后,就来到我们的登录功能,把这个方法进行使用,代码如下:

@RestController
@RequestMapping("/api")
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/userLogin")
    public Result userLogin(@RequestBody SysUser sysUser){
        
        SysUser one = loginService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUserName, sysUser.getUserName()));
        if (!Objects.isNull(one)){

            // 1.如果登录成功,就生成一个 jwt
            String token = JwtUtil.getTokenByUser(one);
            return Result.ok(token);
        }
        return Result.failed();
    }
}

最后,就返回给了前端一个对象,里面包含了 token 值。

前端处理token

loginInfo () {
        this.$axios.post('/userLogin', {
        userName: this.userInfoForm.userName,
        userPassword: this.userInfoForm.userPassword
      }).then(resp => {
        if (resp && resp.data.code === 200) {

          // 解构赋值
          const  token = resp.data; 
          
          // 把后端返回的 token 存储到 localStorage
          this.$store.commit('login', token);
          
          this.$router.replace({ path: '/index' });
        }
      }).catch((error) =>{
        console.log(error);
      });
}

查看token

在上面的代码中, 这行代码 this.$store.commit('login', token); 的作用是,把返回的结果,提交到 store 中的 方法中,打开 /store/index.js 文件,代码如下:

export default new Vuex.Store({
    state: {
        token: window.localStorage.getItem('token') == null ? '': JSON.parse(window.localStorage.getItem('token')).token,
    }, 
		
   // 提交方法
    mutations: {
        login (state, token) {
            state.token = token;
            window.localStorage.setItem('token',JSON.stringify(state.token));
        },
    }
})

效果检验

到这里前期的工作就做完了,接下来,我们就使用登录功能,进行验证。登录成功,打开F12,可以看到,token 值是被正确存放的。下面我们给进行清空,然后演示直接访问 /index 。看它会不会报错。

同时运行前后端项目,访问 http://localhost:8080/index ,发现页面直接跳转到了 http://localhost:8080/login?redirect=%2Findex。

小结:以上就完成了后端传入一个状态值,前端根据状态值去访问需要认证授权之后,才能访问的页面。这里只能说是粗略的使用了 token。后面会再详细补充说明 token 的使用。今天就先到这里。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言