后端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 测试验证
- 启动服务,通过Apifox发起POST请求
http://localhost:8080/login - 输入合法用户名(如songjiang)、密码(123456),返回code=1及用户信息
- 前后端联调:登录成功跳转首页,但存在漏洞------未登录时直接输入
http://localhost:90仍可访问后台(需通过"登录校验"修复)
三、登录校验实现(核心)
3.1 问题本质
HTTP协议是无状态协议,每次请求独立,服务端无法识别"当前请求是否来自已登录用户",需通过"会话跟踪+统一拦截"解决。
3.2 实现思路
- 登录成功后,生成"登录标记"(令牌),返回给前端存储
- 前端后续所有请求携带该标记
- 服务端通过"统一拦截技术"校验标记合法性:合法则放行,非法则返回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),通过
JSESSIONIDCookie返回前端,后续请求携带该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部分组成(用
.分隔):- Header:令牌类型+签名算法(如
{"alg":"HS256","type":"JWT"}) - Payload:自定义数据(如用户ID、用户名)+ 过期时间
- Signature:Header+Payload+秘钥通过签名算法计算得出,防篡改
- Header:令牌类型+签名算法(如
- 特点: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到登录功能
修改 EmpServiceImpl 的 login 方法,生成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前 |
四、核心总结
- 登录功能:基于SpringBoot+MyBatis实现credentials校验,登录成功下发JWT令牌
- 登录校验核心:通过"JWT令牌(会话跟踪)+ Filter/Interceptor(统一拦截)"解决HTTP无状态问题
- JWT关键:签名秘钥需保密,生成与校验秘钥必须一致,令牌含过期时间防永久有效
- 拦截技术选择:需拦截所有资源用Filter,仅拦截接口用Interceptor,根据场景灵活选择
五、测试验证
- 未登录访问
http://localhost:90:跳转登录页面(响应401) - 输入合法用户名/密码登录:跳转首页,可正常访问部门管理、员工管理等功能
- 篡改令牌后请求:返回401,无法访问后台资源