超详细的前后端实战项目(Spring系列加上vue3)前后端篇(四)(一步步实现+源码)

兄弟们,继昨天的代码之后,继续完成最后的用户模块开发,

昨天已经完成了关于用户的信息编辑页面这些,今天再完善一下,

从后端这边开始吧,做一个拦截器,对用户做身份校验,

拦截器

这边先解释一下吧:

那就再用面试题来细说:

拦截器相关面试题:过滤器与拦截器有什么区别?

答:

  • 一:运行的顺序不同

    • 过滤器是servlet容器接收到请求之后,在servlet被调用之前运行的
    • 拦截器则是在servlet被调用之后,但是在响应被发送到客户端之前运行的
  • 二:配置方式不同

    • 过滤器是在web.xml配置
    • 拦截器是在spring的配置文件中配置,或者基于注解进行配置
  • 三:依赖关系

    • Filter依赖于Servlet容器,而Interceptor不依赖于Servlet容器
  • 四:能力方面

    • Filter在过滤器中只能对request和response进行操作
    • Interceptor可以对request,response,handler,modelAndView,exception,相当于Interceptor多了对SpringMvc生态下组件的一个操作能力
  • 接口规范不同

    • 过滤器需要实现Filter接口,拦截器需要实现HandlerInterceptor接口
  • 拦截范围不同

    • 过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源

OK,来做一个拦截器:

创建一个interceptors包,并创建LoginInterceptor类

拦截器的类要继承

复制代码
HandlerInterceptor

注:

首先,创建拦截器类时,需要实现HandlerInterceptor接口。这个接口定义了三个方法:preHandle()postHandle()afterCompletion()preHandle()方法是在控制器方法执行之前调用的,它返回一个布尔值,表示是否继续执行后续操作;postHandle()方法是在控制器方法执行之后,视图解析之前调用的,可以用来对响应进行进一步的处理;afterCompletion()方法是在整个请求处理完成之后调用的,通常用于资源的清理工作。

其次,除了实现HandlerInterceptor接口,还可以选择继承HandlerInterceptorAdapter类来简化拦截器的实现。HandlerInterceptorAdapter提供了HandlerInterceptor接口的空实现,使得开发者只需要重写自己关心的方法即可。

最后,为了让拦截器生效,还需要在Spring配置文件中进行相应的配置。这通常涉及到定义一个配置类,实现WebMvcConfigurer接口,并重写addInterceptors()方法。在这个方法中,可以使用addPathPatterns()来指定拦截路径,使用excludePathPatterns()来指定排除的路径。

在实际开发中,根据需求选择合适的拦截器类型是非常重要的。例如,如果需要在Controller层进行权限验证,那么使用HandlerInterceptor接口是合适的;如果只是对请求进行简单的预处理和后处理,那么可以考虑使用WebRequestInterceptor接口。选择合适的拦截器类型可以确保代码的整洁性和可维护性。

下面给出代码

java 复制代码
import org.example.cetidenet.utils.JwtUtil;
import org.example.cetidenet.utils.ThreadLocalUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

            return false;
}

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

这里继承HandlerInterceptor之后按快捷键Ctrl+o,然后继承三种方法就行了,这里常用的是第一种preHandle,在请求到Controller层之前会被preHandle拦截并处理,如果返回不为true,则会被拦截。

创建了拦截器之后还要注册拦截器。

这里就在之前创建的config包下进行操作

在之前加载Swagger资源的类下加上代码:

java 复制代码
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //登录接口和注册接口不拦截
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login","/user/register","/doc.html#/home");
    }

这里有个小坑(之前单独创建了一个类继承的是WebMvcConfigurer,结果拦截器缺没有起作用,如果是这样,就继承WebMvcConfigueationSupport)

添加了拦截的的路径和放行的路径之后就可以进行检验了

OK,这里使用拦截器之后,登录接口可以正常使用,

而使用更新接口则返回401没有权限。那也就成功了。

JWT令牌:

那么使用了拦截器之后拦截了除了登录注册之外的其他的接口,那该如何才能让其使用呢,必然需要验证身份,这里可以使用JWT令牌,

也就是在用户登录完成之后发放JWT令牌,然后反馈给前端,前端携带其到后端,拦截器对其进行校验,如果带有JWT令牌就放行。

后端引入JWT令牌依赖

java 复制代码
       <!--java-jwt坐标-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>
java 复制代码
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {

    private static final String KEY = "CeTide";
	
	//接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 ))
                .sign(Algorithm.HMAC256(KEY));
    }

	//接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }

}

然后在登录接口这里添加代码:

java 复制代码
    public Result<String> login(UserLoginDTO userLoginDTO) {
        //先获取DTO中的账号面膜
        String username = userLoginDTO.getUserName();
        String password = userLoginDTO.getPassword();

        //先查询数据库中是否有这号人物
        User user = userMapper.findByUsername(username);
        //判断是否存在
        if(user == null){
            return Result.error("该用户不存在");
        }
        String salt = password + "ceTide";
        String pwd = DigestUtils.md5Hex(salt.getBytes()).toUpperCase();
        //存在,判断密码是否正确
        if(!user.getPassword().equals(pwd)){
            return Result.error("用户密码错误");
        }
//        boolean isLog = logService.addUserLogin(user);
//        if(!isLog){
//            return Result.error("用户登录日志记录失败");
//        }
        //登录成功
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", user.getId());
        claims.put("username", user.getUserName());
        String token = JwtUtil.genToken(claims);
        //存在且密码正确
        return Result.success(token);
    }

将用户的id和username放入到token中(虽然但是,尽量不要把密码放进去)

然后在拦截器这里进行校验:

java 复制代码
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //令牌验证
        System.out.println("开始验证");
        String token = request.getHeader("Authorization");
        //验证令牌
        try{
            Map<String,Object> claims = JwtUtil.parseToken(token);

            //放行
            return true;
        }catch (Exception e) {
            //反馈响应码401
            response.setStatus(401);
            return false;
        }
    }

这里如果能够解析就代表拥有JWT令牌,那也就可以放行了

现在回到前端这里:

先看一下修改后的前端代码

