【JavaEE34-博客系统案例实战】从零开始撸一个博客系统(二):登录 + JWT令牌 + 强制登录,让系统先有“门卫”

老铁们,上回咱们把博客列表给整出来了,文章能晒了。但是,现在谁都能进后台、谁都能发文章,这不行啊!

这期咱就给系统加个"门卫"------登录功能 + JWT 令牌 + 强制登录拦截器

学完这篇,你就能明白:为啥集群环境下不能用 Session,令牌到底是个啥,怎么生成、怎么校验、怎么让前端乖乖带着令牌访问。

OK,老铁们,废话不多说,我们接下来就继续完成项目,我们本项目案例中隐含着很多满满的干货,尤其是这一篇,咱们可是要掰扯这个JWT的哦,内容不容错过😊!

代码依然一行一行敲,道理一句一句讲,走起!👇


一、传统 Session 登录在集群环境下的坑

咱先回忆一下传统登录怎么做:

  1. 用户输入账号密码,后端校验通过,把用户信息存到 HttpSession 里。
  2. 后端返回 sessionId(通过 Cookie 自动带回去)。
  3. 后续请求浏览器自动带上 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 服务器发现令牌失效了,就会让你重新登录,重新发一个令牌。

所以,服务器必须具备两个能力:

  1. 生成令牌的能力(发证)
  2. 校验令牌的能力(验证真伪)

我们说的这个"身份标识",在技术里就叫令牌 ,也叫 token


二、令牌技术:把"身份"交给客户端

为了解决上面的问题,咱可以用令牌(Token)

  • 用户登录成功后,服务器生成一个令牌(一串字符串),返回给客户端。
  • 客户端把令牌存起来(比如浏览器的 localStorage),后续每次请求都主动把令牌放在请求头里。
  • 服务器收到请求,从请求头里取出令牌,验证是否合法、是否过期。

好处

  • 服务器不需要存储 Session,令牌自带用户信息。
  • 任何一台服务器都能验证令牌(只要用相同的密钥),天然支持集群。

生活例子:你去游泳馆,办了一张年卡(令牌)。以后每次去,只要出示年卡就行,前台不用记住你是谁,只看卡是否有效。卡丢了可以挂失,别人捡了也用不了(有签名)。


三、JWT 令牌:一种标准的令牌格式

咱不自己造轮子,直接用业界标准 JWT(JSON Web Token)

官网:https://jwt.io/

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)、那为什么我们拿到后还要解码?

因为你需要读出里面的信息啊!比如你存了 userIduserName,你总要知道是哪个用户在访问吧?

解码就是 Base64Url 的反过程,把那一串乱码还原成 JSON 字符串。服务器在解码的同时,还会验证签名,确保数据没有被篡改。

图片里说的"解码有效载荷"就是这个意思:

  • 左边那串 eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXWCj9 解码后得到 {"alg":"HS256","typ":"JWT"}
  • 中间那串解码后得到你存的用户信息。

(4)、一句话总结

不直接传 JSON,是因为要加上签名防止伪造,同时编码成 URL 安全的字符串方便传输。解码是为了读取里面的内容,并验证签名是否有效。

所以你看,JWT 并不是故意"加密"让你看不懂,而是为了安全和方便。你完全可以用官网提供的解码工具看到里面的内容,但一旦有人改了任何一个字符,签名校验就会失败,服务器会拒绝这次请求。这就是 JWT 的防篡改原理。


3.3 为啥 JWT 安全?

因为签名。只要你的密钥不泄露,别人篡改了 Header 或 Payload 里的任何一个字符,签名校验就会失败。所以 JWT 保证的是 令牌内容没有被篡改,而不是内容加密。


3.4 引入令牌的业务逻辑

如图所示:

老铁,这张图把 JWT 的使用流程画得很清楚,咱就用大白话给你捋一遍。


