11. 后端Web实战:登录认证(令牌 + 过滤器 + 拦截器)

后端Web实战:登录认证(令牌 + 过滤器 + 拦截器)

一、核心目标

实现Tlias智能学习辅助系统的登录认证功能,要求:

  • 用户名/密码错误时,禁止登录
  • 登录成功后可访问系统所有功能
  • 未登录时,强制跳转至登录页面,无法直接访问后台资源

二、登录功能实现

2.1 需求说明

  • 登录界面输入用户名、密码,点击"登录"发起POST请求
  • 服务端校验 credentials 合法性,合法则返回用户信息+令牌,前端跳转首页;否则返回错误提示

2.2 接口规范

详情
请求路径 /login
请求方式 POST
请求参数 JSON格式:{"username":"xxx","password":"xxx"}
响应数据 JSON格式,示例如下:
json 复制代码
{
    "code": 1,
    "msg": "success",
    "data": {
        "id": 1,
        "username": "songjiang",
        "name": "宋江",
        "token": "..."
    }
}

2.3 数据库与实体类

2.3.1 数据库表(emp)
sql 复制代码
create table emp (
    id          int unsigned primary key auto_increment comment 'ID,主键',
    username    varchar(20)                  not null comment '用户名',
    password    varchar(32) default '123456' not null comment '密码',
    name        varchar(10)                  not null comment '姓名',
    gender      tinyint unsigned             not null comment '性别, 1:男, 2:女',
    phone       char(11)                     not null comment '手机号',
    job         tinyint unsigned             null comment '职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师',
    salary      int unsigned                 null comment '薪资',
    image       varchar(300)                 null comment '头像',
    entry_date  date                         null comment '入职日期',
    dept_id     int unsigned                 null comment '关联的部门ID',
    create_time datetime                     null comment '创建时间',
    update_time datetime                     null comment '修改时间',
    constraint emp_pk unique (phone),
    constraint username unique (username)
) comment '员工表';
2.3.2 实体类
  • 核心实体类 Emp 已存在
  • 登录结果封装类 LoginInfo
java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
    private Integer id;       // 员工ID
    private String username;  // 用户名
    private String name;      // 姓名
    private String token;     // 令牌(后续JWT用)
}

2.4 功能开发步骤

2.4.1 控制器(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);
        return loginInfo != null ? Result.success(loginInfo) : Result.error("用户名或密码错误~");
    }
}
2.4.2 服务层(EmpService)
  • 接口方法:
java 复制代码
/** 登录校验 */
LoginInfo login(Emp emp);
  • 实现类(EmpServiceImpl):
java 复制代码
@Override
public LoginInfo login(Emp emp) {
    // 调用Mapper查询匹配的员工
    Emp empLogin = empMapper.getUsernameAndPassword(emp);
    if(empLogin != null){
        // 暂存token为null,后续JWT集成时补充
        return new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), null);
    }
    return null;
}
2.4.3 持久层(EmpMapper)
java 复制代码
/** 根据用户名和密码查询员工 */
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getUsernameAndPassword(Emp emp);

2.5 测试验证

  1. 启动服务,通过Apifox发起POST请求 http://localhost:8080/login
  2. 输入合法用户名(如songjiang)、密码(123456),返回code=1及用户信息
  3. 前后端联调:登录成功跳转首页,但存在漏洞------未登录时直接输入 http://localhost:90 仍可访问后台(需通过"登录校验"修复)

三、登录校验实现(核心)

3.1 问题本质

HTTP协议是无状态协议,每次请求独立,服务端无法识别"当前请求是否来自已登录用户",需通过"会话跟踪+统一拦截"解决。

3.2 实现思路

  1. 登录成功后,生成"登录标记"(令牌),返回给前端存储
  2. 前端后续所有请求携带该标记
  3. 服务端通过"统一拦截技术"校验标记合法性:合法则放行,非法则返回401(未授权)

3.3 会话跟踪技术(3种方案对比)

3.3.1 方案一:Cookie(客户端存储)
  • 原理:服务端通过响应头 Set-Cookie 存储用户信息,前端后续请求通过请求头 Cookie 自动携带
  • 代码示例:
java 复制代码
// 设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
    response.addCookie(new Cookie("login_username","itheima"));
    return Result.success();
}

