开发中的登录校验(会话+拦截)

3 阅读16分钟

一、登录校验的核心意义与整体流程

1.1 核心意义

登录校验是Web应用的基础安全防护手段,核心作用是确认用户身份合法性,防止未登录用户访问系统敏感资源(如个人中心、后台管理页面、接口等),同时区分不同用户的操作权限,保障系统数据安全和用户信息隐私。

简单来说,登录校验就是“守门人”——只有通过身份验证(输入正确的账号密码等凭证)的用户,才能进入系统的“核心区域”,未登录用户会被拦截并引导至登录页面。

1.2 登录校验整体流程(

无论使用哪种会话技术和拦截技术,登录校验的核心流程一致,分为3步:

  1. 用户登录:用户提交账号、密码等凭证,服务器校验凭证合法性(如查询数据库匹配账号密码)。

  2. 身份标识存储:校验通过后,服务器生成唯一的用户身份标识(如SessionID、Token),并通过会话技术将标识传递给客户端,由客户端保存。

  3. 后续请求校验:用户后续访问敏感资源时,客户端携带身份标识发送请求,服务器通过拦截技术拦截请求,校验标识的合法性(如判断标识是否有效、是否对应合法用户),校验通过则放行,失败则引导至登录页面。

其中,会话技术负责“身份标识的存储与传递”,拦截技术负责“拦截请求并校验身份标识”,二者协同完成完整的登录校验。

二、核心技术一:会话技术(身份标识的存储与传递)

会话(Session)本质是“客户端与服务器之间的一次交互周期”,会话技术的核心是解决“HTTP协议无状态”的问题——HTTP协议本身不记录用户的访问状态,每次请求都是独立的,服务器无法识别“当前请求来自哪个用户”,而会话技术通过存储用户身份标识,让服务器记住用户状态。

常用的会话技术有3种:Cookie、Session、Token,其中Cookie和Session是传统会话方式,Token是当前前后端分离项目的主流方式。

2.1 Cookie(客户端存储技术)

2.1.1 核心定义

Cookie是服务器发送给客户端的小型文本文件,由客户端(浏览器)保存,每次客户端向服务器发送请求时,会自动携带对应的Cookie,服务器通过Cookie中的信息识别用户身份。

Cookie的核心特点:存储在客户端(浏览器),容量有限(通常4KB),可设置过期时间,支持跨请求携带。

2.1.2 登录校验中的使用流程

  1. 用户登录:提交账号密码,服务器校验通过后,生成唯一的用户标识(如userId)。

  2. 服务器创建Cookie:将用户标识(如userId=123)存入Cookie,设置过期时间、访问路径等属性,发送给客户端。

  3. 客户端保存Cookie:浏览器接收Cookie后,按规则保存(内存中或本地文件)。

  4. 后续请求:客户端访问敏感资源时,自动携带该Cookie,服务器读取Cookie中的userId,校验用户合法性。

2.1.3 实操代码(Java Web,Servlet方式)

// 1. 登录接口:校验通过后,创建Cookie并发送给客户端
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 接收账号密码(简化,实际需做参数校验)
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        
        // 模拟校验:假设账号密码正确(实际需查询数据库)
        if ("admin".equals(username) && "123456".equals(password)) {
            // 生成用户标识(如用户ID)
            Integer userId = 1;
            // 创建Cookie,存储用户标识
            Cookie cookie = new Cookie("userId", userId.toString());
            // 设置Cookie属性
            cookie.setMaxAge(3600); // 过期时间:1小时(单位:秒),0表示立即删除,-1表示关闭浏览器失效
            cookie.setPath("/"); // 访问路径:根路径,所有请求都能携带该Cookie
            cookie.setHttpOnly(true); // 禁止前端JS操作Cookie,防止XSS攻击(重要安全配置)
            // 发送Cookie到客户端
            resp.addCookie(cookie);
            
            // 响应登录成功
            resp.getWriter().write("登录成功");
        } else {
            resp.getWriter().write("账号密码错误");
        }
    }
}

2.1.4 优缺点

  • 优点:实现简单,无需客户端额外操作,自动携带Cookie;服务器无需存储过多用户信息,减轻服务器压力。

  • 缺点:存储容量有限;存在安全风险(可被篡改、窃取,需配合HttpOnly、HTTPS);跨域请求时Cookie携带受限(需配置跨域允许Cookie)。

2.2 Session(服务器端存储技术)

2.2.1 核心定义

Session是服务器端为每个登录用户创建的会话对象,用于存储用户的身份信息、会话状态等数据,每个Session对应一个唯一的SessionID(由服务器生成),SessionID通过Cookie传递给客户端,客户端后续请求携带SessionID,服务器通过SessionID找到对应的Session对象,识别用户身份。

