项目介绍

本文章讲解的是基于 Vue3.x 和 Springboot 的前后端分离项目的登录拦截示例。运用到 vuex 和 Springboot 的拦截器。前端采用路由守卫,后端采用注册拦截器。安全验证采用 JWT 的 token 令牌。


效果展示

登录成功

登录成功后弹出提示信息,并自动跳转到用户页面。

登录失败

登陆验证失败后会弹出提示信息。

访问无效 URL

访问无效 URL 时,会自动跳转到 404 页面。


环境配置

数据库

数据库:MySql8.0.28

开发工具:Navicat Premium 15

依赖导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<properties>
<java.version>1.8</java.version>
<mysql.version>8.0.28</mysql.version>
<druid.version>1.2.9</druid.version>
<mybatis.version>2.1.3</mybatis.version>
<jwt.version>3.10.3</jwt.version>
<hutool.version>5.7.22</hutool.version>
<skipTests>true</skipTests>
</properties>

<dependencies>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>

<!-- mysql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>

<!-- 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>

<!-- springboot 整合 jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- springboot test 模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>

<!-- hu tool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>

项目分析

前端实现

前端路由守卫流程图

前端请求资源流程图

路由守卫代码实现

文件路径:router/index.js

在需要拦截的页面的路由处加上 requireAuth: true

1
2
3
4
5
6
7
8
9
{
path: '/user',
name: 'user',
component: () => import('../views/UserInfoView.vue'),
meta: {
showNav: false,
requireAuth: true
}
}

文件路径:main.js

在页面跳转前,先判断要跳转的页面是否为拦截页面,若不是拦截页面则页面放行;若是拦截页面则判断 vuex 是否存在 token,若不存在 token 则跳转”/404“,由于“/404”为无效 URL,所以会自动跳转到指定页面;若存在 token 则页面放行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth) {
if (store.state.user.token) {
next()
} else {
next({
path: "404"
})
}
} else {
next()
}
}
)
拦截无效 URL 代码实现

文件路径:router/index.js

遇到无效 URL 跳转到指定页面。

注意:此拦截代码只对 vue3 以上有效,应当把这段代码加在路由最后。

1
2
3
4
5
{
name: 'error',
path: '/:pathMatch(.*)',
component: () => import('../views/NotFoundView.vue')
}
封装请求代码实现

文件路径:api/request.js

判断 sessionStorage 是否存在 token,存在则携带 token 发送请求。

判断服务器返回的状态码是否为 401,是则跳转到登录页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import axios from 'axios'
import {HOST} from "../config/config";

const request = axios.create({
baseURL: HOST,
// 超时时间 5 秒
timeout: 5000
})

// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
// Context-Type 响应头
config.headers['Content-Type'] = 'application/json;charset=utf-8';

let token = sessionStorage.getItem("user") ? sessionStorage.getItem("user") : null;
if (token)
{
// 设置请求头
config.headers['token'] = token;
}
return config
}, error => {
return Promise.reject(error)
});

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}

// 验证不通过时给出提示
if (res.code === 401)
{
alert(res.message);
window.location.replace("/login");
}

return res;
},
error => {
// for debug
console.log('err' + error)
return Promise.reject(error)
}
)


export default request
Vuex 代码实现

文件路径:store/index.js

在服务器验证成功后,调用 login 方法,取出响应数据里的 token 保存到 state 和 sessionStorage 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { createStore } from 'vuex'

export default createStore({
state: {
user: {
token: window.sessionStorage.getItem('user') == null ? null : window.sessionStorage.getItem('user')
}
},
getters: {
},
mutations: {
/**
* 登录操作,把token保存到user.token,再保存token到sessionStorage。
*
* @param state
* @param token
*/
login (state, token) {
state.user.token = token;
window.sessionStorage.setItem('user', token);
},

/**
* 退出操作,把user.token的值删除,再移除sessionStorage的token。
*
* @param state
*/
exit (state) {
state.user.token = null;
window.sessionStorage.removeItem('user');
}
},
actions: {
},
modules: {
}
})

后端实现

后端拦截器流程图

拦截器代码实现

文件路径:config/interceptor/JwtInterceptor.java

自定义业务和系统异常,若是 token 不合法,则将状态码和错误信息返回给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class JwtInterceptor implements HandlerInterceptor
{
@Resource
private UserService userService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
String token = request.getHeader(Consts.TOKEN);
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod))
{
return true;
}

// 执行认证
if (StrUtil.isBlank(token)) {
throw new BusinessException(Code.NOT_LOGIN_ERR, "用户未登录");
}

// 获取 token 中的 user id
String userId = null;
try
{
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new BusinessException(Code.NOT_LOGIN_ERR, "Token令牌验证失败");
}

// 根据 token 中的 userId 查询数据库
User user = userService.getUserByAccount(userId);
if (user == null)
{
throw new BusinessException(Code.NOT_LOGIN_ERR, "用户不存在,请重新登录");
}

// 用户密码加签验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
}
catch (JWTVerificationException e)
{
throw new SystemException(Code.NOT_LOGIN_ERR, "登录超时");
}
return true;
}
}
注册拦截器

文件路径:config/WebMvcConfig.java

每个请求服务器资源的请求都将先通过拦截器。这个文件就是注册所有拦截器的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Configuration
public class WebMvcConfig implements WebMvcConfigurer
{
// 解决跨越问题
@Override
public void addCorsMappings(CorsRegistry registry)
{
//添加映射路径
registry.addMapping("/**")
//是否发送Cookie
.allowCredentials(true)
//设置放行哪些原始域
.allowedOriginPatterns("*")
//放行哪些请求方式
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
//.allowedMethods("*") //或者放行全部
//放行哪些原始请求头部信息
.allowedHeaders("*")
//暴露哪些原始请求头部信息
.exposedHeaders("*");
}

// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/users/login", "/users/register");
}

// 拦截器bean
@Bean
public JwtInterceptor jwtInterceptor()
{
return new JwtInterceptor();
}
}
生成 token 令牌工具类

文件路径:utils/TokenUtils.java

通过 JTW 生成 token,封装成工具类。

当调用登录业务时,用 TokenUtils.getToken(user.getAccount(), user.getPassword()); 生成 token 令牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
public class TokenUtils
{
private static UserService STATIC_USER_SERVICE;

@Resource
private UserService userService;

@PostConstruct
public void setStaticUserService()
{
STATIC_USER_SERVICE = userService;
}

/**
* 生成token令牌
*
* @param userId 用户id
* @param sign
* @return
*/
public static String getToken(String userId, String sign)
{
// 将 userId 保存到 token 里面
return JWT.create().withAudience(userId)
// 设置 token 过期时间
.withExpiresAt(DateUtil.offsetMinute(new Date(), Consts.TOKEN_TIME))
// 把 sign 作为 token 密钥
.sign(Algorithm.HMAC256(sign));
}
}

完整的登录流程图


未解决问题

输入有效但被拦截的 URL 时,地址栏会直接重定向到”/404“,而不是显示用户输入的 URL。因为跳转有效 URL 时,要经过路由守卫,而路由守卫跳转都会重定向 URL。



📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!