菜鸟之路Day39一一登录
作者:blue
时间:2025.6.4
0.概述
文章内容学习自黑马程序员BV1m84y1w7Tb
1.登录功能
登录功能可以抽象为以用户的username和password为查询条件,查询用户信息,看是否有匹配的用户,如果有,则登录成功,如果没有则登录失败
LoginController
@RestController
@Slf4j
public class LoginController {
@Autowired
EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("用户登录{}",emp);
Emp loginemp = empService.login(emp);
if(loginemp != null){
return Result.success();
}
else return Result.error("用户名或密码错误");
}
}
EmpService
@Override
public Emp login(Emp emp) {
return empMapper.login(emp);
}
Mapper
@Select("select * from emp where username = #{username} and password = #{password}")
Emp login(Emp emp);
2.登录校验

2.1会话技术
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
会话跟踪方案:
客户端会话跟踪技术:Cookie
服务端会话跟踪技术:Session
令牌技术

2.2cookie技术
Cookie 是由服务器生成、存储在客户端(浏览器)的小型文本文件,用于跟踪用户状态、存储用户信息(如登录凭证、偏好设置等)。它通过HTTP 请求和响应头在客户端与服务器之间传递,实现状态管理。
Cookie 的生命周期主要包括 创建、发送、存储、更新、删除 五个阶段

1.服务器创建 Cookie 并发送给客户端
- 触发场景:用户首次访问网站、登录 / 注册、提交表单等需要标识用户的操作。
- 技术实现:
- 服务器在响应中通过
Set-Cookie响应头 向浏览器发送 Cookie 信息
- 服务器在响应中通过
2.客户端存储 Cookie
- 浏览器接收到Set-Cookie响应后,会将 Cookie 存储在本地文件或内存中,具体规则如下:
- 会话级 Cookie(非持久化):未设置
Expires/Max-Age,仅在浏览器会话期间有效,关闭浏览器后删除。 - 持久化 Cookie:设置了过期时间,存储在本地硬盘,到期后自动删除
- 会话级 Cookie(非持久化):未设置
3.客户端在请求中携带 Cookie
当浏览器再次访问同一域名下符合 Path 规则的资源时,会自动在
Cookie请求头 中携带已存储的 Cookie 信息。作用:服务器通过解析 Cookie 识别用户身份,实现状态保持(如保持登录状态)、个性化设置(如语言偏好)等功能。
4.服务器更新或删除 Cookie
(1)更新 Cookie
- 场景:用户信息变更(如修改密码、切换语言)。
- 实现方式:
- 服务器再次通过
Set-Cookie响应头返回同名 Cookie,覆盖旧值(需确保Domain和Path与旧 Cookie 一致)。
- 服务器再次通过
(2)删除 Cookie
- 场景:用户注销、清除历史记录。
- 实现方式:
- 服务器返回同名 Cookie,设置
Expires为过去的时间 或Max-Age=0,浏览器会立即删除该 Cookie。
- 服务器返回同名 Cookie,设置
5.Cookie 的过期与自动删除
- 持久化 Cookie 到达
Expires时间或Max-Age计时结束后,会被浏览器自动删除。 - 会话级 Cookie 在浏览器关闭时自动清除。
Cookie的优缺点
优点:HTTP协议中支持的技术
缺点:
移动端APP无法使用Cookie
不安全,用户可以自己禁用Cookie
Cookie不能跨域
2.3Session技术

