老铁们,上回咱们把博客列表给整出来了,文章能晒了。但是,现在谁都能进后台、谁都能发文章,这不行啊!
这期咱就给系统加个"门卫"------登录功能 + JWT 令牌 + 强制登录拦截器 。
学完这篇,你就能明白:为啥集群环境下不能用 Session,令牌到底是个啥,怎么生成、怎么校验、怎么让前端乖乖带着令牌访问。
OK,老铁们,废话不多说,我们接下来就继续完成项目,我们本项目案例中隐含着很多满满的干货,尤其是这一篇,咱们可是要掰扯这个JWT的哦,内容不容错过😊!
代码依然一行一行敲,道理一句一句讲,走起!👇
一、传统 Session 登录在集群环境下的坑
咱先回忆一下传统登录怎么做:
- 用户输入账号密码,后端校验通过,把用户信息存到
HttpSession里。 - 后端返回
sessionId(通过 Cookie 自动带回去)。 - 后续请求浏览器自动带上 Cookie,后端从 Session 里取出用户信息。
问题 :如果项目只部署在一台机器上,没问题。但企业里通常一个 Web 应用会部署在多台服务器上(比如三台),前面用 Nginx 做负载均衡。

假设用户第一次登录请求被分到了 服务器1 ,Session 存在了服务器1上。下一次用户请求博客列表,可能被分到 服务器2。服务器2上没有这个用户的 Session,就会提示"未登录"。用户就疯了:明明刚登录过,咋又要登录?
这就是 Session 方案的痛点:Session 存在单台服务器上,集群环境下无法共享。
同时我们的session呢它是存在内存中的,所以重启服务器session会丢失。
况且我们的session它的过期时间是有限的,默认是30分钟,如果你设计时间长一点点的话,假设你有上亿个用户,那这上亿个用户都存在你的服务器上,那此时你这个服务器的效率就非常之低。
🚀补充: session问题以及解决方式🐶
1. 使用Cookie和Session的三个问题
1.1 重启服务器session丢失

1.2 我们的session存储在内存中,它会消耗服务器的资源

1.3 在集群环境下session不可用

2. 如何解决
2.1 解决方案1
你看我们上述第3个集群的原因,他找不到原因,最大的核心就是这个session信息不共存嘛,一个存在服务器1中,一个存在服器3中,不共存。
那么此时就很简单了,我们服务器机去创建这个session信息的时候,我们就把它存在Redis中,然后我们的服务器3你去查找这个session的时候,我们也去服务器redis中去查。那如此就解决了我们的问题。

同时呢我们把session单独用一个新的服务器存储起来,那么此时它就不消耗我们的资源了。
如此一来,我们就解决了上述的问题。
2.2 解决方案2
咱们第二个方案,也是本案例要用的方法,就是令牌技术。这个技术现在非常常用,也特别成熟,咱就用它。
先举个例子帮你理解:

你去住酒店,第一步:拿出身份证给前台。前台把你的身份证信息传到发证机关(比如公安局) 去核实。发证机关确认你的身份证是真的,然后反馈给前台:"这个人没问题,可以入住。"最后前台把身份证还给你,身份证一直拿在你自己手里。
这里面有个关键:发证机关为什么能校验你的身份证? 因为身份证就是它发的,它当然知道真假。
类比到咱们的系统里:
- 发证机关(公安局) = 服务器
- 身份证 = 令牌
- 你 = 客户端
你第一次登录的时候,服务器(发证机关)验证你的账号密码,验证通过后,服务器给你生成一个"身份标识",也就是令牌。然后把这个令牌返回给客户端(你)。客户端拿到令牌之后,就把它存起来(比如放浏览器本地)。
后续你再访问系统里的任何功能,比如看博客列表、写文章,你都带上这个令牌。服务器收到请求,先校验令牌是不是自己发的、有没有过期、有没有被篡改。校验通过,就给你提供服务。

