兄弟们,继昨天的代码之后,继续完成最后的用户模块开发,
昨天已经完成了关于用户的信息编辑页面这些,今天再完善一下,
从后端这边开始吧,做一个拦截器,对用户做身份校验,
拦截器
这边先解释一下吧:
那就再用面试题来细说:
拦截器相关面试题:过滤器与拦截器有什么区别?
答:
-
一:运行的顺序不同
- 过滤器是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',
}
});
}
此时访问页面,就能得到用户名等信息了
然后就算是成功实现了请求的拦截与用户身份的检验