实战总结:登录认证(登录功能 + 登录校验)
本文是对《12-后端Web实战(登录认证)》的总结,记录”登录功能徒有其表 → 登录校验闭环”的完整链路。
项目地址:project/tlias-web-management(SpringBoot + MyBatis + JWT + Filter/Interceptor)
对应讲义:讲义/12-后端Web实战(登录认证).md
一、本节要解决的核心问题
上一节做完基础登录后,会出现一个安全漏洞:浏览器直接访问 http://localhost:90,不登录也能进后台。
原因:HTTP 是无状态协议,每次请求相互独立,服务器根本不知道”这次请求的员工登没登录过”。而部门管理、员工管理等接口在服务端没有任何登录判断。
1 | 登录功能(徒有其表) ──► 需要登录校验(闭环) |
登录校验的实现思路拆成两块:
| 关键点 | 作用 | 用到的技术 |
|---|---|---|
| ① 存登录标记 | 登录成功后留一个”已登录”凭证,后续请求能带上 | 会话技术(令牌 JWT) |
| ② 统一拦截 | 拦下所有请求,统一校验标记,避免每个接口都写一遍 | Filter 过滤器 / Interceptor 拦截器 |
为什么不能在每个 Controller 方法里手写校验?——相同逻辑会重复出现在所有增删改查接口里,代码繁琐。统一拦截是标准做法。
二、会话技术:三种方案对比
1. 概念澄清
- 会话:浏览器与服务器之间的一次连接(从打开浏览器第一次访问,到任一方断开)。一次会话可包含多次请求响应。
- 会话跟踪:服务器识别”多次请求是否来自同一浏览器”,从而在同一次会话的多次请求间共享数据。
- 为什么要共享数据?因为 HTTP 无状态,后一次请求拿不到前一次请求生成的数据。
2. 三种方案横向对比
| 方案 | 存储位置 | 底层依赖 | 优点 | 缺点 |
|---|---|---|---|---|
| Cookie | 客户端浏览器 | HTTP 协议自带(Set-Cookie 响应头 / Cookie 请求头) | 浏览器自动收发,无需手写 | 移动端 APP 不可用;不安全(可禁用);不能跨域 |
| Session | 服务端 | 底层基于 Cookie(JSESSIONID) | 数据在服务端,相对安全 | 集群环境下无法直接用;移动端不可用;可被禁用;不能跨域 |
| 令牌 Token | 客户端(如 localStorage) | 自己实现生成/传递/校验 | 支持 PC + 移动端;解决集群认证;不占服务端存储 | 需自己实现整套逻辑 |
企业开发首选第三种令牌技术,前两种已很少使用。本项目也采用令牌。
3. 跨域速记
三个维度(协议 / IP(域名) / 端口)任意一个不同就是跨域。Cookie 无法跨域,这是 Cookie/Session 方案在前后端分离部署时失效的直接原因。
1 | http://192.168.150.200/login.html → https://192.168.150.200/login 协议不同 → 跨域 |
4. 集群环境为何 Session 失效
部署多份 Tomcat + 负载均衡时:登录请求落到 Tomcat1 创建 Session,下一次查询请求可能被分到 Tomcat2,Tomcat2 里没有这个 JSESSIONID 对应的 Session → 会话丢失。令牌方案把凭证存在客户端,每次请求自带,天然避开这个问题。
三、JWT 令牌
1. 是什么
JWT(JSON Web Token):一种简洁、自包含的字符串格式,用于通信双方以 JSON 格式安全传输信息,靠数字签名保证可靠性。
- 简洁:就是一个字符串,可直接放请求参数或请求头。
- 自包含:可在令牌里存自定义数据(如 id、username),不用查服务端。
- 本质:把原始 JSON 数据安全封装成字符串,传输时基于 base64 编码(编码 ≠ 加密)。
2. 三段组成(用 . 分隔)
1 | eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko |
| 部分 | 内容 | 举例 |
|---|---|---|
| Header | 令牌类型 + 签名算法 | {"alg":"HS256","type":"JWT"} |
| Payload | 自定义信息 + 默认信息(如 exp 过期时间) | {"id":1,"username":"Tom","exp":...} |
| Signature | Header + Payload + 秘钥,按指定算法计算,防篡改 | 签名串 |
关键结论:篡改令牌任意一个字符,解析都会报错;令牌过期后解析也报错(非法)。
3. 生成与校验(讲义示例)
依赖:
1 | <dependency> |
生成 + 校验:
1 | // 生成 |
两条铁律:
- 校验用的签名秘钥,必须和生成时配套。
- 解析报错 = 令牌被篡改 或 已过期 = 非法。
四、登录时下发令牌(项目实现)
1. 整体流程
1 | 浏览器 服务端 |
2. 关键类与代码
| 类/所在层 | 全限定类名 | 作用 |
|---|---|---|
| pojo | com.zhang.pojo.LoginInfo |
登录成功响应封装:id、username、name、token |
| utils | com.zhang.utils.JwtUtils |
JWT 工具类:generateToken 生成、parseToken 解析 |
| controller | com.zhang.controller.LoginController |
/login 接口入口,返回 Result.success(loginInfo) 或 Result.error(...) |
| service | com.zhang.service.impl.EmpServiceImpl |
login(Emp):根据用户名密码查库 → 生成 JWT → 组装 LoginInfo |
| mapper | com.zhang.mapper.EmpMapper |
getByUsernamePassword(Emp):按 username + password 查询员工 |
LoginInfo(响应封装):
1 |
|
JwtUtils(项目版本,方法名是 generateToken / parseToken):
1 | public class JwtUtils { |
LoginController:
1 |
|
EmpServiceImpl.login(查库 + 生成 token):
1 |
|
前端拿到 token 后存
localStorage(不是 cookie),后续每个请求在请求头token字段带上。
五、统一拦截方案一:Filter 过滤器
1. 是什么
Filter 是 JavaWeb 三大组件(Servlet、Filter、Listener)之一,能把对资源的请求拦下来做通用操作(登录校验、统一编码、敏感字符处理等)。要先经过 Filter,才能访问后面的资源。
2. 入门三步
- 定义类实现
Filter接口,重写init/doFilter/destroy。 - 类上加
@WebFilter(urlPatterns = "/*")配置拦截路径。 - 启动类加
@ServletComponentScan开启 Servlet 组件支持。
三个方法:
| 方法 | 调用时机 | 调用次数 |
|---|---|---|
init |
web 服务器启动创建 Filter 实例时 | 1 次 |
doFilter |
每次拦截到请求 | 多次 |
destroy |
web 服务器关闭时 | 1 次 |
放行:chain.doFilter(request, response); —— 不调用就到不了后面的资源。
3. 执行流程(放行前 / 放行后)
1 | public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { |
4. 拦截路径配置
| 拦截路径 | urlPatterns | 含义 |
|---|---|---|
| 具体路径 | /login |
只拦截 /login |
| 目录拦截 | /emps/* |
拦截 /emps 下所有资源 |
| 拦截所有 | /* |
拦截所有资源 |
5. 过滤器链
多个 Filter 形成链,一个接一个执行;放行后回来时顺序相反。注解配置的 Filter,执行顺序按类名字符串自然排序(AbcFilter 先于 DemoFilter)。
6. 登录校验六步(项目类:TokenFilter,全限定类名 com.zhang.filter.TokenFilter)
1 | 1. 获取请求 URI |
1 | // 项目 TokenFilter 核心逻辑(注意:本项目中此 Filter 未启用,见"项目现状") |
六、统一拦截方案二:Interceptor 拦截器
1. 是什么
Interceptor 是 Spring 框架提供的,动态拦截 Controller 方法的执行。作用和 Filter 类似,但只拦 Spring 环境内的资源。
2. 入门两步
- 定义类实现
HandlerInterceptor,重写三个方法,加@Component。 - 写配置类
WebConfig implements WebMvcConfigurer,重写addInterceptors注册拦截器。必须加@Configuration
三个方法:
| 方法 | 调用时机 | 返回值含义 |
|---|---|---|
preHandle |
目标方法执行前 | true 放行;false 不放行(Controller 不执行) |
postHandle |
目标方法执行后 | — |
afterCompletion |
视图渲染完毕后,最后执行 | — |
3. 注册配置(项目类:WebConfig,全限定类名 com.zhang.config.WebConfig)
1 |
|
4. 拦截路径配置
| 拦截路径 | 含义 | 能匹配 | 不能匹配 |
|---|---|---|---|
/* |
一级路径 | /depts、/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 |
排除登录请求有两种写法:① 拦截器里
uri.contains("login")判断放行(项目用法);② 配置里excludePathPatterns("/login")。
5. 登录校验 Interceptor(项目类:TokenInterceptor,全限定类名 com.zhang.interceptor.TokenInterceptor)
逻辑与 Filter 完全一致,只是返回 boolean:
1 |
|
七、Filter vs Interceptor 对比(高频考点)
| 对比项 | Filter 过滤器 | Interceptor 拦截器 |
|---|---|---|
| 接口规范 | 实现 jakarta.servlet.Filter |
实现 HandlerInterceptor |
| 拦截范围 | 拦截所有资源(包括静态资源、JSP 等) | 只拦 Spring 环境中的资源(Controller) |
| 归属 | JavaWeb 三大组件(Servlet 规范) | Spring 框架提供 |
| 配置方式 | @WebFilter + @ServletComponentScan |
WebMvcConfigurer.addInterceptors |
| 核心方法 | doFilter(放行 chain.doFilter) |
preHandle 返回 boolean |
| 执行时机 | DispatcherServlet 之前 | DispatcherServlet 之后、Controller 之前 |
执行顺序(重要):
1 | 浏览器 → Filter(放行前) → DispatcherServlet → Interceptor.preHandle → Controller |
本项目二选一用了 Interceptor,Filter 仅作示例保留(见下)。
八、项目现状速查(重点!)
复习时最容易把”示例代码”和”项目真正生效的校验链路”搞混,这里专门列清。
1. 实际生效的校验链路
1 | 请求 → TokenInterceptor.preHandle (校验 token) → Controller → Service → Mapper |
- 校验组件:
TokenInterceptor(com.zhang.interceptor.TokenInterceptor,加了@Component) - 注册位置:
WebConfig(com.zhang.config.WebConfig,addPathPatterns("/**")注册拦截器)
2. Filter 是”保留示例”,未启用
TokenFilter(com.zhang.filter.TokenFilter)里 @WebFilter(urlPatterns = "/*") 被注释掉了,也没有 @Component,所以不会生效。讲义里也明确:”登录校验的过滤器和拦截器,我们只需要使用其中的一种就可以了。”
3. 项目与讲义的命名差异
| 讲义命名 | 项目命名 | 说明 |
|---|---|---|
JwtUtils.generateJwt |
JwtUtils.generateToken |
同功能,方法名不同 |
JwtUtils.parseJWT |
JwtUtils.parseToken |
同功能,方法名不同 |
empMapper.getUsernameAndPassword |
empMapper.getByUsernamePassword |
同 SQL,方法名不同 |
LoginInfo 字段 token |
同 | 一致 |
4. 项目涉及文件清单
| 层 | 全限定类名 / 类 | 本节做了什么 |
|---|---|---|
| pojo | com.zhang.pojo.LoginInfo |
新增:登录响应封装(id、username、name、token) |
| utils | com.zhang.utils.JwtUtils |
新增:JWT 工具类(generateToken、parseToken) |
| controller | com.zhang.controller.LoginController |
新增:POST /login 接口 |
| service | com.zhang.service.impl.EmpServiceImpl |
新增 login(Emp):查库 → 生成 JWT → 组装 LoginInfo |
| mapper | com.zhang.mapper.EmpMapper |
新增 getByUsernamePassword(Emp):按用户名密码查询 |
| interceptor | com.zhang.interceptor.TokenInterceptor |
新增:token 校验拦截器(当前生效) |
| filter | com.zhang.filter.TokenFilter |
新增:token 校验过滤器(未启用,仅示例) |
| config | com.zhang.config.WebConfig |
新增:注册 TokenInterceptor,拦截 /** |
九、易错点 & 复习提醒
生成 token 时取 id 的来源:项目
EmpServiceImpl.login里写的是map.put("id", emp.getId()),但emp是请求体对象,登录时前端不会传 id,所以这里的 id 实际是 null。更稳的写法是用查询结果e.getId()(讲义示例就是这么写的)。不影响 token 是否生成,但 token 载荷里的 id 会丢。登录请求必须放行:拦截器/过滤器第一件事就是判断
uri.contains("/login")放行,否则永远登不进去拿不到 token。token 的传递字段是请求头
token,不是Authorization,也不是 cookie。前后端要约定一致(本项目前端在token头里带)。401 状态码:未登录、token 为空、token 解析失败,都返回
SC_UNAUTHORIZED(401)。前端拿到 401 会自动跳登录页。JWT 秘钥必须配套:生成和解析用同一个
signKey,换了就全部失效。JWT 不是加密:Payload 是 base64 编码,能直接解码出来,所以别往里放密码等敏感信息。签名只是防篡改,不是防查看。
Filter 和 Interceptor 不要同时启用做同一件事:本项目 Filter 注释掉了,只用 Interceptor。两个都开会导致重复校验。
@WebFilter方式需要@ServletComponentScan;如果用@Component+FilterRegistrationBean则不需要。本项目 Interceptor 走的是 Spring 的WebMvcConfigurer注册方式。拦截路径
/*vs/**:Filter 用/*(Servlet 规范);Interceptor 用/**(Spring Ant 风格)。两者含义不同,别混。
十、一图回顾整条链路
1 | ┌───────── 登录 ─────────┐ |