那为什么令牌可以在多个服务器之间通用?
因为咱们的系统往往不止一台服务器,而是一个服务器集群(比如好几台机器一起跑)。就像全国有很多发证机关,每个机关都能校验你的身份证,因为身份证是统一的、全国通用的。你在 A 服务器登录拿到的令牌,去访问 B 服务器,B 服务器也能校验它(因为所有服务器用的是同一套"发证规则")。如果 B 服务器发现令牌失效了,就会让你重新登录,重新发一个令牌。
所以,服务器必须具备两个能力:
- 生成令牌的能力(发证)
- 校验令牌的能力(验证真伪)
我们说的这个"身份标识",在技术里就叫令牌 ,也叫 token。
二、令牌技术:把"身份"交给客户端
为了解决上面的问题,咱可以用令牌(Token)。
- 用户登录成功后,服务器生成一个令牌(一串字符串),返回给客户端。
- 客户端把令牌存起来(比如浏览器的
localStorage),后续每次请求都主动把令牌放在请求头里。 - 服务器收到请求,从请求头里取出令牌,验证是否合法、是否过期。
好处:
- 服务器不需要存储 Session,令牌自带用户信息。
- 任何一台服务器都能验证令牌(只要用相同的密钥),天然支持集群。
生活例子:你去游泳馆,办了一张年卡(令牌)。以后每次去,只要出示年卡就行,前台不用记住你是谁,只看卡是否有效。卡丢了可以挂失,别人捡了也用不了(有签名)。
三、JWT 令牌:一种标准的令牌格式
咱不自己造轮子,直接用业界标准 JWT(JSON Web Token)。
3.1 JWT 长啥样?
一个 JWT 令牌长这样(中间用点分隔):

xml
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

它由三部分组成:
- Header(头部) :存放令牌类型和加密算法(如
HS256),好比告诉你这是身份证。 - Payload(负载):存放你想存的自定义信息,比如用户 id、用户名,也可以放过期时间等标准字段,好比我们身份证上对应的信息。
- Signature(签名):把前两部分用密钥加密后生成的签名,用来防止令牌被篡改,比如你有身份证,你身份证上有对应的信息,但是你可不可以伪造呢?为了防止这样的伪造,就用了这个签名。
注意 :Payload 负载只是 Base64Url 编码 ,不是加密,任何人拿到令牌都可以解码看到里面的内容。所以不要放敏感信息(比如密码)。
3.2 如图:为什么需要编码为什么需要解码?

先直接回答为何编码:
我们不是"故意"把信息编码成看不懂的样子,而是为了安全传输和防伪造 。你看到的那个像乱码一样的字符串,其实是 三部分拼在一起 的:头部(header)+ 负载(payload)+ 签名(signature) ,每一部分都是经过 Base64Url 编码的。
(1)、为什么不直接传 JSON 明文?
假设你直接传这样一段 JSON:
json
{
"userId": 123,
"userName": "张三",
"exp": 1698399660
}
问题1:传输不安全
HTTP 是明文传输的(除非你用 HTTPS),中间人很容易截获并修改内容。即使你用 HTTPS,传输过程中也可能因为特殊字符(比如换行、引号)导致解析出错。
问题2:无法防伪造
直接传 JSON,谁都可以自己造一个假的 JSON 塞给你。服务器无法判断这个 JSON 到底是你签发的,还是别人伪造的。
问题3:URL 不安全
如果放在 URL 参数里(比如 ?token={...}),JSON 里的 {、}、"、: 等字符需要大量转义,很麻烦。
(2)、JWT 是怎么解决这些问题的?
JWT 把三部分分别编码成 Base64Url 格式(只包含字母、数字、-、_),拼在一起,中间用点 . 隔开。这样:
- 所有特殊字符都没了,可以安全放在 URL、Header 里。
- 最重要的:第三部分签名。签名是用密钥对前两部分(头部+负载)计算出来的。任何人篡改前两部分,签名就会失效,服务器一校验就知道被改过。
所以,JWT 不是加密,而是编码 + 签名 。
编码只是为了让数据能安全地在网络上传输,签名才是防伪造的核心。
(3)、那为什么我们拿到后还要解码?
因为你需要读出里面的信息啊!比如你存了 userId、userName,你总要知道是哪个用户在访问吧?
解码就是 Base64Url 的反过程,把那一串乱码还原成 JSON 字符串。服务器在解码的同时,还会验证签名,确保数据没有被篡改。
图片里说的"解码有效载荷"就是这个意思:
- 左边那串
eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXWCj9解码后得到{"alg":"HS256","typ":"JWT"} - 中间那串解码后得到你存的用户信息。
(4)、一句话总结
不直接传 JSON,是因为要加上签名防止伪造,同时编码成 URL 安全的字符串方便传输。解码是为了读取里面的内容,并验证签名是否有效。
所以你看,JWT 并不是故意"加密"让你看不懂,而是为了安全和方便。你完全可以用官网提供的解码工具看到里面的内容,但一旦有人改了任何一个字符,签名校验就会失败,服务器会拒绝这次请求。这就是 JWT 的防篡改原理。
3.3 为啥 JWT 安全?
因为签名。只要你的密钥不泄露,别人篡改了 Header 或 Payload 里的任何一个字符,签名校验就会失败。所以 JWT 保证的是 令牌内容没有被篡改,而不是内容加密。
3.4 引入令牌的业务逻辑
如图所示:

