目录
[1.1 需求分析](#1.1 需求分析)
[1.2 思路分析](#1.2 思路分析)
[1.3 接口设计](#1.3 接口设计)
[1.4 代码实现](#1.4 代码实现)
[第二步:编写 Controller](#第二步:编写 Controller)
[第三步:编写 Service](#第三步:编写 Service)
[第四步:编写 Mapper](#第四步:编写 Mapper)
[1.5 功能测试](#1.5 功能测试)
[2.1 什么是登录校验?](#2.1 什么是登录校验?)
[2.2 核心问题:HTTP是无状态的](#2.2 核心问题:HTTP是无状态的)
[2.3 会话跟踪技术](#2.3 会话跟踪技术)
[三、JWT 令牌](#三、JWT 令牌)
[3.1 什么是 JWT?](#3.1 什么是 JWT?)
[3.2 JWT 的组成](#3.2 JWT 的组成)
[3.3 生成和校验 JWT](#3.3 生成和校验 JWT)
[生成 JWT](#生成 JWT)
[校验 JWT](#校验 JWT)
[3.4 在登录时下发 JWT](#3.4 在登录时下发 JWT)
[四、使用 Filter 实现登录校验](#四、使用 Filter 实现登录校验)
[4.1 什么是 Filter(过滤器)?](#4.1 什么是 Filter(过滤器)?)
[4.2 Filter 快速入门](#4.2 Filter 快速入门)
[第三步:开启 Servlet 组件支持](#第三步:开启 Servlet 组件支持)
[4.3 实现登录校验 Filter](#4.3 实现登录校验 Filter)
[4.4 Filter 详解](#4.4 Filter 详解)
[五、使用 Interceptor 实现登录校验](#五、使用 Interceptor 实现登录校验)
[5.1 什么是 Interceptor?](#5.1 什么是 Interceptor?)
[5.2 Interceptor 快速入门](#5.2 Interceptor 快速入门)
[5.3 实现登录校验 Interceptor](#5.3 实现登录校验 Interceptor)
[5.4 Interceptor 详解](#5.4 Interceptor 详解)
[六、Filter vs Interceptor](#六、Filter vs Interceptor)
在前面的课程中,我们已经实现了部门管理、员工管理的基本功能。但是大家有没有发现一个问题:我们并没有登录,就直接访问到了Tlias智能学习辅助系统的后台。
这是一个非常严重的安全隐患。如果任何人都能直接访问后台管理系统,那数据安全就无从谈起。所以今天的主题就是------登录认证。
最终我们要实现的效果是:
如果用户名或密码错误,不允许登录系统
如果用户名和密码都正确,则登录成功,可以正常访问系统
接下来,就让我们一步步来实现这个完整的登录认证功能。
一、登录功能
1.1 需求分析
在登录界面中,用户输入用户名和密码,点击"登录"按钮后,请求会发送到服务器。服务端判断用户名和密码是否正确:
正确:返回成功结果,前端跳转至系统首页
错误:返回错误提示,用户无法进入系统
1.2 思路分析
登录功能的本质是什么?其实就是一个查询操作------根据用户名和密码去数据库查询员工信息:
-
如果查询到了数据,说明用户名和密码都正确,登录成功
-
如果没查到数据,说明用户名或密码错误,登录失败
1.3 接口设计
-
请求路径 :
/login -
请求方式 :
POST -
请求参数(JSON格式):
{
"username": "admin",
"password": "123456"
} -
响应数据(JSON格式):
{
"code": 1,
"msg": "success",
"data": {
"id": 1,
"username": "songjiang",
"name": "宋江",
"token": "..."
}
}
1.4 代码实现
第一步:准备响应实体类
我们先创建一个 LoginInfo 类,用来封装登录成功后返回给前端的数据:
java
/**
* 登录成功结果封装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
private Integer id; //员工ID
private String username; //用户名
private String name; //姓名
private String token; //令牌
}
第二步:编写 Controller
在 LoginController 中接收登录请求:
java
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工来登录啦 , {}", emp);
LoginInfo loginInfo = empService.login(emp);
if(loginInfo != null){
return Result.success(loginInfo);
}
return Result.error("用户名或密码错误~");
}
}
解释 :
@RequestBody将前端传来的JSON数据自动封装到Emp对象中。如果登录成功返回用户信息,否则返回错误提示。
第三步:编写 Service
接口 EmpService:
java
/**
* 登录
*/
LoginInfo login(Emp emp);
实现类 EmpServiceImpl:
java
@Override
public LoginInfo login(Emp emp) {
Emp empLogin = empMapper.getUsernameAndPassword(emp);
if(empLogin != null){
LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), null);
return loginInfo;
}
return null;
}
第四步:编写 Mapper
使用 @Select 注解直接编写SQL:
java
/**
* 根据用户名和密码查询员工信息
*/
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getUsernameAndPassword(Emp emp);
1.5 功能测试
重启服务,使用 Apifox 发送 POST 请求测试:
-
输入正确的用户名和密码 → 返回用户信息,登录成功
-
输入错误的用户名或密码 → 返回"用户名或密码错误~"
前端联调测试通过后,我们发现一个严重问题 :虽然登录功能可以正常工作,但即使在未登录状态下,直接在浏览器地址栏输入后台页面地址,依然可以访问系统。
这是因为我们只在登录接口做了验证,但其他所有接口(部门管理、员工管理等)都没有做任何登录判断。也就是说,登录功能只是徒有其表。
要解决这个问题,我们需要引入登录校验机制。
二、登录校验
2.1 什么是登录校验?
所谓登录校验,就是在服务器端接收到请求后,先验证用户是否已经登录:
如果已登录 → 正常执行业务操作
如果未登录 → 拒绝访问,返回错误信息,前端跳转到登录页面
2.2 核心问题:HTTP是无状态的
为什么需要登录校验?因为 HTTP 协议是无状态的------每一次请求都是独立的,下一次请求不会携带上一次请求的任何数据。
当你登录成功后,服务器虽然知道"你登录了",但当你再次发起请求(比如查询部门列表)时,服务器已经不记得你之前登录过了。
所以我们需要一种机制,让服务器能够识别:这个请求到底是哪个用户发来的?这个用户有没有登录过?
2.3 会话跟踪技术
**会话跟踪:**一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
为了解决"多次请求间共享数据"的问题,我们需要使用会话跟踪技术。主要有三种方案:
方案一:Cookie(客户端存储)
Cookie 是存储在客户端浏览器中的。服务器设置 Cookie 后,浏览器会自动保存,并在后续请求中自动携带。
流程:
-
用户登录成功 → 服务器设置 Cookie(包含用户信息)
-
浏览器自动保存 Cookie
-
后续每次请求 → 浏览器自动携带 Cookie
-
服务器读取 Cookie → 识别用户身份

优点 :浏览器自动支持,无需手动操作
缺点:
-
移动端APP无法使用
-
用户可以禁用 Cookie
-
无法跨域
java
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
方案二:Session(服务端存储)
Session 是存储在服务器端的。它的底层基于 Cookie 实现。
流程:
-
首次请求 → 服务器创建 Session 对象,并生成唯一 ID
-
服务器通过 Cookie 将 Session ID 返回给浏览器
-
后续请求 → 浏览器携带 Cookie(包含 Session ID)
-
服务器根据 Session ID 找到对应的 Session 对象

优点 :数据存储在服务端,安全性更高
缺点:
-
集群环境下无法直接使用(多台服务器间 Session 不共享)
-
同样存在 Cookie 的所有缺点
注意:Session 依赖于 Cookie,如果 Cookie 不可用,Session 也就失效了。
java
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
方案三:令牌技术(当前主流)
令牌就是一个用户身份的标识 ,本质就是一个字符串。
流程:
-
登录成功 → 服务器生成一个令牌(包含用户信息),返回给前端
-
前端存储令牌(可存 Cookie 或 localStorage)
-
后续请求 → 前端将令牌携带在请求头中
-
服务器校验令牌有效性 → 有效则放行,无效则拒绝
优点:
-
支持 PC 端和移动端
-
解决集群环境下的认证问题
-
减轻服务器存储压力(令牌由客户端保存)
缺点:需要自己实现生成、传递和校验的逻辑
在现在的企业开发中,令牌技术是主流选择 。而我们使用的令牌类型是 JWT。
三、JWT 令牌
3.1 什么是 JWT?
JWT (JSON Web Token)(https://jwt.io/)是一种简洁的、自包含的令牌格式,用于在通信双方安全地传输信息。
简洁:只是一个字符串,可以在请求头或参数中轻松传递
自包含:令牌本身包含了用户信息,无需额外查询数据库
3.2 JWT 的组成
JWT 由三个部分组成,用英文句号 . 分隔:
java
xxxxx.yyyyy.zzzzz

第一部分:Header(头)
记录令牌类型和签名算法,例如:
java
{"alg":"HS256", "type":"JWT"}
第二部分:Payload(有效载荷)
携带自定义信息(如用户ID、用户名等),例如:
java
{"id":1, "username":"Tom"}
第三部分:Signature(签名)
防止令牌被篡改。将 Header 和 Payload 结合,并使用密钥和指定算法计算得出。
安全原理:一旦 JWT 中的任何字符被篡改,签名验证就会失败,从而保证了令牌的完整性和安全性。
JWT是如何将原始的JSON格式数据,转变为字符串的呢?
其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码
Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号
需要注意的是Base64是编码方式,而不是加密方式。
3.3 生成和校验 JWT
引入依赖
XML
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成 JWT
java
@Test
public void testGenJwt() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 10);
claims.put("username", "itheima");
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0")
.addClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
.compact();
System.out.println(jwt);
}
校验 JWT
java
@Test
public void testParseJwt() {
Claims claims = Jwts.parser().setSigningKey("aXRjYXN0")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko")
.getBody();
System.out.println(claims);
}
注意事项:
签名密钥必须保持一致
令牌过期后解析会报错
令牌被篡改后解析会报错
3.4 在登录时下发 JWT
我们创建一个 JwtUtils 工具类来简化操作:
java
public class JwtUtils {
private static String signKey = "SVRIRUlNQQ==";
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @return
*/
public static String generateJwt(Map<String,Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
修改 EmpServiceImpl 的 login 方法:
java
@Override
public LoginInfo login(Emp emp) {
Emp empLogin = empMapper.getUsernameAndPassword(emp);
if(empLogin != null){
//1. 生成JWT令牌
Map<String,Object> dataMap = new HashMap<>();
dataMap.put("id", empLogin.getId());
dataMap.put("username", empLogin.getUsername());
String jwt = JwtUtils.generateJwt(dataMap);
LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt);
return loginInfo;
}
return null;
}
现在登录成功后会返回 JWT 令牌,前端会将其存储在 localStorage 中,并在后续请求中通过请求头 token 携带回来。
四、使用 Filter 实现登录校验
现在前端已经在每次请求中携带了令牌,我们需要在服务端统一拦截所有请求,校验令牌的有效性。
4.1 什么是 Filter(过滤器)?
Filter 是 JavaWeb 三大组件之一,它可以在请求到达资源之前进行拦截,完成一些通用操作(如登录校验、编码处理等)。
4.2 Filter 快速入门
第一步:定义过滤器
java
public class DemoFilter implements Filter {
//初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init ...");
}
//拦截到请求时,调用该方法,可以调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
System.out.println("拦截到了请求...");
}
//销毁方法, web服务器关闭时调用, 只调用一次
public void destroy() {
System.out.println("destroy ... ");
}
}
第二步:配置过滤器
只需要在Filter类上添加一个注解:@WebFilter,并指定属性urlPatterns,通过这个属性指定过滤器要拦截哪些请求
java
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
//初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init ...");
}
//拦截到请求时,调用该方法,可以调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
System.out.println("拦截到了请求...");
}
//销毁方法, web服务器关闭时调用, 只调用一次
public void destroy() {
System.out.println("destroy ... ");
}
}
第三步:开启 Servlet 组件支持
在启动类上添加 @ServletComponentScan:
java
@ServletComponentScan //开启对Servlet组件的支持
@SpringBootApplication
public class TliasManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasManagementApplication.class, args);
}
}
注意事项: 在过滤器Filter中,如果不执行放行操作,将无法访问后面的资源。 放行操作:
chain.doFilter(request, response);
大概清楚了在Filter过滤器的实现步骤了,那在正式开发登录校验过滤器之前,我们思考两个问题:1.所有的请求,拦截到了之后,都需要校验令牌吗 ?
答案:登录请求例外
2.拦截到请求后,什么情况下才可以放行,执行业务操作 ?
答案:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果
4.3 实现登录校验 Filter
业务逻辑
获取请求 URL
判断是否是登录请求 → 是则直接放行
获取请求头中的 token
判断 token 是否存在 → 不存在则返回 401
解析 token → 解析失败则返回 401
放行
代码实现
java
/**
* 令牌校验过滤器
*/
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//1. 获取请求url。
String url = request.getRequestURL().toString();
//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){ //登录请求
log.info("登录请求 , 直接放行");
chain.doFilter(request, response);
return;
}
//3. 获取请求头中的令牌(token)。
String jwt = request.getHeader("token");
//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){ //jwt为空
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//5. 解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//6. 放行。
log.info("令牌合法, 放行");
chain.doFilter(request , response);
}
}
4.4 Filter 详解
执行流程

java
请求 → Filter(放行前逻辑)→ 放行 → 访问资源 → Filter(放行后逻辑)→ 响应
拦截路径配置
|------------|------------------|-----------------------|
| 拦截路径 | urlPatterns值 | 含义 |
| 拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
| 目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
| 拦截所有 | /* | 访问所有资源,都会被拦截 |
过滤器链

过滤器链上过滤器的执行顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。 比如:
AbcFilter
DemoFilter
这两个过滤器来说,AbcFilter 会先执行,DemoFilter会后执行。
五、使用 Interceptor 实现登录校验
除了 Filter,我们还可以使用 Spring 框架提供的 Interceptor(拦截器) 来实现登录校验。
5.1 什么是 Interceptor?
Interceptor 是 Spring 框架提供的,用于动态拦截控制器方法执行的机制。它比 Filter 更精细,只拦截 Spring 管理的资源(Controller 方法)。
5.2 Interceptor 快速入门
第一步:定义拦截器
java
//自定义拦截器
@Component
public class DemoInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //true表示放行
}
//目标资源方法执行后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ... ");
}
//视图渲染完毕后执行,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion .... ");
}
}
第二步:注册配置拦截器
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}
5.3 实现登录校验 Interceptor
java
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1. 获取请求url。
String url = request.getRequestURL().toString();
//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){ //登录请求
log.info("登录请求 , 直接放行");
return true;
}
//3. 获取请求头中的令牌(token)。
String jwt = request.getHeader("token");
//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){ //jwt为空
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}
//5. 解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}
//6. 放行。
log.info("令牌合法, 放行");
return true;
}
}
注册配置:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
}
}
5.4 Interceptor 详解
拦截路径配置
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(demoInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
.excludePathPatterns("/login");//设置不拦截的请求路径
}
}
|-----------------|---------------|-------------------------------------------|
| 拦截路径 | 含义 | 举例 |
| /* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 |
| /** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
| /depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts |
| /depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 |
执行流程

java
请求 → Filter → DispatcherServlet → Interceptor(preHandle) → Controller
→ Interceptor(postHandle) → 视图渲染 → Interceptor(afterCompletion) → 响应
六、Filter vs Interceptor
| 对比项 | Filter(过滤器) | Interceptor(拦截器) |
|---|---|---|
| 来源 | JavaWeb 规范 | Spring 框架 |
| 拦截范围 | 所有 Web 资源(包括静态资源) | 仅 Spring 管理的 Controller |
| 接口 | javax.servlet.Filter |
org.springframework.web.servlet.HandlerInterceptor |
| 执行时机 | 请求进入 Web 容器后,进入 Servlet 前 | 请求进入 DispatcherServlet 后,进入 Controller 前 |
| 使用场景 | 通用拦截(编码、日志、权限) | 业务相关拦截(登录校验、权限验证) |
建议 :对于登录校验这种业务相关的拦截,推荐使用 Interceptor,因为它更加灵活,能精确控制到 Controller 方法级别。
七、最终效果
完成以上所有步骤后:
-
未登录用户访问后台页面 → 被拦截 → 自动跳转到登录页面
-
登录成功 → 服务端生成 JWT 令牌 → 前端存储令牌
-
已登录用户访问后台功能 → 请求携带令牌 → 服务端校验通过 → 正常访问
完整流程回顾:
实现登录功能(查询数据库验证用户名密码)
引入 JWT 令牌技术(登录成功后生成令牌返回给前端)
使用 Filter 或 Interceptor 统一拦截请求
在拦截器中校验 JWT 令牌的有效性
校验通过放行,不通过返回未登录状态