Session(会话) 是服务器端用于跟踪用户状态的机制,通过存储用户数据(如登录信息、购物车内容等)实现跨请求的状态保持。与 Cookie 不同,Session 数据存储在服务器端,仅通过 会话 ID(Session ID) 与客户端通信,安全性更高。
Session 的生命周期包括 创建、标识、数据存储、更新、销毁 五个阶段
- 服务器创建 Session 并生成会话 ID
- 触发场景:用户首次访问需要会话管理的页面(如登录页、购物车页)。
- 技术实现:
- 服务器通过编程语言(如 Java Servlet、Python Flask、Node.js Express 等)的会话管理模块创建 Session 对象。
- 生成唯一的 会话 ID(Session ID)(通常为随机字符串,如
sess_abc123),作为客户端与服务器会话的关联标识。
- 服务器将会话 ID 传递给客户端
- 会话 ID 需要通过 Cookie 或 URL 重写 等方式传递给浏览器,以便后续请求识别用户会话。
- 客户端在请求中携带会话 ID
浏览器收到会话 ID 后,会在后续同域名请求中自动通过
Cookie请求头 或 URL 参数 携带会话 ID。服务器解析逻辑:
- 从请求中提取会话 ID,查找对应的 Session 对象(通常存储在内存、数据库或缓存中)。
- 若找到有效 Session,则恢复用户状态;若未找到或 Session 过期,则创建新 Session。
- 服务器操作 Session 数据
- 服务器通过会话 ID 找到对应的 Session 对象后,可对其进行 读写、更新或删除 操作。
- Session 的过期与销毁
- 过期机制:
- 闲置超时:若 Session 在指定时间(如 30 分钟)内未被访问,自动过期(由服务器配置,如 Tomcat 默认 30 分钟)。
- 绝对超时:设置 Session 的绝对过期时间(较少使用)。
- 主动销毁:
- 用户注销时,服务器调用
session.invalidate()方法立即销毁 Session。 - 服务器重启或定期清理过期 Session(释放内存 / 存储空间)。
- 用户注销时,服务器调用
- 客户端影响:
- Session 过期或销毁后,客户端携带的会话 ID 变为无效,下次请求时服务器将创建新 Session。
2.4JWT令牌
JWT全称:JSON Web Token
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
组成:
◆第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256","type":"JWT"}
◆第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom")
◆第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
应用场景:登录认证
①登录成功后,生成令牌
②后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
2.4.1JWT令牌生成
导入相关依赖
<!--JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成JWT令牌
/*
* 生成JWT
* */
@Test
public void testGenJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("name","test");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256,"bluening")//设置签名算法和密钥
.setClaims(claims)//自定义内容
.setExpiration(new Date(System.currentTimeMillis()+3600*1000))//设置有效期为一个小时
.compact();//生成JWT
System.out.println(jwt);
}
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidGVzdCIsImlkIjoxLCJleHAiOjE3NDkxMTE4OTR9.sWaLc8jLMlM_H-FBGHPpqAsH0tt3UAqLC4NkhKNPa7Q
2.4.2JWT令牌校验
/*
* JWT校验
* */
@Test
public void praseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("bluening")//指定签名密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidGVzdCIsImlkIjoxLCJleHAiOjE3NDkxMTE4OTR9.sWaLc8jLMlM_H-FBGHPpqAsH0tt3UAqLC4NkhKNPa7Q")//解析令牌
.getBody();
System.out.println(claims);
}
解析结果:
{
name=test, id=1, exp=1749111894}
注意事项:
JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
如果JWT令牌解析校验时报错,则说明JWT令牌被篡改 或 失效了,令牌非法
2.5改造LoginController
我们需要在用户第一次登录成功后为用户发放jwt令牌。
将原本的LoginController改造为以下形式,在这里我们使用jwt工具类中的静态方法来生成jwt令牌,该方法生成令牌的方式,与以上介绍的方式相同
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("用户登录{}",emp);
Emp loginemp = empService.login(emp);
if(loginemp != null){
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginemp.getId());
claims.put("name", loginemp.getName());
claims.put("username", loginemp.getUsername());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
return Result.error("用户名或密码错误");
}

2.6Filter过滤器
概念:Filter 过滤器,是JavaWeb 三大组件(Servlet、Filter、Listener)之一
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