老铁,这张图把 JWT 的使用流程画得很清楚,咱就用大白话给你捋一遍。
第一步:登录(客户端 ↔ 服务器)
- 客户端(你的浏览器) 把 账号 + 密码 发给服务器。
- 服务器 收到后,先校验账号密码对不对。
- 如果不对:直接返回"登录失败"。
- 如果对:服务器就干一件事------生成一个 JWT 令牌(就是一串像乱码的字符串)。
- 服务器把 JWT 令牌 下发给客户端。
- 客户端收到令牌后,把它存起来(通常存在
localStorage或sessionStorage里)。
此时,登录成功!
第二步:后续操作(比如访问博客列表、写文章等)
- 客户端在每次请求后端接口时,主动把 JWT 令牌放到请求头里 (比如
Authorization: Bearer <token>)。 - 服务器收到请求后,不会再去查数据库看你是谁 ,而是直接验证这个 JWT 令牌 :
- 验证签名对不对(防止被别人篡改)
- 验证过期时间到了没有
- 如果令牌有效,服务器就正常处理请求,返回数据;如果无效(比如过期或被篡改),就返回 401 未登录错误。
为啥要这么设计?
- 无状态:服务器不用存 Session,每个请求都自带令牌,方便做集群扩展。
- 安全:令牌有签名,别人伪造不了。
- 高效:验证令牌比查数据库快。
所以图中的两步,其实就是:
登录时发证,后面每次带证验证。
老铁们,这下明白了吧?上述啰嗦的逻辑辅助我们理解了神秘的JWT,到这里,整理不易,老铁们发财的小手点赞一下👍,给主包🤗一个鼓励,🦀🦀各位老铁啦~~
了解令牌的这个逻辑之后,我们接下来就开始去编写代码如何在项目中集成JWT。
四、在项目里集成 JWT
4.1 引入依赖
直接复制即可
xml
<!-- JWT 相关依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
1)使用:如何生成令牌
我们去test的目录下去,测试一下jwt的使用

代码1:头部默认 没有签名


对setClaims()解释

对compact()解释

代码2:加入签名



对signWith()解释

对secretKeyFor()解释

对SignatureAlgorithm.HS256解释

| 方式 | 代码 | 特点 | 适用场景 |
|---|---|---|---|
| 随机生成 | Keys.secretKeyFor(HmacAlgorithms.HS256) |
每次运行生成不同密钥 | 仅限测试、临时演示 |
| 固定密钥 | Keys.hmacShaKeyFor(Decoders.BASE64.decode("固定字符串")) |
永远生成同一个密钥 | 生产环境、正式项目 |
这里可能老铁们会有一些不太清楚,就是随机生成和固定密钥到底什么意思?也就是说我们此处演示的生成令牌使用的是随机生成的方式,而我们后续用的是固定密钥的方式,那具体这个生成令牌的详细内容参考博客👉:JWT令牌
2)使用:如何校验令牌

Ok,使用完之后,我们开始应用在我们的项目中。
4.2 写一个 JWT 工具类
工具类如下:

这个工具类负责生成令牌 和校验令牌。
下述代码 看不懂没关系,我们后面会有解释(但是更推荐老铁优先将这篇博客了解一下:JWT令牌)
java
@Slf4j
public class JwtUtils {
// 密钥
private static final String SECRET = "E9cROwHHi8xm85iY7wA17hVJ44KQZlY5W4NQeE7WUB8=";
// 生成安全密钥对象
private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET));
/**
* 生成 JWT 令牌
* @param claims 要存储的自定义信息(比如用户 id、用户名)
* @return JWT 字符串
*/
public static String genToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims) // 自定义负载
.signWith(KEY) // 签名算法
.compact();
}
/**
* 校验并解析 JWT 令牌
* @param jwt JWT 字符串
* @return 解析后的 Claims(包含所有负载信息),如果无效返回 null
*/
public static Claims parseJwt(String jwt) {
if (!StringUtils.hasLength(jwt)) {
return null;
}
try {
//如果你的载荷信息被伪造的话,就会抛出异常
//此时解析失败,代表的是你的令牌被伪造了,我不让你通过
return Jwts.parserBuilder()
.setSigningKey(KEY)
.build()
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
log.error("解析令牌错误, jwt: {}", jwt, e);
return null;
}
}
}

关于密钥生成:可以用下面的代码生成一个安全的密钥字符串:
java
@Test
public void genKey() {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secret = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secret);
}
解释第一行代码

解释第二行代码:

拿到这个字符串,放到上面的 SECRET 常量里。

看完代码,老铁可能会有点晕,这里的细节很多,下述逻辑是我们从上述提到的JWT博客中摘取过来的一部分内容,可以供老铁们理解完上述的代码。
🤔JWT当中第3段,签名靠什么生成?

这里老铁请记住,签名是通过密钥+前面两段,再通过一个哈希算法得到的。可是想到这儿,再次思考,我们上述代码中,你会观察到:第一段头部由我们的构建器,默认生成,第2段载荷是我们用户自己定义,这第3段签名是一个秘钥对象(Key对象)。

🤔老铁你看图中的第3段,他要的规格是密钥对象,也就是说我们不是把一串数字直接当做密钥去校验,而是呢,把它封装成一个对象密钥对象Key,最后再使用这个密钥对象,结合1、2段编码后进行一个哈希,得到第3段二进制签名,再把第3段进行编码,最后呢将123段拼接成字符串,得到我们的令牌token。

Ok,老铁,到这一步,你知道,我们进行校验用的是密钥对象,而不是一个单一的字符串。
接下来我们就抛出了这样的一个疑问:
🤔这个密钥对象key怎么生成?它有固定的接口,供我们调用吗?
没错,它就是有固定的接口,供我们去调用,直接生成这个密钥对象。

🤔那很简单的呀,为什么我们还要用上述那么麻烦的逻辑呢,我们直接用这个api去生成密钥对象,进行校验不就行了吗?
事实并非如此,老铁你看这样的方式它有一个缺陷,你这样写的话,你每次重启服务器,它都会重新生成一个新的密钥对象。每次都用这个新的密钥对象去进行签名的话,我们就每一次重启服务器,都有不同的签名。换句话说,你每次重启服务器,你都有一个令牌,到时候你把这个令牌是给我们的前端的呀,比如前端用户a拿到这个令牌1之后,你突然间重启服务器了,你后台又有一个新的令牌2,你得校验通过这个新的令牌2的话。而用户a拿着原来的令牌1去校验,发现校验不对,这就问题大了。很显然用不了这种方式。
🤔那怎么办呢?
其实道理很简单,就比如我们的集群服务器,不同的服务器,你最终共用一个密钥对象,也就是说你的这个密钥对象得是固定的,你这个密钥对象保持唯一性,或者说保持固定性不变,那么无论你怎么重启服务器,它都不变的话,你前端永远都是使用这个令牌去后端进行校验。还会出现上述的问题吗?绝对不会。
🤔那如何让这个密钥对象唯一啊?
老铁,这里咱们的技巧性就很能体现出来:你先看下图。