html 复制代码
<template>
  <div id="userCenter">
    <a-layout style="height: 100vh">
      <a-layout-header>
        <a-page-header title="用户中心" subtitle="CeTide网" @click="returnPage"/>
        <div class="header">
          <div class="user-introduce">
            <img
              :src="userNum.userImg"
              width="70px"
              height="70px"
              class="user-img"
            />
            <div>
              <div class="personal-introduce">
                <div style="margin-left: 10px">
                  <span class="name">{{ userNum.userName }}</span>
                  <span class="sex-icon"></span>
                </div>
                <a-space class="btn">
                  <a-button type="dashed" shape="round" @click="handleClick"
                    ><icon-pen-fill />编辑资料</a-button
                  >
                  <a-button type="dashed" shape="round"
                    ><icon-settings />设置</a-button
                  >
                  <a-button type="dashed" shape="round"
                    ><icon-list />文章管理</a-button
                  >
                </a-space>
              </div>
            </div>
          </div>
          <div class="user-follow">
            {{ userNum.attention }}
            <icon-star />
            <span class="follow-num">关注</span>
            {{ userNum.fans }}
            <icon-heart />
            <span class="follow-num">粉丝</span>
            {{ userNum.article }}
            <icon-select-all />
            <span class="follow-num">文章</span>
          </div>
          <div class="user-follow">个人简介:{{ userSelfIntroduce }}</div>
        </div>
      </a-layout-header>
      <a-layout style="margin: 24px 180px">
        <a-layout-sider :resize-directions="['right']">
          <a-layout
            style="
              height: 100%;
              text-align: left;
              padding-left: 20px;
              background-color: #c4c4c4;
            "
          >
            <a-layout-content style="height: 20%">
              <h3>CeTide等级</h3>
            </a-layout-content>
            <a-layout-content style="height: 20%">
              <h3>个人成就</h3>
            </a-layout-content>
            <a-layout-content style="height: 60%">
              <h3>个人动态</h3>
            </a-layout-content>
          </a-layout>
        </a-layout-sider>
        <a-layout-content class="content">
          <h3>用户中心</h3>
        </a-layout-content>
      </a-layout>
      <a-layout-footer>Footer</a-layout-footer>
    </a-layout>

    <!-- 编辑个人信息的抽屉 -->
    <a-drawer
      :visible="visible"
      :width="500"
      @ok="handleOk"
      @cancel="handleCancel"
      unmountOnClose
    >
      <template #title> 编辑个人信息 </template>
      <div :style="{ marginBottom: '20px' }">
        <div >
          <img :src="userNum.userImg" width="70px" height="70px" class="user-img"/>
      <a-button type="primary" @click="handleNestedClick" style="float: right;margin-top: 20px"
        >更换头像</a-button
      >
        </div>
            <a-divider />
        <div> 用户名:<a-input :style="{width:'320px'}" allow-clear v-model="userNum.userName"/></div>
            <a-divider />
        <div> 性别:<a-input :style="{width:'320px'}" v-model="numSex" /></div>
            <a-divider />
        <div> 电话:<a-input :style="{width:'320px'}" v-model="userNum.phone"/></div>
            <a-divider />
        <div> 生日:<a-input :style="{width:'320px'}" v-model="userNum.birthday" /></div>
            <a-divider />
        <div> 城市:<a-input :style="{width:'320px'}" v-model="userNum.county" /></div>
            <a-divider />
        <div> 住址:<a-input :style="{width:'320px'}" v-model="userNum.address" /></div>
            <a-divider />
        <div> CeTide网ID:<a-input :style="{width:'320px'}" v-model="userNum.id" disabled/></div>
            <a-divider />
        <div> 个人简介: <a-textarea v-model="userSelfIntroduce" allow-clear style="height: 100px"/></div>
      </div>

    </a-drawer>
    <a-drawer
      :visible="nestedVisible"
      @ok="handleNestedOk"
      @cancel="handleNestedCancel"
      unmountOnClose
    >


      <template #title> 文件操作 </template>
            <a-space direction="vertical" :style="{ width: '100%' }" class="picture">
    <a-upload
      action="/"
      :fileList="file ? [file] : []"
      :show-file-list="false"
      @change="onChange"
      @progress="onProgress"
    >
      <template #upload-button>
        <div
          :class="`arco-upload-list-item${
            file && file.status === 'error' ? ' arco-upload-list-item-error' : ''
          }`"
        >
          <div
            class="arco-upload-list-picture custom-upload-avatar"
            v-if="file && file.url"
          >
            <img :src="file.url" />
            <div class="arco-upload-list-picture-mask">
              <IconEdit />
            </div>
            <a-progress
              v-if="file.status === 'uploading' && file.percent < 100"
              :percent="file.percent"
              type="circle"
              size="mini"
              :style="{
                position: 'absolute',
                left: '50%',
                top: '50%',
                transform: 'translateX(-50%) translateY(-50%)',
              }"
            />
          </div>
          <div class="arco-upload-picture-card" v-else>
            <div class="arco-upload-picture-card-text">
              <IconPlus />
              <div style="margin-top: 10px; font-weight: 600">Upload</div>
            </div>
          </div>
        </div>
      </template>
    </a-upload>
  </a-space>
    </a-drawer>
  </div>
</template>

<script setup>
import {userGetInfo} from '../api/user';
import { ref } from "vue";
import avatar from "../assets/userbg.png";
import { useRouter } from "vue-router";
const router = useRouter();
const userInfoList= async()=>{
  let result = await userGetInfo();
  userNum.value = result.data;
}
userInfoList();
const userSelfIntroduce = ref("这个人很懒,什么都没有留下");
const userNum = ref({
  id: "007",
  county: "四川",
  address: "成都",
  phone: "12345678910",
  birthday: "1999-09-09",
  gender: "女",
  email: "123@qq.com",
  userImg: avatar,
  userName: "我是小丑",
  attention: 0,
  fans: 0,
  article: 0,
});
const numSex = ref(userNum.value.gender === 'F' ? '男' : '女');
//抽屉显示隐藏
const visible = ref(false);
const nestedVisible = ref(false);

const handleClick = () => {
  visible.value = true;
};
const handleOk = () => {
  visible.value = false;
};
const handleCancel = () => {
  visible.value = false;
};
const handleNestedClick = () => {
  nestedVisible.value = true;
};
const handleNestedOk = () => {
  nestedVisible.value = false;
};
const handleNestedCancel = () => {
  nestedVisible.value = false;
};

//返回方法
const returnPage = () =>{
  router.push('/')
}

//
</script>

<style lang="scss" scoped>
#userCenter {
  background: url("../assets/image.png") no-repeat bottom center / 100% 100%;
}
.header {
  font-family: "Satisfy", cursive;
  margin: 2% 100px;
  height: 20vh;
  background: url("../assets/back.png") no-repeat center / 100% 100%;
  position: relative;
}

.personal-introduce {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  margin-top: 10px;
  text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.31);
  .name {
    line-height: 29px;
    font-size: 26px;
  }
  .sex-icon {
    display: inline-block;
    width: 16px;
    height: 16px;
    margin: 0px 8px;
    margin-bottom: 4px;
    background: url(../assets/user-images/sex-icon.png) no-repeat center;
    background-size: contain;
    border-radius: 50%;
  }
  .level-icon {
    display: inline-block;
    width: 16px;
    height: 16px;
    margin-bottom: 4px;
    background: url(../assets/user-images/leval-icon.png) no-repeat center;
    background-size: contain;
    border-radius: 50%;
  }
}

.user-introduce {
  display: flex;
  justify-items: left;
  padding: 10px;
}
.user-img {
  border-radius: 50%;
  margin-left: 20px;
}
.user-follow {
  margin-left: 30px;
  font-size: 16px;
  display: flex;
  justify-items: left;
}
.follow-num {
  font-size: 16px;
  padding-right: 20px;
}
.content {
  margin-left: 70px;
  background-color: #c4c4c4;
}
.btn {
  position: absolute;
  right: 40px;
}

</style>

在登录之后获取到了JWT值

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjE1LCJ1c2VybmFtZSI6IueUqOaIt1VTRERBRCJ9LCJleHAiOjE3MTY4MDQ4ODB9._9GDxuux5wmoV5CsZCd0QI3wByESKWGGZCKmDaZVlbc"

这样一大串字,然后前端将其放置在请求头的Authorization属性中

举个例子

java 复制代码
export const userGetInfo= () =>{

    return request.get('/user/getInfo',{

        headers : {

            'Authorization' : 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjE1LCJ1c2VybmFtZSI6IueUqOaIt1VTRERBRCJ9LCJleHAiOjE3MTY4MDQ4ODB9._9GDxuux5wmoV5CsZCd0QI3wByESKWGGZCKmDaZVlbc',

        }

    });

}

此时访问页面,就能得到用户名等信息了

然后就算是成功实现了请求的拦截与用户身份的检验

相关推荐
Penge6665 小时前
Go 接口编译期断言
后端
我是一颗柠檬5 小时前
【MySQL全面教学】MySQL面试高频考点汇总Day15(2026年)
数据库·后端·mysql·面试
橙淮6 小时前
并发编程(六)
java·jvm
拽着尾巴的鱼儿6 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影6 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
Ceelog6 小时前
久坐党自救指南:屏幕前 8 小时,身体到底在经历什么
前端·后端
EntyIU7 小时前
JVM内存与GC笔记
java·jvm·笔记
XS0301067 小时前
并发编程 六
java·后端
yaoxin5211237 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道7 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试