Session的核心特点:存储在服务器端,容量无限制(取决于服务器内存),安全性高,默认随浏览器关闭而失效(可手动设置过期时间)。

关键关联:Session依赖Cookie——SessionID必须通过Cookie传递给客户端,若客户端禁用Cookie,Session无法使用(可通过URL重写规避,但不推荐)。

2.2.2 登录校验中的使用流程

  1. 用户登录:提交账号密码,服务器校验通过后,创建Session对象,存入用户信息(如userId、username)。

  2. 服务器生成SessionID:将SessionID存入Cookie,发送给客户端(默认Cookie名称为JSESSIONID)。

  3. 客户端保存Cookie:浏览器保存携带SessionID的Cookie。

  4. 后续请求:客户端携带Cookie(SessionID)请求敏感资源,服务器通过SessionID找到对应的Session对象,若Session存在且有效,则放行;否则引导登录。

2.2.3 实操代码(Java Web,Servlet方式)

// 1. 登录接口:校验通过后,创建Session并存储用户信息
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        
        // 模拟校验通过
        if ("admin".equals(username) && "123456".equals(password)) {
            // 获取Session(若不存在则自动创建)
            HttpSession session = req.getSession();
            // 存储用户信息到Session
            session.setAttribute("userId", 1);
            session.setAttribute("username", "admin");
            // 设置Session过期时间:1小时(单位:秒),默认30分钟
            session.setMaxInactiveInterval(3600);
            
            resp.getWriter().write("登录成功");
        } else {
            resp.getWriter().write("账号密码错误");
        }
    }
}

// 2. 测试接口:需要登录才能访问
@WebServlet("/user/info")
public class UserInfoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取Session(false:若不存在则返回null,不创建新Session)
        HttpSession session = req.getSession(false);
        if (session == null || session.getAttribute("userId") == null) {
            // 未登录,引导至登录页面
            resp.sendRedirect("/login.html");
            return;
        }
        // 已登录,返回用户信息
        String username = (String) session.getAttribute("username");
        resp.getWriter().write("当前登录用户:" + username);
    }
}

2.2.4 优缺点

  • 优点:安全性高(用户信息存储在服务器端,不易被篡改);存储容量大,可存储复杂用户信息;操作简单,Java Web原生支持。

  • 缺点:服务器压力大(每个用户对应一个Session,占用服务器内存);不支持分布式部署(Session存储在单个服务器,多服务器集群时无法共享);依赖Cookie,客户端禁用Cookie则失效。

2.3 Token(无状态会话技术,主流)

2.3.1 核心定义

Token(令牌)是服务器校验用户身份通过后,生成的一串加密字符串,包含用户身份信息(如userId)、过期时间等数据,服务器不存储Token,仅负责生成和校验Token,客户端(浏览器、APP)存储Token(如localStorage、sessionStorage、Cookie),后续请求携带Token,服务器通过解密Token校验用户合法性。

Token的核心特点:无状态(服务器不存储Token,减轻服务器压力);支持跨域、分布式部署;不依赖Cookie,可适配APP、小程序等非浏览器场景;可自定义加密规则,安全性高。

常用Token类型:JWT(JSON Web Token),是目前最主流的Token格式,结构清晰、易于解析,广泛应用于前后端分离项目。

2.3.2 JWT Token结构(必记)

JWT Token由3部分组成,用“.”分隔,整体格式:Header.Payload.Signature

  1. Header(头部):指定加密算法(如HS256)和Token类型(JWT),经过Base64编码(可解码,不加密)。

  2. Payload(载荷):存储核心数据(如userId、username、过期时间),经过Base64编码(可解码,不加密,禁止存储敏感信息如密码)。

  3. Signature(签名):用Header指定的加密算法,结合服务器秘钥(secret),对Header和Payload的编码结果进行加密,用于校验Token是否被篡改(核心安全保障)。

2.3.3 登录校验中的使用流程(JWT为例)

  1. 用户登录:提交账号密码,服务器校验通过后,生成JWT Token(包含userId、过期时间等)。

  2. 服务器返回Token:将Token通过响应体(如JSON)返回给客户端,不依赖Cookie。

  3. 客户端存储Token:客户端将Token存入localStorage(持久化)或sessionStorage(会话级)。

  4. 后续请求:客户端每次请求敏感资源时,通过请求头(如Authorization: Bearer Token)携带Token。

  5. 服务器校验Token:拦截请求,提取Token,用秘钥解密校验(判断Token是否过期、是否被篡改),校验通过则放行,失败则返回未登录提示。

2.3.4 实操代码(Spring Boot + JWT)

第一步:导入JWT依赖(Maven)

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

第二步:编写JWT工具类(生成Token、校验Token)

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;