你看,我们用这个 API呢,可以得到一个安全的密钥对象,所以呢,我们先随机让它生成一个安全的密钥对象,而这个密钥对象中有什么?有二进制密钥数据。这个二进制没有数据我们读不懂呀,但是不用怕,我们可以把它进行编码,编码成我们看得懂的,字符串数数据就是密钥字符串,如下图所示:

此时你看我们可以从密钥对象中取出二进制密钥数据,然后把这个二进制密有数据给编码成一个可以看得懂的字符串密钥:
text
HtZrNdvOQYgg6QxLMyXYqVAOwPD6Ue+lHpXho5m54O0=
🤔你好端端的生成这个字符串密钥干嘛呢?
首先你就说这个字符串密钥你能不能看得明白吧,反正字母能看得懂,对吧?那既然如此,我们可以拿它来进行配置呀,为什么呢?你看:你生成的密钥对象中有二进制密钥,你把二进制密钥可以编码成密钥字符串,那反过来,你可以将这个密钥字符串进行解码成二进制密钥呀。而你解码成二进制密之后,你可以用api对它进行封装,再封装成一个密钥对象,这不就闭合了吗?我们将得到的这个密钥字符串配置在某个地方,固定死在我们的后端,让它保持一个永远不会变的状态。然后我们每一次生成的密钥对象,都通过这个钥字符串先转为二进制密钥,再将这个二进制密钥封装成密钥对象。如此一来,只要你的这个密钥字符串不变动,那么我们的密钥对象他就不会改变,逻辑如下图所示:

好,到这里我们再回头看之前我们很难理解的那两行代码:

这两行代码的解释如下图所示:

😊😊 Ok了,老铁们,到这里了,咱们大概也给他掰扯清楚了,最起码呢这两行代码的意思我们是理解了,那具体更多JWT的细节呢,老铁们可以参考上述我们提到的那篇JWT相关的博客。
对了,老铁们,如果你理解了,别忘了给主包点个赞哦,整理不易,😄,谢谢大家🌶️~~
消化之后,就让俺们继续往下肝💪
五、实现登录接口:生成令牌并返回
5.1 约定接口
- 请求方式:
POST /user/login - 请求参数(JSON 格式):
json
{
"userName": "zhangsan",
"password": "123456"
}
- 响应(成功):
json
{
"code": 200,
"errMsg": null,
"data": {
"userId": 1,
"token": "eyJhbGciOiJIUzI1NiJ9..."
}
}
失败时 data 为空或 code 为 -1。
5.2 创建请求参数 VO 和响应 VO
参数 VO
前端登录的时候,传的参数就只需要用户名和密码。

参数校验:

对于对象的校验直接写在属性上:

光写注解不行,必须在 接口参数 上加 @Validated 才会生效:

代码:
java
@Data
public class UserLoginParam {
@NotBlank(message = "用户名不能为空")
@Length(max = 20, min = 2, message = "用户名长度必须在 2-20 位之间")
private String userName;
@NotBlank(message = "密码不能为空")
@Length(max = 20, min = 5, message = "用户名长度必须在 5-20 位之间")
private String password;
}
响应 VO

