Day12 后端Web实战:登录认证


图1 登录需求
目录

1. 登录功能

思路分析

图2 思路分析

怎么样才算登录成功了呢?

登录功能的本质是什么?

员工登录-思路

图3 员工登录-思路
cn/zjy/pojo/LoginInfo.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
    private Integer id;
    private String username;
    private String name;
    private String token;
}
cn/zjy/controller/LoginController.java
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;
    /**
     * 登录
     */
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        log.info("登录:{}",emp);
        LoginInfo info= empService.login(emp);
        if (info != null) {
            return Result.success(info);
        }
        return Result.error("用户名或密码错误");
    }
}
cn/zjy/service/EmpService.java
    LoginInfo login(Emp emp);
cn/zjy/service/impl/EmpServiceImpl.java
    @Override
    public LoginInfo login(Emp emp) {
        //1.调用mapper接口,根据用户名和密码查询员工信息
        Emp e=empMapper.selectByUsernameAndPassword(emp);

        //2,判断:判断是否存在这个员工,如果存在,组装登录成功信息
        if(e != null) {
            log.info("登录成功,员工信息:{}", e);
            return new LoginInfo(e.getId(), e.getUsername(), e.getName(), "");
        }

        //3.不存在,返回null
        return null;
    }
cn/zjy/mapper/EmpMapper.java
    @Select("select id,name,username from emp where username=#{username} and password=#{password}")
    Emp selectByUsernameAndPassword(Emp emp);
图4 登录验证1
图5 登录验证2

登录校验需求

图6 登录校验需求

2. 登录校验

登录校验思路

图7 登录校验思路

2.1 会话技术

图8 会话
图9 会话跟踪

会话跟踪方案对比

图10 客户端存储会话跟踪技术:Cookie
cn/zjy/controller/SessionController.java
package cn.zjy.controller;

import cn.zjy.pojo.Result;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * HttpSession演示
 */
@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","DuLaoshi")); //设置Cookie/响应Cookie
        return Result.success();
    }

    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("login_username")){
                System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
            }
        }
        return Result.success();
    }

}
图11 cookie测试验证
图12 cookie小结

②服务器端存储会话跟踪技术:session

图13 客户端存储会话跟踪技术:session
cn/zjy/controller/SessionController.java

    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession-s1: {}", session.hashCode());

        session.setAttribute("loginUser", "seesion 张婧怡"); //往session中存储数据
        return Result.success();
    }

    @GetMapping("/s2")
    public Result session2(HttpSession session){
        log.info("HttpSession-s2: {}", session.hashCode());

        Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
        log.info("loginUser: {}", loginUser);
        return Result.success(loginUser);
    }
图14 session测试结果验证
图15 Session小结

③服务器端存储会话跟踪技术:令牌方案

令牌(Token)临时身份钥匙----临时身份证(工牌),具有唯一性、临时性、防篡改性

http会话中的令牌

HTTP 是无状态协议(每次请求都是独立的,服务器记不住上一次的交互),而 “令牌(Token)” 就是解决这个问题的核心工具 —— 它像一把 “临时身份钥匙”,让服务器能识别重复访问的客户端,维持会话(比如登录后保持登录状态)。

令牌的核心逻辑(3 步看懂):

  1. 发令牌:用户首次登录(或需要验证时),客户端提交账号密码等信息;服务器验证通过后,生成一个唯一的随机字符串(即令牌),并关联用户身份(比如 “令牌 A 对应用户小明”),然后把令牌返回给客户端。
  2. 存令牌:客户端收到令牌后,会存在本地(常见方式:Cookie、LocalStorage 等)。
  3. 用令牌:之后客户端再发请求(比如逛个人主页、下单),会自动带上这个令牌(比如放在请求头、参数里);服务器收到后,查 “令牌 - 用户” 关联表,确认令牌有效→识别出用户身份,直接处理请求(不用再让用户重复登录)。

关键特点:

图16 http会话中的令牌
图17 令牌方案小结

2.2 JWT令牌

图18 JWT令牌

JWT令牌-介绍

图19 JWT令牌-介绍

