这篇主要介绍Springboot集成JWT生成Token,前端调用后端接口时加层验证的介绍。
大致流程是,后端引入JWT,根据用户登录时传给后端的用户信息生成Token,然后再返回给前端,前端在拿到这个Token后,可以存到浏览器缓存(localstorage)或者cookie中,在后续前端调用后端接口时,把token放在请求头里传给后端,后端会在调用接口前,在JWT拦截器拦截请求,拦截器主要验证Token是否过期,以及是否合法,校验通过后再去调用接口,做其他业务请求。
后端要做的事情:
1.pom.xml引入JWT依赖。
<!-- 引入JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
2.写JWT实体类,主要有生成token,解析token,获取用户信息
package com.example.bootdemo.helper;
import cn.hutool.core.util.StrUtil;
import com.example.bootdemo.vo.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
//签名私钥
private static final String key = "admin";
//签名失效时间 一分钟
private static final long failureTime = 60000;
/**
* 设置认证token
*
* @param id 用户登录ID
* @param subject 用户登录名
* @param map 其他私有数据
* @return
*/
public static String createJwt(String id, String subject, Map<String, Object> map) {
//1、设置失效时间啊
long now = System.currentTimeMillis(); //毫秒
long exp = now + failureTime;
//2、创建JwtBuilder
JwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(subject)
.setIssuedAt(new Date())
//设置签名防止篡改
.signWith(SignatureAlgorithm.HS256, key);
//3、根据map设置claims
for (Map.Entry<String, Object> entry : map.entrySet()) {
jwtBuilder.claim(entry.getKey(), entry.getValue());
}
jwtBuilder.setExpiration(new Date(exp));
//4、创建token
String token = jwtBuilder.compact();
return token;
}
/**
* 获取用户登录信息
*
* @return User
*/
public static UserInfo getCurrentUser(String token) {
if (StrUtil.isBlank(token)) {
return null;
}
//验证token
Claims map = parseJwt(token);
System.out.println("map:" + map);
if (map == null) {
return null;
}
UserInfo user = new UserInfo();
user.setUserID(Integer.parseInt((map.getId())));
user.setPhoneNumber((String) map.get("phoneNumber"));
user.setUsername((String) map.get("sub"));
System.out.println("user:" + user);
return user;
}
/**
* 解析token
*
* @param token
* @return
*/
public static Claims parseJwt(String token) {
try {
//截取token头
String nowToken = token.substring(7);
System.out.println("token:" + token);
System.out.println("nowToken:" + nowToken);
return Jwts.parser().setSigningKey(key).parseClaimsJws(nowToken).getBody();
} catch (Exception e) {
// 在这里记录错误或执行其他错误处理逻辑
System.out.println("error:" + e.getMessage());
return null;
}
}
}
3.编写拦截器,主要逻辑是,拦截前端请求,从请求头中获取token,然后对token进行验证,验证通过则进行下一步。
package com.example.bootdemo.helper;
import cn.hutool.core.util.StrUtil;
import com.example.bootdemo.vo.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("路径 '{}' 已被拦截",request.getRequestURI());
//1.拦截controller方法,handler还可能是RequestResourceHandler类型,放行
if(!(handler instanceof HandlerMethod))
return true;
//获取token
String token = request.getHeader("Authorization"); //请求头中获取token
System.out.println("前端token传:"+ token);
String requestURI = request.getRequestURI(); //请求URI
log.info("================request start ===============");
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("request token:{}",token);
log.info("================request end ================");
if (StrUtil.isBlank(token))
token = request.getParameter("token"); //请求头中没有,从请求参数中获取
System.out.println("拦截器:"+ token);
//判断token是否为空
if (StrUtil.isBlank(token)) {
log.error("获取当前登录的token失败,token:{}", token);
return false;
}
//登陆验证,并获取user登录基本信息
UserInfo currentUser = JwtUtils.getCurrentUser(token);
if(currentUser == null){
log.error("验证失败,token:{}", token);
return false;
}
//将用户信息存入ThreadLocal中
UserThreadLocal.setThreadLocal(currentUser);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//删除用户信息,防止内存泄漏
UserThreadLocal.removeThreadLocal();
}
}
4.配置拦截信息
package com.example.bootdemo.helper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加拦截器,并指定需要拦截的路径
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/login", "/error", "/swagger**/**", "/v2/api-docs", "/webjars/**"); // 排除不需要拦截的路径
}
// 其他配置...
}
ok,后端关于JWT的准备工作做好之后,接下来就是写个login接口,让前端调用,登录接口的逻辑大体是,在判断登陆信息正确后,调用生成token的方法,返回给前端。(当然,我这不全也不太规范,真正的登录的jwt还有很多nb的集成。仅供参考)
//获取token
@PostMapping("/login")
public String login(@RequestBody Map<String, String> loginMap) {
String username = loginMap.get("username");
String password = loginMap.get("password");
//获取用户数据
UserInfo userInfo = loginService.loginData(username, password);
if (userInfo == null) {
return "";
} else {
Map<String, Object> map = new HashMap<>();
map.put("password", userInfo.getPassword());
map.put("phoneNumber", userInfo.getPhoneNumber());
return JwtUtils.createJwt(String.valueOf(userInfo.getUserID()), username, map);
}
}
写一个接口,验证token验证效果。我这个接口地址,会被拦截做验证再调用接口。如果token过期,或者验证不通过,接口不会调用。(单纯为了验证写的接口,忽略接口业务..)
//获取用户信息
@PostMapping("/getUserData")
public UserInfo getUserData(@RequestHeader("Authorization") String token) {
return JwtUtils.getCurrentUser(token);
}
至此,后端的事情做完了。
前端要做的就是用户登录接口后,拿到token,存起来以备后续接口使用
登录页面:
<template>
<h1>ZWQ-INT</h1>
<form @submit.prevent="submitForm">
<div class="form-group">
<label class="form-label" for="username">用户名:</label>
<input
class="form-input"
type="text"
id="username"
v-model="form.username"
required
/>
</div>
<div class="form-group">
<label class="form-label" for="password">密码:</label>
<input
class="form-input"
type="password"
id="password"
v-model="form.password"
required
/>
</div>
<button class="form-button" type="submit">登录</button>
<button class="form-button" type="reset" @click="resetForm">重置</button>
</form>
</template>
<script>
import { reactive } from "vue";
import axios from "axios";
import { useRouter } from "vue-router";
export default {
setup() {
const router = useRouter();
const form = reactive({
username: "",
password: "",
//http://localhost:8082/zwq/getIsLogin
});
//后端接口地址
const backendUrl = "http://localhost:8082/zwq";
//点击登录
const submitForm = async () => {
if (form.username.trim() && form.password.trim()) {
try {
// 注意:GET请求通常通过查询参数传递数据,而不是请求体
// 所以我们将参数附加到URL上
// const response = await axios.get(
// `${backendUrl}/getIsLogin?username=${form.username}&password=${form.password}`
// );
// console.log(response);
//获取token
const response = await axios.post(`${backendUrl}/login`, {
username: form.username.trim(),
password: form.password.trim(),
});
console.log(response);
// 假设后端返回了一个包含登录状态的对象,例如 { isLogin: true }
//获取token
if (response.data != null && response.data != "") {
localStorage.setItem("access-admin", response.data);
alert("登陆成功");
await router.push("/index");
} else {
alert("登陆失败");
}
//获取用户信息
} catch (error) {
alert("请求失败");
}
}
};
//点击重置
const resetForm = () => {
debugger;
form.password = "";
form.username = "";
};
return {
form,
submitForm,
resetForm,
};
},
};
</script>
<style scoped>
.form-group {
display: flex;
align-items: center; /* 垂直居中 */
margin-bottom: 10px; /* 每个表单项之间的间距 */
}
.form-label {
flex: 1; /* 占据剩余空间 */
text-align: right; /* 文字右对齐 */
padding-right: 10px; /* 与输入框之间的间距 */
}
.form-input {
flex: content; /* 占据更多空间 */
max-width: 10%;
margin-right: 45%;
}
.form-button {
margin-right: 1%;
margin-top: 10px; /* 与上面表单项的间距 */
}
</style>
**登陆成功后,跳转主页面(为了测试token效果,和验证token的有效期,代码也是及其不规范的,大佬勿喷。┗( T﹏T )┛)
<template>
<div v-if="userData">
欢迎回来:{{ userData.username }}
<h1>这是首页</h1>
</div>
<div v-else>加载中...</div>
</template>
<script>
import axios from "axios";
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
export default {
setup() {
const router = useRouter();
//后端接口地址
const backendUrl = "http://localhost:8082/zwq";
const userData = ref(null);
//钩子
onMounted(async () => {
try {
const token = localStorage.getItem("access-admin");
if (!token) {
// 处理没有 token 的情况
return;
}
const response = await axios({
method: "post",
url: `${backendUrl}/getUserData`,
headers: {
Authorization: `Bearer ${token}`,
},
});
userData.value = response.data; // 更新响应式引用的值
if (response.data === "") {
alert("token验证失败,请重新登录!");
await router.push("/");
}
} catch (error) {
console.error("Error fetching user data:", error);
// 处理错误,比如显示错误消息或重定向到登录页面
}
});
return {
userData,
};
},
};
</script>
Demo 效果截图:
登录成功:
登陆成功,跳转主页面,可以看到, 请求头传了token
token过期,返回登陆页面,重新登录获取token,达到token验证的效果。
大体的流程就这样啦,JWT的token自动续期不如session那样方便,session有参数配置,JWT的token的自动续期,要通过双令牌,验证令牌,刷新令牌实现自动续期,具体下期再聊啦!
小白自学上路,有不对的地方欢迎指正,也在学习,只要每天学一点,我们就在进步喔,不做摆烂人✊。