java
@Data
public class UserLoginResponse {
private Integer userId;
private String token;
}
5.3 Service 层实现
代码有详细注解
java
@Service
public class UserServiceImpl implements UserService {
@Autowired //只有一个类 可以直接使用他
private UserInfoMapper userInfoMapper;
//实现校验密码
@Override
public UserLoginResponse checkPassword(UserLoginParam userLoginParam) {
//查询数据库 根据用户名查询用户对象,然后拿出密码进行校验
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();//条件构造器
queryWrapper.lambda().eq(UserInfo::getUserName, userLoginParam.getUserName());//构造条件
//确保这个用户没有被注销 所以delete_flag得是0 0代表没有被删除
queryWrapper.lambda().eq(UserInfo::getDeleteFlag, 0);
//调用的是selectOne只是查询出一个对象,不能是多个,否则就不对了,一个用户只有一个账号
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
//看一下这个用户是否存在
if (userInfo == null) {
//用户不存在
throw new BlogException("用户不存在");
}
//用户存在的话,那么此时就判断用户密码是否正确
if (!userLoginParam.getPassword().equals(userInfo.getPassword())) {
//用户密码不正确 那么此时我就直接抛出一个异常
throw new BlogException("用户密码不正确");
}
//此时就是密码正确的情况,此时就需要返回我们的视图对象VO
//但是视图对象VO里面有着属性是 用户ID和token
//关键就是这个token就是令牌 那么令牌我们怎么生成呢?
//我们借助的是JWT 那么此时就得要有 头部信息header+载荷+签名 {其中头部信息由构建器默认生成,签名我们在工具类中封装了
//由于我们已经封装好了jwt的工具类,那么此处我们就只是需要做的就是将我们的载荷定义好就行了
Map<String, Object> map = new HashMap<>();//载荷
//载荷中我们存储 ID 和 用户名吧
map.put("id", userInfo.getId());
map.put("name", userInfo.getUserName());
//生成token
String token = JwtUtils.genToken(map);
UserLoginResponse userLoginResponse = new UserLoginResponse();
userLoginResponse.setUserId(userInfo.getId());
userLoginResponse.setToken(token);
return userLoginResponse;
}
}

出现用户不存在怎么办?

5.4 Controller 层
java
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Resource(name = "userServiceImpl")
private UserService userService;
@RequestMapping("/login")
public UserLoginResponse login(@RequestBody @Validated UserLoginParam userLoginParam) {
log.info("用户登录,用户名:" + userLoginParam.getUserName());
return userService.checkPassword(userLoginParam);
}
}
注意:这里要用 @RequestBody,因为前端传的是 JSON。

使用什么注入的问题

5.5 测试
问题:为什么密码登录的时候没有出现"密码不正确"的错误信息?

原因其实很简单就是在你自定义异常的时候,出现了问题,你那个message是要传到父类去的,你得加一行代码。

修改之后的测试:

六、前端:保存令牌到 localStorage

修改 blog_login.html 里的 login 函数:
javascript
function login() {
$.ajax({
type: "post",
url: "/user/login",
contentType: "application/json",
data: JSON.stringify({
userName: $("#username").val(),
password: $("#password").val()
}),
success: function (result) {
if (result.code == "SUCCESS" && result.data != null) {
// 保存令牌和用户 id 到 localStorage
localStorage.setItem("user_token", result.data.token);
localStorage.setItem("loginUserId", result.data.userId);
location.href = "blog_list.html";//登录成功跳到列表页
} else {
alert(result.errMsg || "用户名或密码错误");
}
}
});
}

localStorage 是浏览器自带的一个本地存储空间,可以存键值对,不会像 Cookie 那样自动发给服务器,需要咱手动放到请求头里。

七、强制登录拦截器:校验令牌
7.1 后端拦截器(保安)
老铁们,拦截器规则太模糊?💡 别慌,参考这篇博客:
👍拦截器
对应的包:

细节1

细节2:我们拦截器该干嘛呢?我们上面说过,我们登录完之后,服务器给前端一个token,然后呢,我们前端访问后续的任何一个页面都需要去带着这个token去访问,也就是说带着这个令牌去访问,这个令牌就是token,所以我们的拦截器就得去确认这个token是否合法。
在这如图所示,我们的Token是存在localStorage里面的:

然后你登录完毕之后,后续的每一个请求你就带上这个Token,那我们每一个请求如何带上这个token呢?请参考我们本篇博客的第八个知识点。
细节3 :我们约定前端每次去请求的时候携带这个token,这个token是存在请求头header中的,而这个header是以键值对的方式,那我们约定这个token的名称为user_token,所以我们的拦截器到时候得根据这个·user_token·,拿着这个键去请求头中取出这个键对应的值j即token值,那你想这个user_token我们可不可以定义为一个常量的?当然是可以的,如下所示:


java
public class Constants {
//请求头中的令牌:user_token
public static final String USER_TOKEN_HEADER_KEY = "user_token";
}
代码细节