Filter快速入门
定义Filter:定义一个类,实现 Filter接口,并重写其所有方法。
配置Filter:Filter类上加 @Webfilter 注解,配置拦截资源的路径。引导类上加 @servletComponentscan 开启Servlet组件支持。
package com.bluening.talis_web_demo.Filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override
//初始化方法,Web服务器启动,创建Filter时调用,只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
//拦截到请求时,调用该方法,可调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("拦截方法执行,拦截到了请求...");
//放行
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
//销毁方法,服务器关闭时调用,只调用一次
public void destroy() {
Filter.super.destroy();
}
}
2.6.1Filter执行流程

放行后访问对应资源,资源访问完成后,还会回到Filter中吗? 会
如果回到Filter中,是重新执行还是执行放行后的逻辑呢? 执行放行后逻辑
2.6.2Filter拦截路径

2.6.4过滤器链
介绍: 一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
顺序: 注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。

2.7利用Filter实现登录校验
对所有请求进行拦截,拦截到后,所有请求都需要校验其令牌吗?
答:不是的,有一个请求例外,登录请求,因为登录时是没有发放令牌的
拦截到请求后,什么情况下才可以放行,执行业务操作?
答:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果
登录校验Filter-流程
①获取请求url
②判断请求url中是否包含login,如果包含,说明是登录操作,放行
③获取请求头中的令牌(token)
④判断令牌是否存在,如果不存在,返回错误结果(未登录)
⑤解析token,如果解析失败,返回错误结果(未登录)
⑥放行

相关依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
具体实现
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
//1.获取请求url
String url = req.getRequestURI();
log.info("请求的url:{}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("login")){
log.info("登录操作,放行");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//3.获取请求头中的令牌(token)
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录信息");
Result error = Result.error("NOT_LOGIN");
//手动转换,对象--json ------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
}
catch (Exception e){
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//6.放行
log.info("令牌合法,放行");
filterChain.doFilter(servletRequest, servletResponse);
}
}
2.8拦截器(Interceptor)
概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码

Interceptor快速入门
1.定义拦截器,实现HandlerInterceptor接口,并重写其中所有方法
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override//目标资源方法执行前执行,返回true;放行;返回false:不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
@Override//目标资源方法执行后执行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
@Override//视图渲染完毕后执行,最后执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
2.注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
2.8.1拦截器拦截路径
拦截器可以根据需求,配置不同的拦截路径


拦截器执行流程
接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
2.8.2利用拦截器实现登录校验
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override//目标资源方法执行前执行,返回true;放行;返回false:不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url
String url = req.getRequestURI();
log.info("请求的url:{}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("login")){
log.info("登录操作,放行");
return true;
}
//3.获取请求头中的令牌(token)
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录信息");
Result error = Result.error("NOT_LOGIN");
//手动转换,对象--json ------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
}
catch (Exception e){
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//6.放行
log.info("令牌合法,放行");
return true;
}
@Override//目标资源方法执行后执行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
@Override//视图渲染完毕后执行,最后执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
3.异常处理
平时当系统与服务器交互时出现的异常,会被抛到Controller层,然后再由Controller层抛到框架中,由框架返回一个错误响应
类似于这样:
{
"timestamp": "2025-06-05T12:52:44.062+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/depts"
}
但是这种错误响应是不符合我们和前端约定好的前后端交互统一响应结果Result,所以前端无法识别这个错误响应,无法给出用户对应的提示,所以我们要捕获这些异常的信息,然后将这些异常信息,以统一的Result形式返回给前端。
这时我们就可以定义一个全局异常处理器,作为处理异常的方案。

/*
* 全局异常处理器
* */
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)//捕获所有异常
public Result exception(Exception e) {
e.printStackTrace();
return Result.error("对不起操作失败,请联系管理员");
}
}
出现错误后后端返回给前端的数据
{
"code":0,"msg":"对不起操作失败,请联系管理员","data":null}
