在 Web 应用中,登录验证是保障系统安全的第一道防线 ------ 只有通过身份认证的用户,才能访问系统的核心资源(如员工管理、部门数据、个人中心等)。实现登录验证的核心技术包括 "会话管理" 和 "请求拦截",从传统的 Session-Cookie,到如今流行的 JWT 令牌,再配合过滤器(Filter)与拦截器(Interceptor)实现全局校验,形成了一套完整的登录验证体系。本文将以 "后台管理系统" 为场景,全面讲解登录验证的实现思路、核心技术及实战落地。
一、登录验证的核心诉求:为什么需要它?
想象一个没有登录验证的后台系统:任何人都可以直接访问/emp/list(员工列表)、/dept/delete(删除部门)等核心接口,这会导致数据泄露、恶意篡改等严重安全问题。
登录验证的核心诉求有 3 点:
- 身份认证:验证用户输入的账号密码是否正确,确认用户身份合法性;
- 身份保持:用户登录成功后,在一段时间内无需重复登录,可正常访问系统资源(会话技术实现);
- 权限控制:拦截未登录用户的非法请求,拒绝其访问受保护资源(过滤器 / 拦截器实现)。
二、会话技术:实现用户身份保持
用户登录成功后,如何让系统 "记住" 用户身份?这就需要会话技术 ,主流方案分为两类:传统的Session-Cookie和现代的JWT(Json Web Token)。
1. 传统方案:Session-Cookie 会话管理
核心原理
Session-Cookie是基于服务器端存储的会话方案,核心流程如下:
- 用户提交账号密码登录,服务器验证通过后,创建
HttpSession对象(存储用户信息,如用户 ID、用户名),并生成唯一的SessionId; - 服务器将
SessionId通过Set-Cookie响应头写入客户端浏览器; - 客户端后续发起请求时,浏览器会自动携带该
Cookie(包含SessionId)到服务器; - 服务器通过
SessionId查找对应的HttpSession对象,确认用户身份,实现 "免登录" 访问。
实战演示(Java Web)
java
// 1. 登录接口:验证账号密码,创建Session
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
// 获取前端提交的账号密码
String username = req.getParameter("username");
String password = req.getParameter("password");
// 模拟账号密码验证(实际需查询数据库)
if ("admin".equals(username) && "123456".equals(password)) {
// 登录成功:创建Session,存储用户信息
HttpSession session = req.getSession();
session.setAttribute("loginUser", username); // 存储用户名
session.setMaxInactiveInterval(3600); // 设置Session过期时间:1小时(无操作超时)
resp.getWriter().write("{"code":200,"msg":"登录成功"}");
} else {
resp.getWriter().write("{"code":500,"msg":"账号或密码错误"}");
}
}
}
// 2. 核心资源接口:通过Session验证用户身份
@WebServlet("/emp/list")
public class EmpListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=UTF-8");
// 获取Session中的用户信息
HttpSession session = req.getSession(false); // false:不存在则返回null,不创建新Session
if (session == null || session.getAttribute("loginUser") == null) {
// 未登录:返回未授权提示
resp.getWriter().write("{"code":401,"msg":"请先登录"}");
return;
}
// 已登录:返回员工列表数据
resp.getWriter().write("{"code":200,"data":[{"id":1,"name":"张三"},{"id":2,"name":"李四"}]}");
}
}
Session-Cookie 的优缺点
| 优点 | 缺点 |
|---|---|
| 安全性较高(用户信息存储在服务器端,不易被篡改) | 服务器压力大(大量 Session 存储在服务器内存 / 缓存中,高并发场景不友好) |
| 支持会话失效手动控制(如用户退出登录,直接销毁 Session) | 不支持跨域(Cookie 默认不允许跨域携带,需额外配置) |
| 实现简单,无需额外依赖 | 不支持分布式部署(Session 存储在单个服务器,集群环境下需做 Session 共享) |
2. 现代方案:JWT 令牌认证
核心原理
JWT(Json Web Token)是一种基于客户端存储的无状态令牌方案,无需服务器存储会话信息,核心流程如下:
- 用户提交账号密码登录,服务器验证通过后,根据用户信息(如用户 ID、用户名)和密钥,生成 JWT 令牌(包含头部、载荷、签名三部分);
- 服务器将 JWT 令牌返回给客户端(客户端可存储在 LocalStorage/SessionStorage/Cookie 中);
- 客户端后续发起请求时,通过请求头(如
Authorization: Bearer <JWT令牌>)携带令牌到服务器; - 服务器接收令牌后,通过密钥验证令牌的合法性(是否过期、是否被篡改),验证通过则确认用户身份。
JWT 令牌结构
JWT 令牌是一个字符串,由三部分组成,用.分隔:
-
Header(头部) :指定令牌类型(JWT)和加密算法(如 HS256),示例:
json{"alg": "HS256", "typ": "JWT"} -
Payload(载荷) :存储用户自定义信息(如用户 ID、用户名)和过期时间等元数据,示例:
json{"userId": 1, "username": "admin", "exp": 1735689600} -
Signature(签名) :服务器用头部指定的加密算法,将头部、载荷和密钥进行加密生成的签名,用于验证令牌是否被篡改。
实战演示(Java + JJWT 依赖)
(1)引入 JJWT 依赖(Maven)
JJWT 是 Java 生态中常用的 JWT 工具库,简化 JWT 的生成与验证:
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
(2)JWT 工具类
java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;
public class JwtUtils {
// 密钥(生产环境需配置在配置文件中,且保证安全性)
private static final String SECRET_KEY = "my-secret-key-32bytes-long-12345678";
// 令牌过期时间:1小时(单位:毫秒)
private static final long EXPIRE_TIME = 3600 * 1000L;
// 生成JWT令牌
public static String generateToken(String username) {
// 创建密钥(HS256算法要求密钥长度至少32位)
SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
// 构建并生成令牌
return Jwts.builder()
.setSubject(username) // 设置主题(存储用户名)
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) // 设置过期时间
.signWith(key) // 使用密钥签名
.compact();
}
// 验证JWT令牌合法性,并获取令牌中的用户信息
public static Claims verifyToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
// 验证令牌并解析载荷
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
// 令牌无效(过期、被篡改等),返回null
return null;
}
}
// 从令牌中获取用户名
public static String getUsernameFromToken(String token) {
Claims claims = verifyToken(token);
return claims == null ? null : claims.getSubject();
}
}
(3)JWT 登录与验证接口
java
@WebServlet("/jwt/login")
public class JwtLoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
// 模拟账号密码验证
if ("admin".equals(username) && "123456".equals(password)) {
// 生成JWT令牌
String token = JwtUtil.generateToken(username);
// 返回令牌给客户端
resp.getWriter().write("{"code":200,"msg":"登录成功","data":"" + token + ""}");
return;
}
resp.getWriter().write("{"code":500,"msg":"账号或密码错误"}");
}
}
@WebServlet("/jwt/emp/list")
public class JwtEmpListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=UTF-8");
// 获取请求头中的JWT令牌
String authorization = req.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
resp.getWriter().write("{"code":401,"msg":"请先登录"}");
return;
}
// 提取令牌(去除"Bearer "前缀)
String token = authorization.substring(7);
// 验证令牌并获取用户名
String username = JwtUtil.getUsernameFromToken(token);
if (username == null) {
resp.getWriter().write("{"code":401,"msg":"令牌无效或已过期"}");
return;
}
// 令牌有效:返回员工列表
resp.getWriter().write("{"code":200,"data":[{"id":1,"name":"张三"},{"id":2,"name":"李四"}]}");
}
}
JWT 的优缺点
| 优点 | 缺点 |
|---|---|
| 无状态(服务器无需存储会话信息,降低服务器压力) | 安全性较低(令牌存储在客户端,若泄露可能被恶意使用,需配合 HTTPS) |
| 支持跨域(令牌通过请求头携带,不受跨域限制) | 令牌一旦生成,无法主动作废(除非设置短期过期,或维护黑名单) |
| 支持分布式部署(任意服务器均可通过密钥验证令牌,无需 Session 共享) | 载荷部分 Base64 编码(非加密),敏感信息不可直接存储 |
三、请求拦截:实现全局登录验证
无论是 Session-Cookie 还是 JWT,若每个接口都单独编写身份验证逻辑,会造成代码冗余。此时需要 过滤器(Filter) 和 拦截器(Interceptor) 实现全局请求拦截,统一处理登录验证。
1. 过滤器(Filter):Servlet 规范中的全局拦截
Filter 是 Java Servlet 规范定义的组件,运行在 Servlet 之前,可对请求和响应进行统一拦截处理,支持对所有 Web 资源(Servlet、JSP、静态资源)进行过滤。
实战演示:JWT 全局验证过滤器
java
package com.tgt.filters;
import cn.hutool.core.util.StrUtil;
import com.tgt.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//1.获取请求url。
// servletRequest.getRequestURI() 这个api是子接口HttpServletRequest才有
// 将父接口 ServletRequest 转换为 子接口HttpServletRequest
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String uri = request.getRequestURI();
//2.判断请求url中是否包含login,
if (uri.contains("/login")){
//如果包含,说明是登录操作,放行。
filterChain.doFilter(request,response);
return;
}
//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
//4.判断令牌是否存在,如果不存在,响应401。
if (StrUtil.isEmpty(token)){
response.setStatus(401);
return;
}
try {
//5.解析token,如果解析失败,响应401
Claims claims = JwtUtils.parseJWT(token);//如果篡改或过期会发生异常
//6.放行。
filterChain.doFilter(request,response);
} catch (Exception e) {
log.error("登录校验失败,解析token失败:",e);
//解析token失败 返回401
response.setStatus(401);
}
}
}
2. 拦截器(Interceptor):Spring 框架中的全局拦截
Interceptor 是 Spring MVC 框架提供的组件,运行在 Controller 方法执行前后,仅对 Controller 请求进行拦截,功能更灵活(支持 Spring 容器管理,可注入 Service 等 Bean)。
实战演示:Spring MVC JWT 拦截器
(1)编写 JWT 拦截器
java
import cn.hutool.core.util.StrUtil;
import com.tgt.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
@Slf4j
public class LoginInterceptors implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
//4.判断令牌是否存在,如果不存在,响应401。
if (StrUtil.isEmpty(token)){
response.setStatus(401);
return false;
}
try {
//5.解析token,如果解析失败,响应401
Claims claims = JwtUtils.parseJWT(token);//如果篡改或过期会发生异常
Integer empId = Integer.valueOf(claims.get("empId").toString());
log.info("登录成功,用户id为:{}",empId);
//6.放行。
return true;
} catch (Exception e) {
log.error("登录校验失败,解析token失败:",e);
//解析token失败 返回401
response.setStatus(401);
return false;
}
}
}
(2)配置拦截器(Spring MVC 配置类)
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptors loginInterceptors;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器并绑定拦截路径,注意 是两个*,代表任意路径
registry.addInterceptor(loginInterceptors)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
3. 过滤器 vs 拦截器:核心区别
| 对比维度 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 所属规范 | Servlet 规范(Java EE 标准) | Spring MVC 框架自定义 |
| 拦截范围 | 所有 Web 资源(Servlet、JSP、静态资源) | 仅拦截 Controller 请求 |
| 执行时机 | 运行在 Servlet 容器层面,早于 Interceptor | 运行在 Spring MVC 层面,晚于 Filter |
| 依赖容器 | 不依赖 Spring 容器,可独立使用 | 依赖 Spring 容器,可注入 Bean(Service/Mapper) |
| 执行次数 | 一次请求只执行一次 | 若存在视图转发,会执行多次 |
| 功能灵活性 | 功能单一,仅支持请求 / 响应拦截 | 功能丰富,支持前置拦截、后置处理、视图渲染后处理 |
四、完整登录验证流程(实战总结)
以 "JWT + 拦截器" 为例,完整的登录验证流程如下:
-
用户登录 :前端提交账号密码到
/jwt/login接口,后端验证通过后生成 JWT 令牌,返回给前端; -
前端存储令牌:前端将 JWT 令牌存储在 LocalStorage 中;
-
前端发起请求 :前端后续访问核心接口(如
/emp/list)时,通过Authorization请求头携带 JWT 令牌; -
全局拦截验证:后端拦截器拦截请求,提取并验证 JWT 令牌;
-
请求放行 / 拦截:
- 令牌有效:放行请求,Controller 处理业务并返回数据;
- 令牌无效 / 未携带:拦截请求,返回 401 未授权提示,前端跳转至登录页。
五、登录验证最佳实践
-
令牌安全存储:
- JWT 令牌优先存储在
HttpOnlyCookie 中(防止 XSS 攻击),其次存储在 LocalStorage(需做 XSS 防护); - 敏感信息不存储在 JWT 载荷中(载荷仅 Base64 编码,可被解码)。
- JWT 令牌优先存储在
-
设置合理过期时间:
- 短期令牌:访问令牌(Access Token)过期时间设为 1 小时,减少令牌泄露风险;
- 长期令牌:刷新令牌(Refresh Token)过期时间设为 7 天,用于过期后重新获取访问令牌,无需用户重复登录。
-
HTTPS 传输:所有请求(尤其是登录请求和携带令牌的请求)使用 HTTPS 协议,防止令牌被中间人劫持。
-
避免匿名访问:除登录接口、静态资源外,所有核心接口均需拦截验证,禁止匿名访问。
-
令牌黑名单机制:若用户退出登录(令牌未过期),后端需维护 JWT 黑名单,拦截已注销的令牌。
六、总结与拓展
本文全面讲解了登录验证的核心技术,包括 Session-Cookie、JWT 两种会话方案,以及 Filter、Interceptor 两种全局拦截方案,核心要点总结:
- 会话方案选择:传统单体应用可使用 Session-Cookie,分布式 / 跨域应用优先使用 JWT;
- 拦截方案选择:简单场景用 Filter,Spring Boot/Spring MVC 项目优先用 Interceptor(更灵活);
- 安全核心:令牌存储需防 XSS、传输需用 HTTPS、过期时间需合理设置;
- 代码规范:全局拦截统一处理登录验证,避免接口内重复编写验证逻辑。
拓展方向:
- 权限细化:基于 RBAC 模型(角色 - 权限 - 用户),实现 "基于角色的访问控制"(如管理员可删除部门,普通用户仅可查看);
- 单点登录(SSO) :基于 JWT 或 CAS 协议,实现多系统统一登录(一次登录,多系统免登);
- 令牌刷新机制:实现 Access Token 过期后,通过 Refresh Token 自动刷新令牌,提升用户体验;
- 防暴力破解:登录接口添加验证码、限流机制,防止恶意账号密码爆破。
登录验证是 Web 系统安全的基石,掌握本文的核心技术,可搭建一套高效、安全的登录验证体系,为系统的稳定运行提供保障。