完整代码:
java
@Slf4j
@Component//将这个类交给Spring管理 到时候直接拿去注册
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 从请求头中获取 token(前端会将放在 header 里,key 叫 user_token
// 我们已经封装成常量了,到时候就算你前端变为xxx_token我们也不用怕)
String token = request.getHeader(Constants.USER_TOKEN_HEADER_KEY);
//取到token之后就判断一下是否合法
if (token == null) {
//用户没传递token
//此时代表的是用户未登录 响应返回401
response.setStatus(401);
return false;//不允许通过
}
//校验token是否合法: 如何校验呢?我们在jwt中定义过了
//怎么校验?也就是说,我们登录成功的话,那么用户信息(用户id和用户名)我们存在了token的载荷中
//那么此时调用JWT校验方法,如果你返回的载荷Claims中有数据(不为null) 那么就是校验成功 如果返回的是null 就校验失败
Claims claims = JwtUtils.parseJwt(token);
if (claims == null) {
//如果是空的:被篡改或者没有token
//此时就拦击不让你过 代表你没有登录 响应返回401
response.setStatus(401);
return false;//不通过
}
//程序到这里 那么就是返回true 放行
return true;
}
}
7.2 注册拦截器(排除登录接口)(给保安签合同上岗)

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;//将保安注入进来
//添加保安 合同签约成功 你要为我干活
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor) //添加拦截器
.addPathPatterns("/blog/**", "/user/**") // 拦截所有博客和用户接口
.excludePathPatterns("/user/login"); // 排除登录接口
}
}
这样,除了登录接口,其他接口都会先过拦截器,token 无效就返回 401。
测试
登录不被拦截

其他页面被拦截

我们自己设置带上token就不被拦截

八、前端统一处理:每次请求都带上 token,遇到 401 跳转登录
8.1 用 ajaxSend 统一添加请求头
新建一个 common.js 文件,放入以下代码:
javascript
// 在每一个 AJAX 请求发送之前,自动从 localStorage 取出 token 放到请求头里
$(document).ajaxSend(function (e, xhr, opt) {
var token = localStorage.getItem("user_token");
if (token) {
xhr.setRequestHeader("user_token", token);
}
});
// 统一处理 AJAX 错误(比如 401)
$(document).ajaxError(function (event, xhr, options, exc) {
if (xhr.status == 400) {
alert("参数校验失败");
} else if (xhr.status == 401) {
alert("用户未登录,即将跳转到登录页");
location.href = "blog_login.html";
}
});
然后在每个需要认证的页面(blog_list.html、blog_detail.html、blog_edit.html 等)引入这个 common.js:
html
<script src="js/common.js"></script>
这样,所有 AJAX 请求都会自动带上 token,后端拦截器校验失败时会返回 401,前端就会自动跳转到登录页。
九、测试一下
-
启动项目,直接访问
http://localhost:8080/blog_list.html。由于没有 token,页面会通过ajaxError捕获 401,弹窗后跳转到登录页。
-
登录成功后,
localStorage里存了 token,再访问列表页,AJAX 请求会自动带上 token,后端校验通过,正常显示列表。

-
打开浏览器的开发者工具 → Application → Local Storage,可以看到 token 和 userId 都存在。你也可以把 token 复制出来,去 jwt.io 解码看看里面的内容。

十、总结 + 下期预告
今天咱干了几件大事:
- 弄懂了为啥集群环境下不能用 Session,令牌技术的优势。
- 集成了 JWT 依赖,写了工具类生成和校验令牌。
- 实现了登录接口,登录成功后返回 JWT 令牌。
- 前端把令牌存到
localStorage,并统一在请求头里带上。 - 写了拦截器校验令牌,未登录返回 401。
- 前端统一处理 401,自动跳转登录页。
现在系统有"门卫"了,不登录啥也干不了。
内容确实有点多,尤其是 JWT 那一块,我反复改了好几版,就怕讲不清楚。
整理这一篇真的熬了不少夜(🤦♀️),代码、原理、流程图、生活例子......能上的都上了。
如果你觉得读完有收获,点赞👍、收藏、关注 安排一下,这对我真的很重要。
下期预告:博客详情页、发布博客(支持 Markdown 编辑器)、编辑和删除(只允许作者操作)。还没关注的老铁点个关注,不迷路。咱们下篇见!🚀