JWT令牌-生成/解析

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
src/test/java/cn/zjy/JwtTest.java 主要代码
/**
* 生成JwT令牌-Jwts.builder()
*/
@Test
public void testGenJwt() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 10);
    claims.put("username", "张婧怡");
    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "SVRIRUlNQQ==")
        //"SVRIRUlNQQ==" 是itheima的base64编码 密钥
        .addClaims(claims)
        .setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) //设置1小时后过期
        .compact();
    System.out.println(jwt);
}

@Test
public void testParseJwt() throws Exception {
    String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..换成上面生成的字符串.";
    Claims claims = Jwts.parser()
        .setSigningKey("SVRIRUlNQQ==")  //指定密钥
        .parseClaimsJws(jwtToken)  //解析令牌
        .getBody();  //获取自定义信息 -- 载荷
    System.out.println(claims);
}

http://base64.us

图20 jwt测试1
图21 Jwt测试2

小结

JWT令牌由哪几个部分组成 ,每个部分都存储什么内容?

JWT令牌生成及校验?

JWT令牌解析(校验)时什么情况会报错?

注意事项:JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的

图22 小结

案例:登录成功后-生成令牌

通义千文提问词
请帮我基于如下单元测试方法,改造成一个JWT令牌操作的工具类,类名:JwtUtils,具体要求如下:
1.工具类中有两个方法,一个方法生成令牌,另一个是解析令牌。
2.生成令牌时使用的秘钥,和测试类中的一致即可。
3.令牌的过期时间设置12小时。
原始的测试类的代码如下:

 @Test
    public void testGenJwt() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 10);
        claims.put("username", "张婧怡");
        String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "SVRIRUlNQQ==")
                //"SVRIRUlNQQ==" 是itheima的base64编码 密钥
                .addClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 100*1000)) //设置100秒后过期
                .compact();
        System.out.println(jwt);
    }
    @Test
    public void testParseJwt() throws Exception {
        String jwtToken = "eyJhbGciOiJIUzI1NiJ9"
                +".eyJpZCI6MTAsInVzZXJuYW1lIjoi5p2c6ICB5biIIiwiZXhwIjoxNzU3MTYyODc2fQ"
                +".aFy7pbnynULwxaILyOygUspkZjRENmMSod_S8d-RL9o";
        Claims claims = Jwts.parser()
                .setSigningKey("SVRIRUlNQQ==")
                .parseClaimsJws(jwtToken)
                .getBody();
        System.out.println(claims);
    }

tongyi.aliyun.com/qianwen
cn/zjy/utils/JwtUtils.java
package cn.zjy.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

/**
 * JWT 工具类,用于生成和解析 JWT 令牌
 */
public class JwtUtils {

    // 密钥:使用与测试类中一致的 "SVRIRUlNQQ=="("itheima" 的 Base64 编码)
    private static final String SECRET_KEY = "SVRIRUlNQQ==";

    // 令牌过期时间:1 小时(单位:毫秒)
    private static final long EXPIRATION = 60 * 60 * 1000; // 1小时

    /**
     * 生成 JWT 令牌
     *
     * @param claims 要包含在令牌中的自定义声明(如用户ID、用户名等)
     * @return JWT 令牌字符串
     */
    public static String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims) // 添加自定义声明
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用 HS256 算法签名
                .compact();
    }

    /**
     * 解析 JWT 令牌
     *
     * @param token JWT 令牌字符串
     * @return 解析后的 Claims 对象,包含令牌中的信息
     * @throws io.jsonwebtoken.JwtException 如果令牌无效或签名不匹配
     */
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY) // 使用相同的密钥解析
                .parseClaimsJws(token)    // 解析并验证 JWT
                .getBody();               // 获取 payload 中的 Claims
    }
}
cn/zjy/service/impl/EmpServiceImpl.java 修改public LoginInfo login(Emp emp)方法 if代码
        if(e != null) {
            log.info("登录成功,员工信息:{}", e);

            //生成JWT令牌
            Map<String,Object> claims =new HashMap<>();
            claims.put("id", e.getId());
            claims.put("username",e.getUsername());
            claims.put("name",e.getName());
            String jwt= JwtUtils.generateToken(claims);
            return new LoginInfo(e.getId(),e.getUsername(), e.getName(),jwt);
        }
图23 登录成功后-生成令牌验证
图24 登录成功后-生成令牌验证2

