JWT 由三部分组成,用点(.
)分隔 Header(头部) Payload(负载)Signature(签名)
一、原理
Jwt原理其实很简单,在后端首先要有个拦截器,他会拦截所有http请求,意思就是所有访问都被拦截掉,当然这是不合理的。所以首先我们要手动放行登陆页面,也就是登录请求不拦截可以访问,然后自此以后的所有请求都会被拦截。如何不让他拦截呢?这时候就要设置一些规则来放行请求,那么这个规则就是Jwt令牌的用处。他会生成一个token在前后端传来传去,怎么传呢?前端的http请求都有一个header,token就会被携带在里面,后端解析http请求,拿到token来验证。如果通过则放行,不通过就拦截。注意(token在实体类中有字段,但是数据库中不必有专门的列来保存token)
@TableField(exist = false)
private String token;
二、第一次请求生成token,不验证token
1.首先,当用户成功登陆时,会请求/login
@PostMapping("/login")
public Result login(@RequestBody User user) {
if(StrUtil.isBlank(user.getUsername()) || StrUtil.isBlank(user.getPassword())) {
return Result.error("帐号或密码不能为空");
}
user = userService.loginUser(user);
return Result.success(user);
}
2.访问user的服务层的loginUSer,先通过user参数,查询对应的user给dbuser,判断是否存在,和user参数的密码和查到的密码是否相同。都满足则创建token,把token通过set方法给dbuser,返回给前端
@Override
public User loginUser(User user) {
User dbuser = userMapper.selectByUsername(user.getUsername());
if(dbuser == null){
throw new ServiceException("用户名或密码错误");
}
if(!user.getPassword().equals(dbuser.getPassword())){
throw new ServiceException("用户名或密码错误");
}
String token = TokenUtils.createToken(dbuser.getId().toString(), dbuser.getPassword());
dbuser.setToken(token);
return dbuser;
}
这里面用到了TokenUtils的createToken方法,就是生成token的地方
@Component // 标记此类为Spring组件,使其可以被Spring管理
public class TokenUtils {
// 声明一个静态的UserMapper,用于在静态方法中访问用户数据
private static UserMapper staticUserMapper;
// 注入UserMapper实例
@Resource
UserMapper userMapper;
// 在类实例化后,设置静态的UserMapper
@PostConstruct // 标记此方法在构造函数后自动执行
public void setUserService() {
staticUserMapper = userMapper; // 将实例UserMapper赋值给静态变量
}
/**
* 生成token
*
* @param userId 用户ID
* @param sign 用于签名的密钥
* @return 生成的token字符串
*/
public static String createToken(String userId, String sign) {
// 创建JWT并设置载荷
return JWT.create()
.withAudience(userId) // 将用户ID存储在token的载荷中
.withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 设置token有效期为2小时
.sign(Algorithm.HMAC256(sign)); // 使用指定的密钥签名token
}
}
三、随后的请求都要验证token
上面我们已经说到第一次请求,也就是登录时会生成token,返回给前端。前端会保存在浏览器中,以后每次header里面都会携带这个token,那么现在就开始第二次请求。
1.验证token过程
因为拦截器的作用,发过来的http请求会被拦截,以验证规则。
当前端请求发送到后端会首先进到这里验证token,而不是直接访问@GetMapping("*请求的接口*")
首先,会从请求头拿token,如果没有则拦截,否则 验证token是否为空,如果为空拦截,
不为空后,解析token中user的id,然后查询数据库是否有这个user,否则拦截,当查到后,
使用用户密码生成一个验证器(为什么是密码呢?因为上面我们生成token时就是用密码作为密钥),与传过来的token进行验证,如果通过则,验证成功,不拦截请求,访问@GetMapping("*请求的接口*")
/**
* 功能:JWT 拦截器,用于对请求进行身份验证
* 作者:lhp
* 日期:2024/9/26 23:01
*/
public class JwtInterceptor implements HandlerInterceptor {
@Resource
private UserMapper userMapper; // 自动注入 UserMapper,用于数据库操作
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从请求头中获取 token
String token = request.getHeader("token");
// 如果请求头中没有 token,则尝试从请求参数中获取
if (StrUtil.isBlank(token)) {
token = request.getParameter("token");
}
// 执行认证,首先检查 token 是否为空
if (StrUtil.isBlank(token)) {
// 如果 token 为空,抛出未授权异常
throw new ServiceException("401", "请登录");
}
// 获取 token 中的用户 ID
String userId;
try {
userId = JWT.decode(token).getAudience().get(0); // 解码 token,获取用户 ID
} catch (JWTDecodeException j) {
// 解码失败,抛出未授权异常
throw new ServiceException("401", "请登录");
}
// 根据 token 中的 userId 查询数据库,获取用户信息
User user = userMapper.selectById(Integer.valueOf(userId));
if (user == null) {
// 如果用户不存在,抛出未授权异常
throw new ServiceException("401", "请登录");
}
// 使用用户密码生成 JWTVerifier,用于验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
// 验证 token 的有效性
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
// 验证失败,抛出未授权异常
throw new ServiceException("401", "请登录");
}
// 所有验证通过,返回 true,表示请求可以继续
return true;
}
}
至此就是Jwt的整个流程。以下是拦截器的代码,这个配置类的主要功能是设置一个 JWT 拦截器,用于拦截所有的 HTTP 请求,以便于进行身份验证,但对 /login
请求路径不进行拦截
/**
* 功能:拦截器配置类,用于配置和注册自定义的拦截器
* 作者:lhp
* 日期:2024/9/26 23:15
*/
@Configuration // 标记该类为配置类,Spring 会在运行时自动识别并加载该类的 Bean 定义
public class InterceptorConfig extends WebMvcConfigurationSupport {
/**
* 重写 addInterceptors 方法,用于添加自定义拦截器
*
* @param registry 拦截器注册中心,用于注册自定义拦截器
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 注册 JwtInterceptor 拦截器
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**") // 拦截所有请求路径
.excludePathPatterns("/login"); // 排除登录路径,不拦截登录请求
// 调用父类的 addInterceptors 方法,确保其他配置能够正常工作
super.addInterceptors(registry);
}
/**
* 定义 JwtInterceptor Bean,Spring 会自动管理该 Bean 的生命周期
*
* @return JwtInterceptor 实例
*/
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor(); // 创建并返回 JwtInterceptor 的新实例
}
}