// 获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
    for (Cookie cookie : request.getCookies()) {
        if("login_username".equals(cookie.getName())){
            System.out.println("当前登录用户:"+cookie.getValue());
        }
    }
    return Result.success();
}
  • 优缺点:
    • 优点:HTTP原生支持,无需手动处理存储/携带
    • 缺点:移动端APP不支持、用户可禁用、无法跨域(协议/IP/端口不同即为跨域)
3.3.2 方案二:Session(服务端存储)
  • 原理:基于Cookie实现,服务端创建Session对象(含唯一ID),通过 JSESSIONID Cookie返回前端,后续请求携带该ID查询Session
  • 代码示例:
java 复制代码
// 存储Session
@GetMapping("/s1")
public Result session1(HttpSession session){
    session.setAttribute("loginUser", "tom");
    return Result.success();
}

// 获取Session
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
    HttpSession session = request.getSession();
    Object loginUser = session.getAttribute("loginUser");
    System.out.println("登录用户:"+loginUser);
    return Result.success(loginUser);
}
  • 优缺点:
    • 优点:数据存储在服务端,安全
    • 缺点:依赖Cookie、集群环境下无法直接使用(Session存储在单个服务器)、移动端不支持
3.3.3 方案三:令牌技术(主流方案)
  • 原理:登录成功后服务端生成随机字符串(令牌),前端存储(localStorage),后续请求通过请求头携带,服务端校验令牌合法性
  • 优缺点:
    • 优点:支持PC/移动端、集群环境友好、不依赖Cookie、减轻服务端存储压力
    • 缺点:需手动实现令牌生成、携带、校验(推荐使用JWT标准)

3.4 JWT令牌实现(令牌技术的标准化)

3.4.1 JWT简介
  • 全称:JSON Web Token,由3部分组成(用.分隔):
    1. Header:令牌类型+签名算法(如 {"alg":"HS256","type":"JWT"}
    2. Payload:自定义数据(如用户ID、用户名)+ 过期时间
    3. Signature:Header+Payload+秘钥通过签名算法计算得出,防篡改
  • 特点:Base64编码(可解码)+ 数字签名(防篡改),简洁自包含
3.4.2 JWT依赖引入
xml 复制代码
<!-- JWT依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
3.4.3 JWT工具类(JwtUtils)
java 复制代码
package com.itheima.util;

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

public class JwtUtils {
    private static String signKey = "SVRIRUlNQQ=="; // 签名秘钥(需保密)
    private static Long expire = 43200000L;        // 过期时间(12小时)

    /** 生成JWT令牌 */
    public static String generateJwt(Map<String,Object> claims){
        return Jwts.builder()
                .addClaims(claims) // 自定义负载
                .signWith(SignatureAlgorithm.HS256, signKey) // 签名算法+秘钥
                .setExpiration(new Date(System.currentTimeMillis() + expire)) // 过期时间
                .compact();
    }

    /** 解析JWT令牌 */
    public static Claims parseJWT(String jwt){
        return Jwts.parser()
                .setSigningKey(signKey) // 签名秘钥需与生成时一致
                .parseClaimsJws(jwt)
                .getBody();
    }
}
3.4.4 集成JWT到登录功能

修改 EmpServiceImpllogin 方法,生成JWT令牌:

java 复制代码
@Override
public LoginInfo login(Emp emp) {
    Emp empLogin = empMapper.getUsernameAndPassword(emp);
    if(empLogin != null){
        // 封装JWT负载数据
        Map<String,Object> dataMap = new HashMap<>();
        dataMap.put("id", empLogin.getId());
        dataMap.put("username", empLogin.getUsername());
        
        // 生成JWT令牌
        String jwt = JwtUtils.generateJwt(dataMap);
        return new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt);
    }
    return null;
}

3.5 统一拦截技术实现(校验令牌)

3.5.1 方案一:过滤器(Filter)
3.5.1.1 Filter简介
  • JavaWeb三大组件之一,拦截所有资源请求(Servlet、静态资源等)
  • 核心方法:doFilter(拦截请求时执行),需手动调用 chain.doFilter() 放行
3.5.1.2 登录校验Filter实现
java 复制代码
package com.itheima.filter;

import com.itheima.util.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.util.StringUtils;
import java.io.IOException;