2.3 过滤器Filter

图25 校验:统一拦截

①快速入门

图26 过滤器Filter

Filter快速入门

cn/zjy/filter/DemoFilter.java
package cn.zjy.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("初始化方法 init ...");
    }

    @Override
    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {
        log.info("拦截到了请求... 放行前...");
        chain.doFilter(servletRequest, servletResponse);
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        log.info("销毁方法 destroy ... ");
    }
}
cn/zjy/TliasWebManagementApplication.java 添加注解
@ServletComponentScan
@SpringBootApplication
public class TliasManagementApplication {
...
图27 Filter入门程序验证1mark>
图28 Filter入门程序验证2mark>
图29 Filter小结

②令牌校验Filter

问题提出

图30 问题提出

令牌校验Filter--流程

图31 令牌校验Filter--流程
cn/zjy/filter/TokenFilter.java
package cn.zjy.filter;
import cn.zjy.utils.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 java.io.IOException;

@Slf4j
@WebFilter("/*")
public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1.获取到请求路径
        String path = request.getRequestURI();

        //2.判断是否是登录请求,如果路径中末尾/login,说明是登录操作,放行
        if(path.endsWith("/login")) {
            log.info("登录操作,放行");
            filterChain.doFilter(request,response);
            return;
        }

        //3.获取请求头中的token
        String token = request.getHeader("token");

        //4.判断token是否存在,如果不存在,说明用户没有登录,返回错误信息(响应401状态码)
        if(token == null || token.isEmpty()) {
            log.info("令牌为空,响应401");
            response.setStatus(401);
            return;
        }

        //5.如果token存在,校验令牌,如果校验失败->返回错误信息(响应401状态码)
        try {
            JwtUtils.parseToken( token);
        } catch (Exception e) {
            log.info("令牌非法,响应401");
            response.setStatus(401);
            return;
        }

        //6.校验通过,放行
        log.info("令牌合法,放行");
        filterChain.doFilter(request,response);

    }
}

测试

1.登录并获取令牌

2.不带令牌查询性别统计

3.带篡改令牌查询性别统计

4.带正确令牌查询性别统计

图32 令牌校验Filter测试

③详解(执行流程、拦截路径、过滤器链)

执行流程

图33 Filter执行流程

问题1:放行后访问对应资源,资源访问完成后,还会回到Filter中吗?会

问题2:如果回到Filter中,是重新执行还是执行放行后的逻辑呢?执行放行后逻辑

// 注释TokenFilter的@WebFilter,放开DemoFilter的@WebFilter并修改其doFilter方法
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {
        log.info("拦截到了请求... 放行前...");
        chain.doFilter(servletRequest, servletResponse);
        log.info("拦截到了请求... 放行后...");
    }
图34 执行流程验证

拦截路径

Filter 可以根据需求,配置不同的拦截资源路径:@WebFilter("/*")

拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

过滤器链

图35 过滤器链
将DemoFilter.java复制为AbcFilter.java;将AbcFilter改为XyzFilter测试
图36 过滤器链验证

小结

过滤器的执行流程 ?

配置的过滤器的拦截路径/ 与 /emps/ 分别代表什么意思 ?

什么是过滤器链 ?

图37 Filter小结

2.4 拦截器Interceptor

①快速入门

图38 拦截器

Interceptor快速入门

图39 拦截器定义与注册
cn/zjy/interceptor/DemoInterceptor.java
@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行,返回true:放行,返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        log.info("preHandle... 目标资源方法执行前执行");
        return true;
    }

    //目标资源方法执行后执行
    public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView mv) throws Exception {
        log.info("postHandle... 目标资源方法执行后执行");
    }

    // 视图渲染完毕后执行,最后执行
    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion... 视图渲染完毕后执行,最后执行");
    }
}
cn/zjy/config/WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private DemoInterceptor demoInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
    }
}

在ApiFox中执行:根据ID查询员工信息,控制台显示如下。

图40 根据ID查询员工信息验证

小结

拦截器Interceptor的使用步骤 ?

图41 小结

②令牌校验Interceptor

图 令牌校验流程
cn/zjy/interceptor/TokenInterceptor.java
package cn.zjy.interceptor;

