前言
JWT是一种在web应用中广泛使用的令牌格式,用于在用户和服务器之间传递安全可靠的信息。JWT通常包含了用户的身份信息和一些其他的元数据,被用作身份验证和授权。因此,人们经常将JWT简称为令牌(token)。本篇我们结合前面实现的代码详细解读一个token的使用过程。
一、后端代码解读
我们先来看登录过程,以下为UserController.java的login函数:
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//根据用户名查询用户
User loginUser = userService.findByUserName(username);
//判断该用户是否存在
if (loginUser == null) {
return Result.error("用户名错误");
}
//判断密码是否正确 loginUser对象中的password是密文
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
//登录成功
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
String token = JwtUtil.genToken(claims);
//把token存储到redis中
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token,token,1,TimeUnit.HOURS);
return Result.success(token);
}
return Result.error("密码错误");
}
用户登录时,后端会根据用户名查询数据库users表,校验密码。密码校验正确表示登录成功,此时,根据id和username信息生成JWT令牌token,并保存到redis中,然后返回token信息给前端,后续前端来其它请求时都要带上这个token。值得注意的是,token信息保存到redis时,可以设置过期失效时间,上面代码设置的是一个小时。
再来看看拦截器处理,即每次前端请求处理之前需要做什么,以下代码为LoginInterceptor.java的preHandle函数:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证token
try {
//从redis中获取相同的token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
String redisToken = operations.get(token);
if (redisToken==null){
//token已经失效了
throw new RuntimeException();
}
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
前端来的每次请求中,浏览器都需要在请求头header中携带token到后端,请求头的名称为 Authorization,值为登录时下发的JWT令牌token。后端会到redis中查询该token是否存在,如果没找到,表示token已经失效,则http响应状态码为401,表示未通过鉴权。如果找到,则解析token内容保存到线程局部变量中,供业务使用。
二、前端代码解读
再来看看前端的登录过程,以下代码为Login.vue的login函数:
//调用接口,完成登录
let result = await userLoginService(registerData.value);
ElMessage.success(result.msg ? result.msg : '登录成功')
//把得到的token存储到pinia中
tokenStore.setToken(result.data)
//跳转到首页 路由完成跳转
router.push('/user')
前端登录成功后,会把获取的token保存起来,然后跳转到首页。
然后每次请求时,会在请求头中带上token,以下代码为前端拦截器代码:
request.js
import {useTokenStore} from '@/stores/token.js'
//添加请求拦截器
instance.interceptors.request.use(
(config)=>{
//请求前的回调
//添加token
const tokenStore = useTokenStore();
//判断有没有token
if(tokenStore.token){
config.headers.Authorization = tokenStore.token
}
return config;
},
(err)=>{
//请求错误的回调
Promise.reject(err)
}
)
import router from '@/router'
//添加响应拦截器
instance.interceptors.response.use(
result => {
//判断业务状态码
if(result.data.code===0){
return result.data;
}
//操作失败
ElMessage.error(result.data.msg?result.data.msg:'服务异常')
//异步操作的状态转换为失败
return Promise.reject(result.data)
},
err => {
//判断响应状态码,如果为401,则证明未登录,提示请登录,并跳转到登录页面
if(err.response.status===401){
ElMessage.error('请先登录')
router.push('/')
}else{
ElMessage.error('服务异常')
}
return Promise.reject(err);//异步的状态转化成失败的状态
}
)
在前端响应拦截器中,如果收到401,表示token过期,提示“请先登录”然后返回到登录界面。
总结
以上设计会带来一个问题,如果token过期失效时间设置过长,安全性得不到保障,只要别人截获了token就可以长时间反复使用该token来获取或者篡改信息。如果token过期失效时间设置过短,会经常需要重新登录来获取新的token,用户体验很差。为了解决这个问题,一个常用的做法就是无感刷新token。下一篇我们就来研究一下这个无感刷新token如何实现。