public class JwtUtils {
    // 服务器秘钥(自定义,必须保密,用于签名和验签)
    private static final String SECRET = "abc1234567890xyz";
    // Token过期时间:1小时(单位:毫秒)
    private static final long EXPIRATION = 3600000;

    // 生成JWT Token
    public static String generateToken(Integer userId) {
        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + EXPIRATION);
        
        return Jwts.builder()
                .setSubject(userId.toString()) // 存储用户ID(Payload)
                .setIssuedAt(now) // 生成时间
                .setExpiration(expirationDate) // 过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET) // 加密算法+秘钥
                .compact();
    }

    // 校验Token,并获取Token中的用户ID
    public static Integer verifyToken(String token) {
        try {
            // 解析Token,获取Payload
            Claims claims = Jwts.parser()
                    .setSigningKey(SECRET) // 用秘钥验签
                    .parseClaimsJws(token)
                    .getBody();
            // 提取用户ID并返回
            return Integer.parseInt(claims.getSubject());
        } catch (Exception e) {
            // Token无效(过期、篡改、格式错误)
            return null;
        }
    }
}

第三步:登录接口(生成Token)

@RestController
public class LoginController {
    // 模拟登录校验(实际需查询数据库)
    @PostMapping("/login")
    public Result login(@RequestBody LoginDTO loginDTO) {
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();
        if ("admin".equals(username) && "123456".equals(password)) {
            // 校验通过,生成Token
            String token = JwtUtils.generateToken(1); // 假设用户ID为1
            return Result.success("登录成功", token);
        }
        return Result.error("账号密码错误");
    }
}

2.3.5 优缺点

  • 优点:无状态,减轻服务器压力;支持分布式部署、跨域;不依赖Cookie,适配多端(浏览器、APP、小程序);安全性高(签名机制防止篡改)。

  • 缺点:Token一旦生成,无法主动撤回(除非修改秘钥或等待过期);Payload部分可解码,禁止存储敏感信息;需要手动实现Token的生成、校验逻辑。

2.4 三种会话技术对比(实战选型重点)

技术类型存储位置安全性分布式支持适配场景
Cookie客户端(浏览器)较低(可篡改、窃取)差(跨服务器Cookie不共享)简单Web项目、存储少量非敏感信息
Session服务器端较高(用户信息不暴露)差(需额外配置Session共享)传统Java Web项目、非分布式部署
Token(JWT)客户端(任意存储)高(签名校验,防篡改)好(无状态,支持集群)前后端分离项目、分布式项目、多端适配(APP/小程序)

三、核心技术二:拦截技术(请求拦截与身份校验)

拦截技术的核心作用是“拦截客户端发送的请求”,在请求到达目标接口/资源前,校验用户的身份标识(Cookie、SessionID、Token),实现“统一登录校验”——无需在每个敏感接口中重复编写校验逻辑,提升代码复用性和维护性。

Java Web中常用的拦截技术有2种:Filter(过滤器)和Interceptor(拦截器),二者功能类似,但底层实现、适用场景有差异,实际开发中可单独使用,也可结合使用。

3.1 Filter(过滤器,Java Web原生)

3.1.1 核心定义

Filter是Java Web原生的拦截组件,属于Servlet规范的一部分,运行在请求进入Servlet之前,可拦截所有请求(包括静态资源、接口请求),核心功能是对请求进行预处理(如登录校验、编码设置、跨域配置)。

Filter的核心特点:基于函数回调实现,拦截范围广(所有请求),优先级高于Interceptor,可修改请求和响应对象。

3.1.2 登录校验中的使用(Session为例)

// 登录校验过滤器
@WebFilter(urlPatterns = "/user/*") // 拦截/user/开头的所有请求(敏感资源)
public class LoginFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 1. 转换请求/响应对象(ServletRequest -> HttpServletRequest)
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        
        // 2. 获取Session,校验登录状态
        HttpSession session = req.getSession(false);
        if (session == null || session.getAttribute("userId") == null) {
            // 未登录,响应提示或引导至登录页面
            resp.setContentType("text/html;charset=utf-8");
            resp.getWriter().write("请先登录!");
            return; // 拦截请求,不继续向下执行
        }
        
        // 3. 已登录,放行请求(让请求到达目标接口/资源)
        filterChain.doFilter(req, resp);
    }
}

