简单地说,JWT 就是一种网络身份认证和信息交换格式。
# 整体结构
header.payload.signature
JWT模块的核心主要是两个类:
JWT
类用于链式生成、解析或验证JWT信息。JWTUtil
类主要是JWT的一些工具封装,提供更加简洁的JWT生成、解析和验证工作。逻辑较为简单,下面的代码作为参考。
private static final ThreadLocal<UserDto> *THREAD_LOCAL* = new ThreadLocal<>();
,以便后续取用。根据需要调整 userDto
package com.zibo.common.util;
import cn.hutool.jwt.JWT;
import com.zibo.modules.user.dto.UserDto;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
/**
* JWT 工具类
*
* @author zibo
* @date 2023/7/2 14:36
* @slogan 慢慢学,不要停。
*/
public class JWTUtility {
/**
* 密钥
*/
private static final byte[] KEY = "zibo".getBytes();
/**
* 过期时间(秒):7 天
*/
public static final long EXPIRE = 7 * 24 * 60 * 60;
private JWTUtility() {
}
/**
* 根据 userDto 生成 token
*
* @param dto 用户信息
* @return token
*/
public static String generateTokenForUser(UserDto dto) {
Map<String, Object> map = new HashMap<>();
map.put("id", dto.getId());
map.put("nickname", dto.getNickname());
return generateToken(map);
}
/**
* 根据 map 生成 token 默认:HS265(HmacSHA256)算法
*
* @param map 携带数据
* @return token
*/
public static String generateToken(Map<String, Object> map) {
JWT jwt = JWT.create();
// 设置携带数据
map.forEach(jwt::setPayload);
// 设置密钥
jwt.setKey(KEY);
// 设置过期时间
jwt.setExpiresAt(new Date(System.currentTimeMillis() + EXPIRE * 1000));
return jwt.sign();
}
/**
* token 校验
* @param token token
* @return 是否通过校验
*/
public static boolean verify (String token) {
if (StringUtils.isBlank(token)) return false;
return JWT.of(token).setKey(KEY).verify();
}
/**
* token 校验,并获取 userDto
* @param token token
* @return userDto
*/
public static UserDto verifyAndGetUser(String token) {
if(!verify(token)) return null;
// 解析数据
JWT jwt = JWT.of(token);
Long id = Long.valueOf(jwt.getPayload("id").toString());
String nickname = jwt.getPayload("nickname").toString();
// 返回用户信息
return new UserDto(id, nickname);
}
}
package com.zibo.common.config;
import com.zibo.common.enums.AppCode;
import com.zibo.common.pojo.ApiException;
import com.zibo.common.pojo.UserHolder;
import com.zibo.common.util.JWTUtility;
import com.zibo.modules.user.dto.UserDto;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 自定义拦截器
* @author Administrator
*/
@Component
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
/**
* 进入controller方法之前执行。如果返回false,则不会执行 controller 的方法
*
* @param request 请求
* @param response 响应
* @param handler 处理器
* @return 是否放行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 获取 header 中的 Authorization 信息
String token = request.getHeader("token");
if (StringUtils.isNotBlank(token)) {
UserDto dto = JWTUtility.verifyAndGetUser(token);
if (dto != null) {
UserHolder.setUserInfo(dto);
} else {
log.error("token 验证失败!token is {}, uri is {}", token, request.getRequestURI());
throw new ApiException(AppCode.TOKEN_ERROR, "token 校验不通过!");
}
} else {
log.error("token 验证失败!token is {}, uri is {}", token, request.getRequestURI());
throw new ApiException(AppCode.TOKEN_ERROR, "token 为空!");
}
return true;
}
/**
* 响应结束之前
* @param request 请求
* @param response 响应
* @param handler 处理器
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 清理掉当前线程中的数据,防止内存泄漏
UserHolder.remove();
}
}
@PostMapping("/loginOrRegister")
public UserDto loginOrRegister(@RequestBody @Validated UserDto dto) {
// 通过手机号查询
UserDto byPhone = service.findByPhone(dto.getPhone());
// 如果操作标记为空,则报错
if (ObjectUtils.isEmpty(dto.getOperation())) {
throw new ApiException("操作标记不能为空!");
}
// 如果是注册
if (dto.getOperation() == 0) {
// 如果用户已经存在,则报错
if (ObjectUtils.isNotEmpty(byPhone)) {
throw new ApiException("注册失败!账号已存在!");
}
// 创建用户
Long save = service.save(dto);
// 返回用户信息
UserDto userDto = service.findById(save);
// 根据用户生成 token
String token = JWTUtility.generateTokenForUser(userDto);
// 保存到 redis
service.saveToken(userDto.getId(), token);
// 设置 token
userDto.setToken(token);
LogUtility.baseInfoWith("注册成功!" + userDto);
return userDto;
}
// 登录
if (ObjectUtils.isEmpty(byPhone)) {
log.error("登录失败!账号不存在!" + dto);
// 账号不存在
throw new ApiException("登录失败!账号不存在!com/zibo/controller/user/UserController.java:62");
} else {
// 比较密码是否一致
if (!byPhone.getPassword().equals(dto.getPassword())) {
throw new ApiException("登录失败!账号或密码错误!");
}
// 更新最后登录时间
byPhone.setLastLoginTime(LocalDateTime.now());
service.save(byPhone);
// 从 redis 获取 token
String token = service.getToken(byPhone.getId());
if (StringUtils.isBlank(token)) {
// 根据用户生成 token
token = JWTUtility.generateTokenForUser(byPhone);
log.info("用户登录,并生成token,id 为:{}, 昵称为:{},token 为:{}", byPhone.getId(), byPhone.getNickname(), token);
// 保存到 redis
service.saveToken(byPhone.getId(), token);
}
// 设置 token
byPhone.setToken(token);
LogUtility.baseInfoWith("登录成功!" + byPhone);
return byPhone;
}
}
package com.zibo.common.pojo;
import com.zibo.modules.user.dto.UserDto;
/**
* 存放用户信息的容器
* @author Administrator
*/
public class UserHolder {
private static final ThreadLocal<UserDto> THREAD_LOCAL = new ThreadLocal<>();
private UserHolder() {
}
/**
* 获取线程中的用户
* @return 用户信息
*/
public static UserDto getUserInfo() {
return THREAD_LOCAL.get();
}
/**
* 设置当前线程中的用户
* @param info 用户信息
*/
public static void setUserInfo(UserDto info) {
THREAD_LOCAL.set(info);
}
public static Long getUserId() {
UserDto dto = THREAD_LOCAL.get();
if (dto != null) {
return dto.getId();
} else {
// 注册或登录时没有,返回 0
return 0L;
}
}
public static void remove() {
THREAD_LOCAL.remove();
}
}