【自用】Vue+Spring Boot前后端分离架构,集成vuex实现登录+登录校验
大致流程
登录
- 前端输入用户名密码
- 后端校验用户名密码是否正确
- 生成token
- 后端在redis中保存token,
- 前端保存token
校验
- 前端请求时,在header中带上token
- 后端拦截器,从header中获取token并与redis中存储的进行校验
- 校验成功继续业务
- 校验失败返回登录页面
具体流程
登录时,当后端校验用户名与密码正确后,生成一个令牌token,并放入Redis中,键值对为:{token: UserLoginResp},其中UserLoginResp为后端返回给前端的用户登录态信息,包含用户的id,loginName,name,token(id,用户名,昵称,token),该键值对在Redis中的有效时间是24H。
UserController.java
@PostMapping("/login")
public JsonResult<UserLoginResp> login(@Valid @RequestBody UserLoginReq userLoginReq) {
// 密码加密存储,变为32bit的HEX字符串
userLoginReq.setPassword(DigestUtils
.md5DigestAsHex(
userLoginReq
.getPassword()
.getBytes()));
JsonResult<UserLoginResp> result = new JsonResult<>();
UserLoginResp userLoginResp = userService.login(userLoginReq);
// 生成单点登录token
String token = IdUtil.fastUUID();
// 存入redis,24h过期
redisTemplate.opsForValue().set(token,
JSONObject.toJSONString(userLoginResp),
3600 * 24,
TimeUnit.SECONDS);
// 返回给前端
userLoginResp.setToken(token);
result.setData(userLoginResp);
return result;
}
UserLoginResp.java
package cn.codersy.wiki.resp;
public class UserLoginResp {
private Long id;
private String loginName;
private String name;
private String token;
// 省略getter、setter
}
前端拿到后端返回的UserLoginResp后,需要进行存储,否则登录态刷新后即会失效,此功能需要集成vuex和sessionStorage
首先自己封装一下内置的sessionStroage(使得它的值不是object类型而是JSON字符串):
public/js/session-storage.js
SessionStorage = {
get: function (key) {
var v = sessionStorage.getItem(key);
if (v && typeof(v) !== "undefined" && v !== "undefined") {
return JSON.parse(v);
}
},
set: function (key, data) {
sessionStorage.setItem(key, JSON.stringify(data));
},
remove: function (key) {
sessionStorage.removeItem(key);
},
clearAll: function () {
sessionStorage.clear();
}
};
然后用vuex再包装一下sessionStorage,这样项目的其他文件就不用操作sessions,只需要操作vuex提供的方法即可。
sec/store/index.ts
import { createStore } from 'vuex'
// 整合sessionStorage
declare let SessionStorage: any;
const USER = "USER";
const store = createStore({
state: {
// 全局变量,保存用户登录状态,初始化从缓存取,取不到则为空
user: SessionStorage.get(USER) || {}
},
mutations: {
setUser(state, user) {
state.user = user;
// 存入缓存
SessionStorage.set(USER, user);
}
},
actions: {
},
modules: {
}
});
export default store;
准备好以上,编写前端登录
the-header.vue
<template>
<a-layout-header class="header">
<div class="logo"/>
<a-menu
theme="dark"
mode="horizontal"
v-model:selectedKeys="selectedKeys1"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">
<router-link to="/">首页</router-link>
</a-menu-item>
<a-menu-item key="2">
<router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
<a-menu-item key="3">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="4">
<router-link to="/admin/user">用户管理</router-link>
</a-menu-item>
<a-menu-item key="5">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a class="login-menu" @click="showLoginModal" v-if="!user.id">
<span>登录</span>
</a>
<a class="login-menu" v-if="!!user.id">
<a-popconfirm
title="确认退出登录?"
ok-text="是"
cancel-text="否"
@confirm="logout"
>
<span>退出登录</span>
</a-popconfirm>
</a>
<a class="login-menu" v-if="!!user.id">
<span>欢迎您,{{ user.loginName }}</span>
</a>
</a-menu>
<!--登录模态框-->
<a-modal
title="用户登录"
ok-text="确认"
cancel-text="取消"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<!--表单-->
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
<a-form-item label="用户名">
<a-input v-model:value="loginUser.loginName"/>
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password"/>
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import {computed, defineComponent, ref} from 'vue';
import Axios from "axios";
import {message} from "ant-design-vue";
import store from "@/store";
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'the-header',
setup() {
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const loginUser = ref();
loginUser.value = {};
// 记录已登录的用户
const user = computed(() => store.state.user);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 点击登录模态框的确认按钮
const login = () => {
loginModalLoading.value = true;
// 密码在传输时即进行加密
loginUser.value.password = hexMd5(loginUser.value.password + KEY);
Axios.post("/user/login", loginUser.value)
.then((res) => {
loginModalLoading.value = false;
if (res.data.success) {
loginModalVisible.value = false;
loginUser.value = {};
// 触发setUser方法,存储登录态
store.commit('setUser', res.data.data);
message.success('登录成功!', 1.5);
} else {
message.error(res.data.message, 1.5);
}
});
};
const logout = () => {
Axios.get("/user/logout/" + user.value.token)
.then((res) => {
if (res.data.success) {
store.commit('setUser', {});
} else {
message.error(res.data.message, 1.5);
}
});
};
return {
loginUser,
user,
loginModalVisible,
loginModalLoading,
showLoginModal,
login,
logout
}
}
});
</script>
<style scoped>
.logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 50px;
background: rgba(255, 255, 255, 0.3);
}
.login-menu {
float: right;
color: white;
width: 120px;
}
</style>
以上只是实现了登录功能,为了防止手动通过url绕开界面发送不合法请求,我们需要进行登录校验。
后端增加登录校验
- 前端在请求头中加入token
- 后端从请求头中获取token进行校验。
编写登录拦截器:LoginInterceptor.java
package cn.codersy.wiki.interceptor;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登录拦截器
* @author TenMoons
* @date 2021-08-13 10:04
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginInterceptor.class);
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 前后端分离的架构,前端会发送一个OPTIONS做预检,该请求不校验
if (request.getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
// 从header中获取token参数
String token = request.getHeader("token");
LOGGER.info("登录校验开始,token: {}", token);
if (StrUtil.isBlank(token)) {
LOGGER.info("token为空,请求被拦截");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
Object o = redisTemplate.opsForValue().get(token);
if (ObjectUtil.isEmpty(o)) {
LOGGER.warn("token无效,请求被拦截");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
LOGGER.info("登录成功: {}", o);
return true;
}
}
}
}
前端在请求中加入token,此处也需要用到上面的vuex。
使用axios拦截器,同时拦截请求和响应,并打印出日志,在拦截请求时,向header中加入sessionStorage中的token
main.ts
// 使用axios拦截器拦截请求和响应并打印日志
axios.interceptors.request.use(function (config) {
console.log('请求参数:', config);
// 向header中加入token
const token = store.state.user.token;
if (Tool.isNotEmpty(token)) {
config.headers.token = token;
}
return config;
}, err =>{
return Promise.reject(err);
});
axios.interceptors.response.use(function (res) {
console.log('返回结果:', res);
return res;
}, err => {
console.log('返回错误:', err);
return Promise.reject(err)
});
前端界面登录校验
- 未登录时,管理菜单要隐藏
- 对路由做判断,防止用户通过手动url访问管理界面
router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/home.vue'
import About from '../views/about.vue'
import AdminEbook from '../views/admin/admin-ebook.vue'
import AdminCategory from '../views/admin/admin-category.vue'
import AdminDoc from '../views/admin/admin-doc.vue'
import Doc from '../views/doc.vue'
import AdminUser from '../views/admin/admin-user.vue'
import store from "@/store";
import {Tool} from "@/utils/tool";
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/about.vue')
},
{
path: '/admin/ebook',
name: 'AdminEbook',
component: AdminEbook,
meta: {
loginRequire: true
}
},
{
path: '/admin/category',
name: 'AdminCategory',
component: AdminCategory,
meta: {
loginRequire: true
}
},
{
path: '/admin/doc',
name: 'AdminDoc',
component: AdminDoc,
meta: {
loginRequire: true
}
},
{
path: '/doc',
name: 'Doc',
component: Doc,
meta: {
loginRequire: true
}
},
{
path: '/admin/user',
name: 'AdminUser',
component: AdminUser,
meta: {
loginRequire: true
}
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
// 路由登录拦截
router.beforeEach((to, from, next) => {
// 判断目标路由是否需要拦截
if (to.matched.some((item) => {
console.log(item, "是否需要登录校验:", item.meta.loginRequire);
return item.meta.loginRequire;
})) {
const loginUser = store.state.user;
if (Tool.isEmpty(loginUser)) {
console.log('未登录!');
// 跳转回首页
next("/");
} else {
// 正常跳转目标路由
next();
}
}
});
export default router

浙公网安备 33010602011771号