第一步:登录(客户端 ↔ 服务器)

  1. 客户端(你的浏览器)账号 + 密码 发给服务器。
  2. 服务器 收到后,先校验账号密码对不对。
    • 如果不对:直接返回"登录失败"。
    • 如果对:服务器就干一件事------生成一个 JWT 令牌(就是一串像乱码的字符串)。
  3. 服务器把 JWT 令牌 下发给客户端。
  4. 客户端收到令牌后,把它存起来(通常存在 localStoragesessionStorage 里)。

此时,登录成功!


第二步:后续操作(比如访问博客列表、写文章等)

  1. 客户端在每次请求后端接口时,主动把 JWT 令牌放到请求头里 (比如 Authorization: Bearer <token>)。
  2. 服务器收到请求后,不会再去查数据库看你是谁 ,而是直接验证这个 JWT 令牌
    • 验证签名对不对(防止被别人篡改)
    • 验证过期时间到了没有
  3. 如果令牌有效,服务器就正常处理请求,返回数据;如果无效(比如过期或被篡改),就返回 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.htmlblog_detail.htmlblog_edit.html 等)引入这个 common.js

html 复制代码
<script src="js/common.js"></script>

这样,所有 AJAX 请求都会自动带上 token,后端拦截器校验失败时会返回 401,前端就会自动跳转到登录页。


九、测试一下

  1. 启动项目,直接访问 http://localhost:8080/blog_list.html。由于没有 token,页面会通过 ajaxError 捕获 401,弹窗后跳转到登录页。

  2. 登录成功后,localStorage 里存了 token,再访问列表页,AJAX 请求会自动带上 token,后端校验通过,正常显示列表。

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


十、总结 + 下期预告

今天咱干了几件大事:

  1. 弄懂了为啥集群环境下不能用 Session,令牌技术的优势。
  2. 集成了 JWT 依赖,写了工具类生成和校验令牌。
  3. 实现了登录接口,登录成功后返回 JWT 令牌。
  4. 前端把令牌存到 localStorage,并统一在请求头里带上。
  5. 写了拦截器校验令牌,未登录返回 401。
  6. 前端统一处理 401,自动跳转登录页。

现在系统有"门卫"了,不登录啥也干不了。

内容确实有点多,尤其是 JWT 那一块,我反复改了好几版,就怕讲不清楚。

整理这一篇真的熬了不少夜(🤦‍♀️),代码、原理、流程图、生活例子......能上的都上了。

如果你觉得读完有收获,点赞👍、收藏、关注 安排一下,这对我真的很重要。

下期预告:博客详情页、发布博客(支持 Markdown 编辑器)、编辑和删除(只允许作者操作)。还没关注的老铁点个关注,不迷路。咱们下篇见!🚀

相关推荐
indexsunny7 小时前
互联网大厂Java面试实录:Spring Boot到微服务的深入探讨
java·spring boot·微服务·面试·eureka·kafka·jwt
弹简特1 天前
【JavaEE】JWT令牌:签名、密钥、生成令牌、校验令牌、编码、解码,一篇文章彻底讲透
jwt·密钥
庞轩px4 天前
JWT与Session比较
jwt·session·登录鉴权·无状态
庞轩px5 天前
JWT + Redis 双 Token 机制:从原理到实战
数据库·redis·缓存·jwt·token·登录认证
带娃的IT创业者6 天前
WeClaw_43_双重认证与Token自动刷新:Device Fingerprint与JWT安全机制
jwt·认证机制·双重认证·设备指纹·token刷新·http安全
૮・ﻌ・20 天前
Node.js - 04:MongoDB、会话控制
数据库·mongodb·node.js·jwt·token·cookie·session
indexsunny20 天前
互联网大厂Java面试实战:从Spring Boot到微服务架构的音视频场景解析
java·spring boot·spring cloud·mybatis·spring security·jwt·flyway
没有bug.的程序员21 天前
撕裂微服务网关的认证风暴:Spring Security 6.1 与 JWT 物理级免登架构大重构
java·spring·微服务·架构·security·jwt
独断万古他化22 天前
【抽奖系统开发实战】Spring Boot 项目的用户模块设计:注册登录、权限管控与敏感数据加密
java·spring boot·redis·后端·mvc·jwt·拦截器