import cn.zjy.utils.JwtUtils;
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;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行,返回true:放行,返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取到请求路径
        String path = request.getRequestURI();

        //2.判断是否是登录请求,如果路径中末尾/login,说明是登录操作,放行
        if(path.endsWith("/login")) {
            log.info("登录操作,放行");
            return true;
        }

        //3.获取请求头中的token
        String token = request.getHeader("token");

        //4.判断token是否存在,如果不存在,说明用户没有登录,返回错误信息(响应401状态码)
        if(token == null || token.isEmpty()) {
            log.info("令牌为空,响应401");
            response.setStatus(401);
            return false;
        }

        //5.如果token存在,校验令牌,如果校验失败->返回错误信息(响应401状态码)
        try {
            JwtUtils.parseToken( token);
        } catch (Exception e) {
            log.info("令牌非法,响应401");
            response.setStatus(401);
            return false;
        }

        //6.校验通过,放行
        log.info("令牌合法,放行");
        return true;
    }
}
cn/zjy/config/WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    //private DemoInterceptor demoInterceptor;
    private TokenInterceptor tokenInterceptor;
    @Override
    public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
        //registry.addInterceptor(demoInterceptor)
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**");  // 拦截所有请求
                //.excludePathPatterns("/login");  // 排除登录接口
    }
}

测试

1.登录并获取令牌

2.不带令牌查询

3.带篡改令牌查询

4.带正确令牌查询

图42 拦截器验证

③详解(拦截路径、执行流程)

拦截路径

拦截器可以根据需求,配置不同的拦截路径:

cn/zjy/config/WebConfig.java
package cn.zjy.config;

