前言
在现代的后端开发中,用户登录验证和信息存储是一个常见的需求。传统的验证方式通常依赖于会话(Session)和频繁的数据库查询,这不仅增加了服务器的负担,还可能影响系统的性能。而使用 JWT(JSON Web Token)和拦截器(Interceptor)结合 ThreadLocal 存储用户信息,可以有效解决这些问题。
UserSQL
-- 创建数据库
create database user_test;
-- 使用数据库
use user_test;
-- 用户表
create table user (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) comment '密码',
nickname varchar(10) default '' comment '昵称',
email varchar(128) default '' comment '邮箱',
user_pic varchar(128) default '' comment '头像',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '用户表';
创建springbboot3项目并且引入相关依赖
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.0</version>
</dependency>
application.yml配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_test
driver-class-name: com.mysql.cj.jdbc.Driver
username: root //你的用户名
password: 123456 //你的密码
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #开启驼峰命名和下划线命名的自动转换
创建 JWT 工具类
JWT这些工具类一般放在utils文件夹下,JWT 工具类用于生成和解析 JWT Token。
JWT 由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),它们通过 Base64 编码后用点号(.
)连接在一起。
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
// 定义一个静态常量,作为 JWT 的签名密钥,这里的 KEY 可以换成你自己的
private static final String KEY = "strcy";
/**
* 接收业务数据,生成 JWT Token 并返回
*
* @param claims 业务数据,以 Map 形式传入,将被存储在 JWT 的 claims 中
* @return 生成的 JWT Token 字符串
*/
public static String genToken(Map<String, Object> claims) {
return JWT.create() //创建一个JTW生成器
.withClaim("claims", claims) // 添加载荷 将业务数据添加到 JWT 的 claims 中
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) //设置 Token 的过期时间
.sign(Algorithm.HMAC256(KEY)); //签名 使用 HMAC256 算法加密
}
/**
* 接收 JWT Token,验证 Token,并返回业务数据
*
* @param token 需要验证的 JWT Token 字符串
* @return 验证通过后,返回 JWT 中存储的业务数据(Map 形式)
*/
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY)) // 使用 HMAC256 算法和密钥进行验证
.build() // 构建JWT验证器
.verify(token) // 验证token是否正确
.getClaim("claims") //提取 JWT 中的保存的数据
.asMap();//将业务数据转换为 Map 形式并返回
}
}
创建拦截器
拦截器用于在请求到达控制器之前验证 JWT Token,并将用户信息存储到 ThreadLocal 中。
ThreadLocalUtis工具类
ThreadLocal 是一个线程局部变量,用于在同一个线程中存储和访问用户信息。
TreadLocalUtil有三个方法
- set方法用于存储 JWT中的业务数据
- get方法用于获取存入的数据
- remove方法防止内存溢出
/**
* ThreadLocal 工具类
*/
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}
//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}
//清除ThreadLocal 防止内存泄漏
public static void remove(){
THREAD_LOCAL.remove();
}
}
拦截器类
拦截器通常在项目包下重新创建一个文件夹interceptors,在这个文件夹下创建拦截器类用于是否登录拦截就叫LoginInterceptor。
import cn.strcy.utils.JwtUtil;
import cn.strcy.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
//将登录拦截器类放在 springboot 容器里面, 后面在webconfig中要排除对登录和注册请求的拦截
@Component
public class LoginInterceptor implements HandlerInterceptor {
//重写 preHandle 方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证token
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//没有登录,将http响应状态码改为401
response.setStatus(401);
//不放行
return false;
}
}
// 重写 afterCompletion 方法,在一次请求结束后,释放ThreadLocal中的业务数据
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清除ThreadLocal防止内存溢出
ThreadLocalUtil.remove();
}
}
配置拦截器
WebConfig一般放在config文件夹下,在 Spring 配置中注册拦截器,使其生效。
import cn.strcy.interceptors.LoginInterceptor;
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 LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录接口和注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","user/register");
}
}
实现登录接口
登录接口用于验证用户身份,并生成 JWT Token。
MD5加密工具类
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util {
/**
* 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
*/
protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
protected static MessageDigest messagedigest = null;
static {
try {
messagedigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsaex) {
System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
nsaex.printStackTrace();
}
}
/**
* 生成字符串的md5校验值
*
* @param s
* @return
*/
public static String getMD5String(String s) {
return getMD5String(s.getBytes());
}
/**
* 判断字符串的md5校验码是否与一个已知的md5码相匹配
*
* @param password 要校验的字符串
* @param md5PwdStr 已知的md5校验码
* @return
*/
public static boolean checkPassword(String password, String md5PwdStr) {
String s = getMD5String(password);
return s.equals(md5PwdStr);
}
public static String getMD5String(byte[] bytes) {
messagedigest.update(bytes);
return bufferToHex(messagedigest.digest());
}
private static String bufferToHex(byte bytes[]) {
return bufferToHex(bytes, 0, bytes.length);
}
private static String bufferToHex(byte bytes[], int m, int n) {
StringBuffer stringbuffer = new StringBuffer(2 * n);
int k = m + n;
for (int l = m; l < k; l++) {
appendHexPair(bytes[l], stringbuffer);
}
return stringbuffer.toString();
}
private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
// 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
stringbuffer.append(c0);
stringbuffer.append(c1);
}
}
创建User实体类
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@NotNull
private Integer id;//主键ID
private String username;//用户名
@JsonIgnore //让springmvc把当前对象转换为json格式时,忽略password,最终json字符串中就没有password这个属性了
private String password;//密码
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$")
private String nickname;//昵称
@NotEmpty
@Email
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
创建 UserMapper
import cn.strcy.pojo.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface UserMapper {
@Select("select * from user where username = #{username}")
User getByUsername(String username);
@Insert("insert into user(username,password,create_time,update_time) " +
"values (#{username},#{password},now(),now())")
void add(String username, String password);
@Update("update user set nickname = #{nickname},email = #{email},update_time = #{updateTime} where id = #{id}")
void update(User user);
@Update("update user set user_pic=#{avatarUrl}, update_time=now() where id =#{id}")
void updateAvatar(String avatarUrl, Integer id);
@Update("update user set password =#{password} ,update_time=NOW() where id =#{id}")
void updatePwd(String password, Integer id);
}
Service
UserService接口类
import cn.strcy.pojo.User;
import org.hibernate.validator.constraints.URL;
public interface UserService {
//根据用户名查询用户
User getByUsername(String username);
//添加用户
void register(String username, String password);
//更新用户
void update(User user);
//更新用户头像
void updateAvatar(@URL String avatarUrl);
//更新用户密码
void updatePwd(String newPwd);
}
UserServiceImpl实现类
import cn.strcy.mapper.UserMapper;
import cn.strcy.pojo.User;
import cn.strcy.service.UserService;
import cn.strcy.utils.Md5Util;
import cn.strcy.utils.ThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
//根据用户名查询用户
@Override
public User getByUsername(String username) {
User user = userMapper.getByUsername(username);
return user;
}
//添加用户
@Override
public void register(String username, String password) {
//先加密 MD5工具类加密处理
String md5String = Md5Util.getMD5String(password);
//后添加
userMapper.add(username,md5String);
}
//更新用户信息
@Override
public void update(User user) {
user.setUpdateTime(LocalDateTime.now());
userMapper.update(user);
}
//更新用户头像
@Override
public void updateAvatar(String avatarUrl) {
//获取用户id
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updateAvatar(avatarUrl,id);
}
//更新用户密码
@Override
public void updatePwd(String newPwd) {
String password = Md5Util.getMD5String(newPwd);
//获取用户id
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updatePwd(password,id);
}
}
UserController
import cn.strcy.pojo.Result;
import cn.strcy.pojo.User;
import cn.strcy.service.impl.UserServiceImpl;
import cn.strcy.utils.JwtUtil;
import cn.strcy.utils.Md5Util;
import cn.strcy.utils.ThreadLocalUtil;
import jakarta.validation.constraints.Pattern;
import org.hibernate.validator.constraints.URL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Validated
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserServiceImpl userService;
/**
* 注册
* @param username
* @param password
* @return
*/
@PostMapping("register")
public Result register(@Pattern(regexp = "^\\S{6,15}$") String username,@Pattern(regexp = "^\\S{6,15}$") String password) {
//用户名是否存在
User user = userService.getByUsername(username);
if (user == null) {
//注册
userService.register(username,password);
return Result.success();
}
return Result.error("用户名已存在");
}
/**
* 用户登录
* @param username
* @param password
* @return
*/
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{6,15}$") String username,@Pattern(regexp = "^\\S{6,15}$") String password) {
//通过用户名查询用户
User loginUser = userService.getByUsername(username);
//判断用户是否存在
if (loginUser == null) {
return Result.error("用户不存在");
}
//判断密码是否正确 loginUser中的是MD5加密过的,我们需要先把password加密在对比
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
//创建Claims
Map<String,Object> claims = new HashMap<>();
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
//生成JWT令牌
String token = JwtUtil.genToken(claims);
return Result.success(token);
}
//密码错误
return Result.error("密码错误");
}
/**
* 用户信息详情
* @return
*/
@GetMapping("/userInfo")
public Result<User> userInfo() {
//通过ThreadLocalUtil获取存入的token中的claims
Map<String ,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
//通过用户名查询用户信息
User user = userService.getByUsername(username);
return Result.success(user);
}
/**
* 更新用户信息
* @param user
* @return
*/
@PutMapping("/update")
public Result update( @RequestBody @Validated User user) {
userService.update(user);
return Result.success();
}
/**
* 更新用户头像
* @param avatarUrl
* @return
*/
@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam @URL String avatarUrl) {
userService.updateAvatar(avatarUrl);
return Result.success();
}
/**
* 更新用户密码
* @param map
* @return
*/
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String,String> map){
//1.参数校验
String oldPwd = map.get("old_pwd");
String newPwd = map.get("new_pwd");
String rePwd = map.get("re_pwd");
if ( !StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)) {
return Result.error("缺少必要的参数");
}
//2.oldPwd是否正确
Map<String,Object> m = ThreadLocalUtil.get();
String username = (String) m.get("username");
User user = userService.getByUsername(username);
if (!user.getPassword().equals(Md5Util.getMD5String(oldPwd))) {
return Result.error("原密码错误");
}
//3.newPwd == rePwd
if (!newPwd.equals(rePwd)){
return Result.error("两次密码输入不一致");
}
userService.updatePwd(newPwd);
return Result.success();
}
}
PostMan测试
这是相关的接口测试,发送请求要先进行登录获取token将他复制下来到用户相关接口全局Scripts中的Pre-req 中替换Authorization冒号后的token
总结
本文介绍了在 Spring Boot 项目中实现基于 JWT 和拦截器结合 ThreadLocal 存储用户信息的完整流程。通过创建 JWT 工具类、拦截器、用户相关实体类、Mapper、Service 和 Controller,实现了用户注册、登录、获取用户信息、更新用户信息等功能。JWT 用于生成和验证 Token,拦截器用于在请求到达控制器之前验证 Token 并将用户信息存储到 ThreadLocal 中,从而减少数据库查询次数并提升系统性能。最后通过 Postman 对接口进行了测试验证。