在前后端分离的架构中,传统的 Session 认证因为依赖服务端存储,难以应对分布式和跨域场景。JWT 凭借其无状态、自包含的特性,成为了现代 Web 开发的主流选择。
本文将带你使用 Spring Boot 结合 JWT 和自定义拦截器,快速搭建一套安全、高效的登录认证系统
一、配置依赖
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/big_event
username: root
password: 123456
<dependencies>
<!--jwt依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
二、为什么要登录认证和登录认证的流程
登录认证的设计主要是为安全性考虑:
1.首先,为了保护用户信息和防止数据泄露,篡改,我们必须保证用户的身份,划分权限,只有有权限才能提供相应服务,有些场景甚至需要实名认证,以保证用户信息的真实,合法,有效。
2.其次,若用户数据异常、系统故障,可以根据登录信息记录定位责任人,便于问题排查和责任追溯。
3.最后,保障服务合规性,多数场景(如政务服务、金融、电商)需通过登录认证确认用户身份,符合行业规范和安全要求,建立服务方与用户之间的合法服务关系。
登录认证流程:
用户提交凭证
用户在登录页面输入用户名(或邮箱/手机号)和密码。
后端校验与比对
服务器接收到请求后,首先会验证数据的完整性。随后,系统会根据用户名在数据库中查询对应的用户记录。
令牌颁发
一旦密码比对成功,服务器会生成一个临时的身份凭证------通常是 JSON Web Token。这个令牌包含了用户的 ID、过期时间等元数据,并经过数字签名以防篡改。
客户端存储与后续交互
服务器将生成的令牌返回给客户端。前端通常会将其存储在本地存储或 Cookie 中。然后前端就能在请求头中携带这个令牌,以证明用户身份,从而实现无状态认证。
三、JWT令牌
令牌的要求:
看完登录认证的流程以后,我们可以想一想,一个令牌应当具有哪些功能,有什么要求?
1、首先,令牌必须可靠,不能被篡改,也不能被破解,作为令牌其本身必须要足够令人信服。
2、其次,令牌还要包含用户的所有必要信息,这样后端就无需频繁查数据库,减轻数据库压力。
3、它必须能被任何平台解密,只要具备相应算法和密钥就能解开。
JWT 的结构(三段式)
JWT 的字符串格式非常独特,由三部分组成,中间用点号 . 分隔:Header.Payload.Signature。
- Header(头部):声明令牌的类型(JWT)以及加密算法(如 HMAC SHA256)。
- Payload(载荷) :存放有效信息的地方。这里存放用户 ID、过期时间等。注意:这里只是 Base64 编码,不是加密,所以千万不要放密码等敏感信息。
- Signature(签名):这是防伪标识。它是用"头部 + 载荷 + 服务器私有的密钥"通过算法生成的。一旦前两部分被篡改,签名就会对不上,服务器就会拒绝请求
创建JWT
在实际开发中,我们通常会写一个JWT工具类,以实现令牌的生成和解密。
java
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
private static final String KEY = "hzy";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)//数据的名字和存的东西
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))//设置过期时间
.sign(Algorithm.HMAC256(KEY));//加密方式
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()//创建检查器
.verify(token)//验证
.getClaim("claims")//获取名为claims的数据
.asMap();//强制转换
}
}
四、拦截器的建立与注册
在此之前,先介绍一个工具,ThreadLocal,可以将从令牌中解密的用户信息放入其中。这个工具是线程安全的,我们给用户提供服务的时候通常是一个用户开一个线程,有了这个传参会很方便。
下面展示ThreadLocal工具类的实现:
java
package org.example.utils;
import java.util.HashMap;
import java.util.Map;
/**
* 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();
}
}
值得注意的是ThreadLocal用完以后必须要清空,防止数据泄露。
拦截器是什么:
拦截器就像一个大门的安检员,它负责在请求到达业务代码前,对请求进行拦截和检查,统一进行身份校验。
为什么这么做?这样可以保证从始至终只进行一次JWT的解码,降低代码耦合度。我们没有必要在每个业务代码前都安置一个检查令牌的安检员。
拦截器的具体实现类:
结合我们前面提到的ThreadLocal工具:
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.bytebuddy.asm.Advice;
import org.example.pojo.Result;
import org.example.utils.JwtUtil;
import org.example.utils.ThreadLocalUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token= request.getHeader("Authorization");
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据加入到TreadLocal
ThreadLocalUtil.set(claims);
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//释放threadlocal
ThreadLocalUtil.remove();;
}
}
工作原理:三段式生命周期
拦截器不仅仅是在"之前"拦截,它其实贯穿了请求的整个生命周期。在 Spring Boot 中,我们通常实现 HandlerInterceptor 接口,它包含三个关键方法:
preHandle(事前拦截 - 最重要)- 时机 :Controller 方法执行之前。
- 作用 :这是做登录认证的最佳位置。我们在这里校验 JWT 令牌。
- 返回值 :返回
true表示放行(继续往下走);返回false表示拦截(请求终止)。
postHandle(事后处理)- 时机 :Controller 方法执行之后 ,但在视图渲染或数据返回给用户之前。
- 作用:可以对数据进行微调,或者添加一些通用的模型数据。
afterCompletion(最终完成)- 时机:整个请求结束之后(视图渲染完毕)。
- 作用:通常用于资源清理,比如记录日志、计算接口耗时、释放数据库连接等。
拦截器注册:
具体注册方式就是让WebConfig实现WebMvcConfigurer接口,并重写方法
注册时可以规定拦截器可以直接放行哪些业务代码,比如说登录功能就不能被拦截器拦截,因为不能因为用户没登录就不让用户登录。
java
import org.example.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");
}
}
五、Controller层实现
登录接口的实现:
java
@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("用户名错误");
}
//密码是否正确
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);
return Result.success( token);
}
return Result.error("密码错误");
}
密码正确后直接生成令牌返回到前端。
获取用户信息接口的实现:
java
@GetMapping("/userInfo")
public Result<User> userInfo(@RequestHeader(name="Authorization") String token){
Map<String,Object>map=ThreadLocalUtil.get();
String username= (String)map.get("username");
User user= userService.findByUserName(username);
return Result.success(user);
}
从请求中获取带有Authorization请求头的令牌,获取用户名后去数据库中查找用户信息。
六、总结
至此,登录认证的功能就完成了,这套方案利用 JWT 的无状态特性,配合拦截器的"守门员"机制,实现了轻量级且安全的接口保护。