3.1.3 关键说明

  • urlPatterns:指定拦截的请求路径(如/user/*拦截所有/user/开头的请求,/*拦截所有请求)。

  • FilterChain.doFilter():放行请求,若不调用该方法,请求会被拦截,无法到达目标资源。

  • 放行白名单:实际开发中,需对登录接口(/login)、注册接口、静态资源(如/login.html)设置白名单,避免拦截。

3.2 Interceptor(拦截器,Spring框架提供)

3.2.1 核心定义

Interceptor是Spring框架提供的拦截组件,运行在Spring MVC的DispatcherServlet之后、目标Controller之前,仅拦截Controller接口请求(不拦截静态资源),核心功能是对接口请求进行预处理和后处理(如登录校验、日志记录)。

Interceptor的核心特点:基于AOP思想实现,与Spring框架深度整合,可注入Spring容器中的Bean(如Service、Mapper),仅拦截接口请求,灵活性高。

3.2.2 登录校验中的使用(JWT Token为例)

第一步:编写拦截器类

// 登录校验拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
    // 预处理方法:请求到达Controller之前执行(核心校验逻辑)
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 提取请求头中的Token
        String token = request.getHeader("Authorization");
        // 处理Token格式(若前端传递格式为Bearer Token,需截取)
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        
        // 2. 校验Token
        Integer userId = JwtUtils.verifyToken(token);
        if (userId == null) {
            // 未登录,返回JSON提示(前后端分离场景)
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(JSON.toJSONString(Result.error("请先登录")));
            return false; // 拦截请求,不进入Controller
        }
        
        // 3. 已登录,放行请求(可将用户ID存入请求域,供Controller使用)
        request.setAttribute("userId", userId);
        return true;
    }
}

第二步:配置拦截器(Spring Boot)

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/api/**") // 拦截/api/开头的所有接口(敏感接口)
                .excludePathPatterns("/api/login", "/api/register"); // 白名单(不拦截)
    }
}

3.2.3 关键说明

  • preHandle():预处理方法,返回true则放行,返回false则拦截,是登录校验的核心方法。

  • addPathPatterns():指定拦截的接口路径,支持通配符(如/**拦截所有接口)。

  • excludePathPatterns():指定不拦截的白名单(如登录、注册接口),避免拦截必要请求。

  • 依赖注入:Interceptor可注入Spring容器中的Bean(如JwtUtils、UserService),方便扩展校验逻辑(如查询用户是否有效)。

3.3 Filter与Interceptor对比(选型重点)

对比维度Filter(过滤器)Interceptor(拦截器)
底层实现Servlet规范,基于函数回调Spring AOP,基于反射
拦截范围所有请求(静态资源、接口)仅拦截Controller接口请求
依赖框架无,Java Web原生支持依赖Spring框架
依赖注入不支持注入Spring Bean支持注入Spring Bean,灵活性高
执行时机请求进入Servlet之前DispatcherServlet之后,Controller之前
适用场景编码设置、跨域配置、拦截静态资源接口登录校验、日志记录、权限控制

四、登录校验实战注意事项(避坑重点)

  • 身份标识的安全防护

    • Cookie需设置HttpOnly=true,禁止前端JS操作,防止XSS攻击;设置Secure=true,仅在HTTPS协议下传输,防止Cookie被窃取。

    • Token需使用复杂秘钥,避免秘钥泄露;Payload部分禁止存储敏感信息(如密码、手机号),仅存储userId等非敏感标识。

    • SessionID、Token需设置合理的过期时间,避免长期有效导致安全风险(如1-2小时,可根据业务调整)。

  • 白名单配置:必须对登录接口、注册接口、静态资源(登录页面、CSS、JS)设置白名单,否则会出现“无法访问登录页面”的死循环。

  • 分布式部署适配

    • 使用Session时,需配置Session共享(如Redis存储Session),避免多服务器集群时Session无法共享导致登录失效。

    • 使用Token时,无需额外配置,天然支持分布式(服务器仅需统一秘钥即可校验Token)。

  • Token的撤回问题:Token一旦生成无法主动撤回,可通过“Token黑名单”(如Redis存储失效Token)解决——用户退出登录时,将Token存入黑名单,校验时先判断Token是否在黑名单中。

  • 请求头携带Token规范:前后端分离场景中,建议使用Authorization请求头,格式为“Bearer Token”,便于统一拦截和解析。

  • 异常处理:登录校验失败时(Token过期、无效,Session失效),需返回清晰的提示信息(如“请先登录”“登录已过期,请重新登录”),并引导用户重新登录。

五、总结

登录校验的核心是“身份标识的生成-存储-校验”,其中会话技术负责“生成和存储身份标识”,拦截技术负责“拦截请求并校验身份标识”,二者协同完成安全防护。

选型建议:传统Java Web项目可使用Session+Filter/Interceptor;前后端分离、分布式项目优先使用Token(JWT)+Interceptor,兼顾灵活性和安全性;简单项目可使用Cookie,但需做好安全配置。

实际开发中,登录校验不仅要实现“是否登录”的基础校验,还需结合权限控制(如不同角色访问不同资源),同时做好安全防护,避免出现XSS、CSRF、Token泄露等安全问题,保障系统和用户数据安全。