@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. 排除登录请求(无需校验令牌)
        String url = request.getRequestURL().toString();
        if(url.contains("login")){
            log.info("登录请求,直接放行:{}", url);
            chain.doFilter(request, response);
            return;
        }

        // 2. 获取请求头中的令牌
        String jwt = request.getHeader("token");

        // 3. 令牌为空,返回401
        if(!StringUtils.hasLength(jwt)){
            log.info("令牌为空,拒绝访问");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return;
        }

        // 4. 解析令牌失败(篡改/过期),返回401
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            log.info("令牌非法,拒绝访问:{}", e.getMessage());
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return;
        }

        // 5. 令牌合法,放行
        log.info("令牌合法,放行请求:{}", url);
        chain.doFilter(request, response);
    }
}
3.5.1.3 开启Servlet组件支持(启动类)
java 复制代码
@ServletComponentScan // 开启Servlet组件(Filter、Servlet、Listener)支持
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
}
3.5.2 方案二:拦截器(Interceptor)
3.5.2.1 Interceptor简介
  • Spring框架提供,仅拦截Spring环境中的Controller方法
  • 核心方法:preHandle(Controller方法执行前)、postHandle(执行后)、afterCompletion(视图渲染后)
3.5.2.2 登录校验Interceptor实现
java 复制代码
package com.itheima.interceptor;

import com.itheima.util.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 排除登录请求
        String url = request.getRequestURL().toString();
        if(url.contains("login")){
            log.info("登录请求,直接放行:{}", url);
            return true;
        }

        // 2. 获取令牌
        String jwt = request.getHeader("token");

        // 3. 令牌为空,返回401
        if(!StringUtils.hasLength(jwt)){
            log.info("令牌为空,拒绝访问");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }

        // 4. 解析令牌失败,返回401
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            log.info("令牌非法,拒绝访问:{}", e.getMessage());
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }

        // 5. 令牌合法,放行
        log.info("令牌合法,放行请求:{}", url);
        return true;
    }
}
3.5.2.3 注册Interceptor(配置类)
java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/login"); // 排除登录请求
    }
}
3.5.3 Filter与Interceptor区别
对比维度 Filter Interceptor
接口规范 实现Filter接口 实现HandlerInterceptor接口
拦截范围 所有资源(Servlet、静态资源等) 仅Controller方法
依赖环境 JavaWeb标准,无框架依赖 依赖Spring框架
执行时机 Tomcat接收请求后,DispatcherServlet前 DispatcherServlet后,Controller前

四、核心总结

  1. 登录功能:基于SpringBoot+MyBatis实现credentials校验,登录成功下发JWT令牌
  2. 登录校验核心:通过"JWT令牌(会话跟踪)+ Filter/Interceptor(统一拦截)"解决HTTP无状态问题
  3. JWT关键:签名秘钥需保密,生成与校验秘钥必须一致,令牌含过期时间防永久有效
  4. 拦截技术选择:需拦截所有资源用Filter,仅拦截接口用Interceptor,根据场景灵活选择

五、测试验证

  1. 未登录访问 http://localhost:90:跳转登录页面(响应401)
  2. 输入合法用户名/密码登录:跳转首页,可正常访问部门管理、员工管理等功能
  3. 篡改令牌后请求:返回401,无法访问后台资源
相关推荐
阿珊和她的猫3 分钟前
页面停留时长埋点实现技术详解
开发语言·前端·javascript·ecmascript
chilavert3186 分钟前
技术演进中的开发沉思-275 AJax : Slider
前端·javascript·ajax·交互
梦65012 分钟前
基于 Vue3 + TypeScript 封装 UEditor 富文本编辑器组件
前端·vue.js·typescript
沛沛老爹12 分钟前
从Web开发到AI应用——用FastGPT构建实时问答系统
前端·人工智能·langchain·rag·advanced-rag
锥锋骚年13 分钟前
Vue 3 Vben Admin 框架的Mention提及组件
前端·javascript·vue.js
QT 小鲜肉13 分钟前
【Linux命令大全】001.文件管理之mlabel命令(实操篇)
linux·运维·服务器·前端·笔记
七月巫山晴14 分钟前
【iOS】OC中的一些宏
前端·ios·objective-c
elangyipi12315 分钟前
从嵌套依赖到符号链接:4款主流npm包管理器的架构演进与深度对比
前端·架构·npm
武帝为此21 分钟前
【Shell脚本函数介绍】
前端·chrome
谢尔登9 小时前
Monorepo 架构
前端·arcgis·架构