import cn.zjy.interceptor.TokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    //private DemoInterceptor demoInterceptor;
    private TokenInterceptor tokenInterceptor;
    @Override
    public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
        //registry.addInterceptor(demoInterceptor)
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")  // 拦截所有请求
                .excludePathPatterns("/login");  // 排除登录接口
    }
}
拦截路径 含义 举例
/* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1
/** 任意级路径 能匹配/depts,/depts/1,/depts/1/2
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1
图43 拦截器--拦截路径

执行流程

Filter 与 Interceptor 区别:

图44 过滤器和拦截器同时运行的执行流程

验证

打开DemoFilter的@WebFilter设置、关闭其他Filter----注释@WebFilter设置;

cn/zjy/config/WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private DemoInterceptor demoInterceptor;
    //private TokenInterceptor tokenInterceptor;
    @Override
    public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
        registry.addInterceptor(demoInterceptor)
        //registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**");  // 拦截所有请求
               // .excludePathPatterns("/login");  // 排除登录接口
    }
}

小结

拦截器中的拦截路径 / 与 /** 的区别是什么 ?

过滤器与拦截器的区别 ?

图46 小结

①②③④⑤⑥⑦⑧⑨⑩


在 HTTP 协议中,与 Cookie 相关的 “三个自动” 通常指浏览器或客户端对 Cookie 的三种自动处理机制,这些机制是实现状态管理的核心基础:

一、自动存储(Automatic Storage)

浏览器在接收到服务器通过Set-Cookie响应头发送的 Cookie 时,会自动将其存储在本地缓存中。这一过程无需用户干预,存储位置根据 Cookie 的属性分为两种:

  1. 内存存储:未设置ExpiresMax-Age的会话 Cookie(Session Cookie),仅在浏览器会话期间存在,关闭浏览器后自动清除。
  2. 磁盘存储:设置了过期时间的持久化 Cookie(Persistent Cookie),会被写入本地硬盘,在指定时间内有效。

例如,当用户访问电商网站时,服务器返回的Set-Cookie: session_id=abc123会被浏览器自动保存,用于识别用户身份。

二、自动携带(Automatic Inclusion)

在后续所有对同一域名的请求中,浏览器会自动在请求头中附加已存储的 Cookie,无需用户手动添加。这一机制确保服务器能够通过 Cookie 识别用户状态,例如:http

GET /cart HTTP/1.1
Host: example.com
Cookie: session_id=abc123; theme=dark

此过程完全透明,用户无需感知,极大简化了状态管理流程。即使跨页面或跨标签页访问,浏览器也会自动同步 Cookie 信息。

三、自动过期(Automatic Expiration)

浏览器会根据 Cookie 的ExpiresMax-Age属性自动处理过期逻辑:

  1. 时间触发:当 Cookie 的Expires时间到达或Max-Age倒计时结束,浏览器会自动删除该 Cookie。
  2. 会话结束:会话 Cookie 在浏览器关闭时自动失效,无需显式清理。

例如,某 Cookie 设置Max-Age=86400(24 小时),浏览器会在 24 小时后自动清除该 Cookie,确保用户数据的时效性和安全性。

扩展机制:自动路径匹配与域限制

除上述核心机制外,浏览器还会自动执行以下策略:

  1. 路径匹配:仅在请求路径与 Cookie 的Path属性匹配时携带该 Cookie。例如,Path=/login的 Cookie 仅在访问/login路径时发送。
  2. 域限制:Cookie 仅在同源请求中自动携带,跨域请求(如example.comapi.example.com)需通过Domain属性显式授权。

实际应用场景

  1. 电商购物车:用户添加商品后,服务器通过 Cookie 记录购物车内容,后续访问结算页面时,浏览器自动携带 Cookie,确保购物车数据不丢失。
  2. 自动登录:用户勾选 “记住我” 后,服务器返回长期有效的 Cookie,浏览器自动存储并在下次访问时携带,实现免密登录。
  3. 个性化推荐:服务器通过 Cookie 记录用户浏览偏好,后续请求时自动返回定制化内容。

技术实现原理

  1. Cookie 管理器:浏览器内部的 Cookie 管理器(如 JMeter 中的HTTP Cookie Manager)负责统一管理存储、携带和过期逻辑。
  2. 属性控制:服务器通过Secure(仅 HTTPS 传输)、HttpOnly(防止 XSS 攻击)等属性进一步约束 Cookie 的自动处理行为。

与 HTTP 协议的协同

Cookie 的自动机制与 HTTP 的无状态性形成互补:

注意事项

  1. 安全风险:自动携带 Cookie 可能导致跨站请求伪造(CSRF)攻击,需通过SameSite属性限制第三方站点携带。
  2. 存储容量限制:单个域名的 Cookie 数量通常限制在 50 个以内,总大小不超过 4KB,超出时浏览器会自动丢弃旧 Cookie。

通过这三个自动机制,Cookie 在无需用户干预的情况下实现了高效的状态管理,成为现代 Web 应用的基础设施之一。理解这些机制有助于开发者优化用户体验、防范安全漏洞,并合理设计 Cookie 策略(如设置Max-Age平衡时效性与性能)

使用 jjwt:0.12.7` 依赖实现 JWT 生成与解析的测试示例


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

基于 JUnit 5 编写:

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;

public class JwtTest {

    // 过期时间:1小时(3600000毫秒)
    private static final long EXPIRATION_TIME = 3600000;
    // 生成256位HMAC密钥(JJWT 0.12.x要求密钥长度符合算法安全标准)
    private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    /**
     * 测试生成JWT令牌
     */
    @Test
    public void testGenerateJwt() {
        // 1. 准备自定义载荷(claims)
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", 1001);
        claims.put("username", "testUser");
        claims.put("role", "admin");

        // 2. 生成JWT令牌
        String jwt = Jwts.builder()
                .setClaims(claims) // 设置自定义载荷
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 设置过期时间
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256) // 设置签名算法和密钥
                .compact();

        // 验证令牌不为空
        assertNotNull(jwt);
        System.out.println("生成的JWT令牌:" + jwt);
    }

    /**
     * 测试解析有效的JWT令牌
     */
    @Test
    public void testParseValidJwt() {
        // 1. 先生成一个令牌
        String jwt = generateTestJwt();

        // 2. 解析令牌
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY) // 设置验证密钥
                    .build()
                    .parseClaimsJws(jwt) // 解析JWT
                    .getBody();

            // 3. 验证载荷内容
            assertEquals(1001, claims.get("userId"));
            assertEquals("testUser", claims.get("username"));
            assertEquals("admin", claims.get("role"));
            assertNotNull(claims.getIssuedAt());
            assertNotNull(claims.getExpiration());

            System.out.println("解析成功,用户ID:" + claims.get("userId"));

        } catch (JwtException e) {
            fail("解析有效令牌时发生异常:" + e.getMessage());
        }
    }

    /**
     * 测试解析过期的JWT令牌
     */
    @Test
    public void testParseExpiredJwt() {
        // 1. 生成一个已过期的令牌(过期时间设为1毫秒后)
        String expiredJwt = Jwts.builder()
                .setClaims(Map.of("userId", 1001))
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1)) // 1毫秒后过期
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256)
                .compact();

        // 2. 等待令牌过期
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 3. 解析过期令牌,预期抛出ExpiredJwtException
        assertThrows(ExpiredJwtException.class, () -> {
            Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(expiredJwt);
        });
    }

    /**
     * 测试解析签名错误的JWT令牌(使用错误密钥)
     */
    @Test
    public void testParseInvalidSignatureJwt() {
        // 1. 生成正常令牌
        String validJwt = generateTestJwt();

        // 2. 使用错误的密钥解析
        Key wrongKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 新的随机密钥
        assertThrows(SignatureException.class, () -> {
            Jwts.parserBuilder()
                    .setSigningKey(wrongKey) // 错误的密钥
                    .build()
                    .parseClaimsJws(validJwt);
        });
    }

    // 辅助方法:生成测试用JWT
    private String generateTestJwt() {
        return Jwts.builder()
                .setClaims(Map.of(
                        "userId", 1001,
                        "username", "testUser",
                        "role", "admin"
                ))
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256)
                .compact();
    }
}

