登录认证 · JWT · 过滤器 · 拦截器 · 全局异常处理 · 重点总结
一、登录认证整体流程
1.1 核心思路
| 步骤 |
操作 |
| ① 用户登录 |
浏览器发起 POST /login 请求,携带用户名和密码 |
| ② 服务端校验 |
根据用户名和密码查询数据库,校验通过则生成 JWT 令牌返回给前端 |
| ③ 前端存储令牌 |
前端将 JWT 令牌存入 localStorage(浏览器本地存储) |
| ④ 后续请求携带令牌 |
每次请求自动在 请求头 token 中携带 JWT |
| ⑤ 服务端拦截校验 |
用过滤器或拦截器统一拦截,校验令牌有效性,无效则返回 NOT_LOGIN 错误 |
1.2 HTTP 协议无状态特性
HTTP 是无状态协议:每次请求独立,下次请求不会自动携带上次的数据。所以需要会话跟踪技术才能识别用户登录状态。
二、会话跟踪三种技术对比
| 方案 |
数据位置 |
优点 |
缺点 |
| Cookie |
客户端浏览器 |
HTTP 协议原生支持,自动携带 |
移动端不支持、可被禁用、不能跨域、不安全 |
| Session |
服务端 |
数据安全(存服务端) |
集群环境下 Session 不共享、底层依赖 Cookie、移动端不支持 |
| 令牌(JWT) ⭐ |
客户端任意位置 |
支持 PC/移动端、可跨域、解决集群问题、减轻服务器存储压力 |
需要自己实现生成、传递、校验 |
企业开发首选:令牌技术(JWT)
2.1 Cookie/Session 关键 API
1 2 3 4 5 6 7
| response.addCookie(new Cookie("login_username", "itheima")); Cookie[] cookies = request.getCookies();
session.setAttribute("loginUser", "tom"); Object user = session.getAttribute("loginUser");
|
三、JWT 令牌
3.1 核心概念
| 项目 |
说明 |
| 全称 |
JSON Web Token |
| 官网 |
https://jwt.io/ |
| 本质 |
一个简洁的字符串(三部分用 . 分隔) |
| 特点 |
简洁(字符串可在请求头/参数传递)、自包含(可存储自定义数据) |
3.2 JWT 三部分组成
| 组成 |
内容 |
示例 |
| Header(头) |
令牌类型、签名算法 |
{"alg":"HS256","typ":"JWT"} |
| Payload(载荷) |
自定义数据(用户 ID、名称、过期时间等) |
{"id":1,"username":"Tom","exp":1672729730} |
| Signature(签名) |
由 Header + Payload + 密钥计算出的签名,防止令牌被篡改 |
加密后的字符串 |
Header 和 Payload 都是 Base64 编码(可直接解码出来),签名部分不是编码而是签名算法计算结果。
篡改任何一个字符都会导致校验失败 → JWT 是安全可靠的。
3.3 引入 JWT 依赖
1 2 3 4 5
| <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
|
3.4 生成与解析 JWT
| 操作 |
方法 |
| 生成 JWT |
Jwts.builder().setClaims(map).signWith(算法, 密钥).setExpiration(过期时间).compact() |
| 解析 JWT |
Jwts.parser().setSigningKey(密钥).parseClaimsJws(令牌).getBody() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Map<String, Object> claims = new HashMap<>(); claims.put("id", 1); claims.put("username", "Tom");
String jwt = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS256, "itheima") .setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) .compact();
Claims claims = Jwts.parser() .setSigningKey("itheima") .parseClaimsJws(jwt) .getBody(); System.out.println(claims.get("id")); System.out.println(claims.get("username"));
|
关键规则:
- 解析时使用的密钥必须与生成时一致
- 令牌过期或被篡改 → 解析时抛异常
- 密钥长度建议 ≥ 32 字节(256 比特)
3.5 JWT 工具类(Spring 管理 + 配置文件)
application.yml:
1 2 3 4
| myjwt: key: sdjfkgsdsfgyusadgfklsay78657iusfdgu98dstfoghkjsdtgf time: 86400000
|
JWT 工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Component public class MyJwtUtils {
@Value("${myjwt.key}") private String signKey;
@Value("${myjwt.time}") private Long expire;
public String generateJwt(Map<String, Object> claims) { return Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256, signKey) .setExpiration(new Date(System.currentTimeMillis() + expire)) .compact(); }
public Claims parseJWT(String jwt) { return Jwts.parser() .setSigningKey(signKey) .parseClaimsJws(jwt) .getBody(); } }
|
3.6 登录接口生成令牌
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @RestController public class LoginController {
@Autowired private EmpService empService;
@PostMapping("/login") public Result login(@RequestBody LoginDto dto) { String token = empService.login(dto); if (token != null) { return Result.success(token); } return Result.error("NOT_LOGIN"); } }
@Service public class EmpServiceImpl implements EmpService {
@Autowired private EmpMapper empMapper; @Autowired private MyJwtUtils myJwtUtils;
@Override public String login(LoginDto dto) { Emp emp = empMapper.selectEmpByName(dto.getUsername()); if (emp == null) return null; if (emp.getPassword().equals(dto.getPassword())) { Map<String, Object> map = Map.of("id", emp.getId(), "name", emp.getName()); return myJwtUtils.generateJwt(map); } return null; } }
|
四、Filter 过滤器
4.1 核心概念
| 项目 |
说明 |
| 定义 |
JavaWeb 三大组件(Servlet、Filter、Listener)之一 |
| 作用 |
拦截浏览器请求,做统一处理(登录校验、编码处理、敏感字符过滤等) |
| 特点 |
必须先经过滤器,才能访问 Web 资源 |
4.2 三大生命周期方法
| 方法 |
调用时机 |
调用次数 |
init() |
Web 服务器启动时(创建对象时) |
1 次 |
doFilter() |
每次请求到达时 |
多次(每次请求一次) |
destroy() |
服务器关闭时(销毁对象时) |
1 次 |
4.3 定义 Filter 三步骤
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @WebFilter(urlPatterns = "/*") public class MyFilter implements Filter {
@Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { System.out.println("放行前逻辑...");
chain.doFilter(req, resp);
System.out.println("放行后逻辑..."); } }
|
1 2 3 4 5 6 7 8
| @ServletComponentScan @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
|
4.4 拦截路径配置
| 拦截路径 |
urlPatterns 值 |
含义 |
| 拦截具体路径 |
/login |
仅访问 /login 才被拦截 |
| 目录拦截 |
/emps/* |
访问 /emps 下所有资源被拦截 |
| 拦截所有 |
/* |
所有请求都被拦截(最常用) |
4.5 过滤器执行流程
1 2 3
| 浏览器请求 → Filter 放行前逻辑 → chain.doFilter() → Web 资源(Controller) ↓ 浏览器响应 ← Filter 放行后逻辑 ← ────────────────────────
|
4.6 过滤器链
一个项目可定义多个 Filter,形成过滤器链。
| 规则 |
说明 |
| 执行顺序 |
按 类名首字母排序(A 比 X 先执行) |
| 放行前逻辑 |
正向执行:1 → 2 → 3 |
| 放行后逻辑 |
反向执行:3 → 2 → 1 |
4.7 登录校验过滤器完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @Slf4j @WebFilter(urlPatterns = "/*") public class LoginCheckFilter implements Filter {
@Resource private MyJwtUtils myJwtUtils;
@Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp;
String url = request.getRequestURL().toString();
if (url.contains("/login")) { chain.doFilter(req, resp); return; }
String token = request.getHeader("token");
if (!StringUtils.hasLength(token)) { returnError(response); return; }
try { myJwtUtils.parseJWT(token); } catch (Exception e) { log.info("令牌解析失败"); returnError(response); return; }
chain.doFilter(req, resp); }
private void returnError(HttpServletResponse response) throws IOException { String json = JSON.toJSONString(Result.error("NOT_LOGIN")); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(json); } }
|
五、Interceptor 拦截器
5.1 核心概念
| 项目 |
说明 |
| 定义 |
Spring 框架提供的,动态拦截 Controller 方法执行的机制 |
| 类似 |
类似过滤器,但只拦截 Spring 环境内的资源 |
| 接口 |
实现 HandlerInterceptor 接口 |
5.2 三大生命周期方法
| 方法 |
调用时机 |
返回值含义 |
preHandle() |
Controller 方法执行前 |
true = 放行;false = 不放行 |
postHandle() |
Controller 方法执行后,视图渲染前 |
— |
afterCompletion() |
视图渲染后(最后执行) |
— |
5.3 定义 + 注册拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Component @Slf4j public class LoginCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token");
if (!StringUtils.hasLength(token)) { returnError(response); return false; }
try { JwtUtils.parseJWT(token); } catch (Exception e) { returnError(response); return false; }
return true; }
private void returnError(HttpServletResponse response) throws IOException { String json = JSONObject.toJSONString(Result.error("NOT_LOGIN")); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(json); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class WebConfig implements WebMvcConfigurer {
@Autowired private LoginCheckInterceptor loginCheckInterceptor;
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login"); } }
|
5.4 拦截路径写法
| 拦截路径 |
含义 |
示例匹配 |
/* |
一级路径(不含子路径) |
匹配 /depts、/login;不匹配 /depts/1 |
/** |
任意级路径(含子路径) |
匹配 /depts、/depts/1、/depts/1/2 |
/depts/* |
/depts 下的一级路径 |
匹配 /depts/1;不匹配 /depts/1/2 |
/depts/** |
/depts 下的任意级路径 |
匹配 /depts、/depts/1、/depts/1/2 |
六、Filter vs Interceptor(⭐ 常考)
| 对比项 |
Filter(过滤器) |
Interceptor(拦截器) |
| 接口规范 |
Filter(JavaWeb 规范) |
HandlerInterceptor(Spring 提供) |
| 拦截范围 |
所有资源(包括静态资源) |
仅 Spring 环境的资源(Controller 方法) |
| 触发顺序 |
在 DispatcherServlet 之前触发 |
在 DispatcherServlet 之后、Controller 之前触发 |
| 配置方式 |
@WebFilter + 启动类 @ServletComponentScan |
实现 WebMvcConfigurer 注册 |
| 使用场景 |
通用过滤(编码、压缩、跨域) |
业务相关(登录校验、权限校验) |
6.1 完整执行流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 浏览器请求 ↓ [Filter 放行前逻辑] ←—— Filter 拦截所有资源 ↓ DispatcherServlet(前端控制器) ↓ [Interceptor preHandle()] ←—— Interceptor 只拦截 Spring 资源 ↓ Controller 方法 ↓ [Interceptor postHandle()] ↓ 视图渲染 ↓ [Interceptor afterCompletion()] ↓ DispatcherServlet ↓ [Filter 放行后逻辑] ↓ 浏览器响应
|
七、ThreadLocal 保存用户信息
7.1 应用场景
拦截器解析 JWT 后获取的用户信息,需要在后续 Controller/Service 中使用,但不便每个方法都传参 → 使用 ThreadLocal 存储。
1 2 3 4 5 6 7
| public class MyThreadLocalUtils { private static final ThreadLocal<Emp> threadLocal = new ThreadLocal<>();
public static void set(Emp emp) { threadLocal.set(emp); } public static Emp get() { return threadLocal.get(); } public static void remove() { threadLocal.remove(); } }
|
1 2 3 4 5 6 7 8
| Claims claims = myJwtUtils.parseJWT(token); Emp emp = new Emp(claims.get("id", Integer.class), claims.get("name", String.class)); MyThreadLocalUtils.set(emp);
chain.doFilter(req, resp);
MyThreadLocalUtils.remove();
|
1 2
| Emp currentUser = MyThreadLocalUtils.get();
|
八、全局异常处理
8.1 当前问题
三层架构中任意一层抛异常 → 默认抛给框架 → 返回的 JSON 不符合统一响应规范 → 前端无法解析。
8.2 解决方案对比
| 方案 |
评价 |
| 每个 Controller 方法都 try-catch |
❌ 代码臃肿,不推荐 |
| 全局异常处理器 |
✅ 简单优雅,推荐 |
8.3 全局异常处理器代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class) public Result doRuntimeException(RuntimeException e) { e.printStackTrace(); return Result.error("亲,您的操作有问题,请重试..."); }
@ExceptionHandler(Exception.class) public Result doException(Exception e) { e.printStackTrace(); return Result.error("亲,服务器正在升级,请稍后重试..."); } }
|
8.4 关键注解
| 注解 |
作用 |
@RestControllerAdvice |
标识当前类为全局异常处理器(= @ControllerAdvice + @ResponseBody,返回值自动转 JSON) |
@ExceptionHandler(异常类型.class) |
指定该方法处理哪种类型的异常 |
九、综合速查
核心注解一览
| 注解 |
用途 |
@Value("${xxx}") |
在 Spring Bean 中读取配置文件的值 |
@Resource(name="bean名") |
按名称注入 Bean(vs @Autowired 按类型注入) |
@WebFilter(urlPatterns="...") |
配置过滤器拦截路径 |
@ServletComponentScan |
启动类加,开启 Servlet 组件扫描 |
@Configuration |
配置类专用 |
@RestControllerAdvice |
全局异常处理类 |
@ExceptionHandler(异常.class) |
处理指定类型异常的方法 |
登录认证完整流程
1 2 3 4 5 6 7 8 9 10 11
| 1. 用户登录 → POST /login(用户名密码) 2. 服务端校验 + 生成 JWT → 返回 JWT 给前端 3. 前端存储 JWT → localStorage 4. 后续每次请求携带 JWT → 请求头 token 5. Filter/Interceptor 拦截 → 校验 JWT - 登录请求 → 直接放行 - 无 token → 返回 NOT_LOGIN - 解析失败 → 返回 NOT_LOGIN - 解析成功 → 放行(可选:存入 ThreadLocal) 6. 业务处理 → 返回数据 7. 全局异常处理 → 统一捕获异常并返回标准格式
|
Filter vs Interceptor 速记口诀
1 2
| Filter → JavaWeb 规范,拦截所有资源,先于 DispatcherServlet Inter... → Spring 提供,仅拦 Controller,preHandle 返 true 才放行
|
三种会话技术速记
1 2 3
| Cookie → 客户端存储,不安全、不跨域、移动端不支持 Session → 服务端存储,集群下不共享、依赖 Cookie JWT → 字符串令牌,跨平台/跨域/集群通用 ⭐
|