代码说明:

  1. 依赖适配:针对 0.12.7 版本,使用 Keys.secretKeyFor() 生成符合算法要求的密钥(HS256 需 256 位密钥),避免手动生成密钥可能导致的长度不足问题。
  2. 核心功能
    • 生成令牌:设置自定义载荷(userId、username 等)、签发时间、过期时间,并使用 HS256 算法签名
    • 解析令牌:验证签名合法性、提取载荷信息
    • 异常测试:覆盖令牌过期(ExpiredJwtException)、签名错误(SignatureException)等场景
  3. 注意事项
    • 密钥(SECRET_KEY)在实际项目中需安全存储,避免硬编码
    • 过期时间根据业务需求调整(示例为 1 小时)
    • 0.12.x 版本相比旧版本强化了安全校验,密钥长度和算法必须匹配(如 HS256 对应 256 位密钥)

运行测试前,需确保添加 JUnit 5 依赖(如 junit-jupiter-apijunit-jupiter-engine)。

Spring中,多个 Interceptor 的执行顺序?


在 Spring 中,多个 Interceptor(拦截器)的执行顺序由它们在配置时的注册顺序决定,具体规则如下:

  1. 注册顺序决定前置方法(preHandle)的执行顺序 当多个拦截器被注册到 Spring 容器时,它们的 preHandle 方法会按照注册的先后顺序执行(先注册的先执行)。
  2. 后置方法(postHandle)和完成方法(afterCompletion)的执行顺序相反
    • postHandle 方法:按照注册的逆序执行(后注册的先执行)。
    • afterCompletion 方法:同样按照注册的逆序执行(后注册的先执行)。
  3. 特殊情况:preHandle 返回 false 时的中断机制 如果某个拦截器的 preHandle 方法返回 false,则后续所有拦截器的 preHandle 方法都不会执行,且已执行过 preHandle 的拦截器会按照逆序执行 afterCompletion 方法

示例说明

假设有三个拦截器 InterceptorAInterceptorBInterceptorC,注册顺序为 A → B → C,则执行流程如下:

1. A.preHandle() → 执行
2. B.preHandle() → 执行
3. C.preHandle() → 执行
4. 控制器方法执行
5. C.postHandle() → 执行
6. B.postHandle() → 执行
7. A.postHandle() → 执行
8. 视图渲染完成
9. C.afterCompletion() → 执行
10. B.afterCompletion() → 执行
11. A.afterCompletion() → 执行

配置方式对顺序的影响

例如,Java 配置中按以下顺序添加时,执行顺序为 InterceptorA → InterceptorB

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new InterceptorA()); // 先注册
    registry.addInterceptor(new InterceptorB()); // 后注册
}

总结:拦截器的执行顺序本质上由注册顺序决定,前置方法正序执行,后置和完成方法逆序执行。


返回