嗨朋友们,好久不见(其实也就摸了两天鱼)!最近在搓一个前后端分离的小项目,然后嘛,登录认证这东西肯定绕不开的对吧?传统那种 Session + Cookie 的方式在分布式场景下就有点不太够看了,于是咱就盯上了 JWT 这个玩意儿。

刚开始接触 JWT 的时候我也是一脸懵逼——啥?Token 里面还能带数据?啥?不需要在服务端存 Session?听着就很玄乎啊喂!

不过实际整了一遍之后发现,嘿,也没那么难嘛。所以今天就跟大家分享一下,如何在 SpringBoot 项目中使用 JWT 来实现登录认证,希望看完你也能库库地整出来 (๑•̀ㅂ•́)و✧

JWT到底是个啥玩意儿?

        在动手写代码之前,得先把概念搞明白,不然就是照着教程敲完也不知道自己在写啥(别问我怎么知道的)。

        咱们先来简单点说:想象一下你去酒店开房间的时候,传统的 Session 模式是这样的:你到前台登记身份证,前台小姐姐把你的信息记在一个小本本上,然后给你一张房卡,房卡上面只有一个编号

你每次要进房间,掏出房卡刷一下,前台就得翻开小本本查:编号 007 对应的是谁?住哪个房间?住多久?

这个过程里,小本本就是 Session,存服务器上的。来一百个客人,小本本就记一百条。要是哪天前台换了个人(服务器重启),小本本没了,所有房卡全废。

那 JWT 的模式呢?

你到前台登记身份证,前台小姐姐直接把你的信息加密后印在房卡上(token),根本不用小本本

你每次来,前台看一眼房卡上印的东西,解密一下就知道你是谁了,完全不用去翻什么小本本

服务器不用存任何东西,全靠 token 本身携带的信息来验证身份,这就是 JWT 的核心思想。是不是一下子就好理解了?

        现在咱们再来正经的说一下:JWT(JSON Web Token)本质上是一个字符串,由三部分组成,用 `.` 分隔:header.payload.signature

Header(头部):标注了这个 token 用的是什么算法(一般是 HS256)

Payload(载荷):存数据的地方,比如用户 ID、用户名、过期时间等。注意,这部分只是 Base64 编码,不是加密!所以千万别把密码之类的敏感信息塞进去

Signature(签名):用 Header 里指定的算法,把 Header + Payload + 一个只有服务器知道的密钥(secret)一起算出一个签名,用来防止 token 被篡改

JWT的整体流程

大概可以看成这么几步:

1. 用户登录,服务器验证账号密码

2. 验证通过,服务器生成一个 JWT 返回给客户端

3. 客户端把这个 JWT 存起来(一般是 localStorage)

4. 以后每次请求,客户端在请求头里带上这个 JWT

5. 服务器拦截请求,解析 JWT 验证合法性

6. 合法就放行,不合法就返回 401

好了,概念捋清楚了,咱开始整代码!

第一步:准备工作,搭个项目

<!-- pom.xml 核心依赖 -->
<dependencies>
    <!-- SpringBoot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- SpringBoot Security,做安全认证的核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- JWT 工具库,咱用 jjwt,简单好使 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.5</version>
        <scope>runtime</scope>
    </dependency>

    <!-- MyBatis-Plus,操作数据库猛猛的方便 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.7</version>
    </dependency>

    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok,告别 getter/setter 地狱 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

小贴士:jjwt 从 0.12.x 版本开始把包拆成了 api、impl、jackson 三个,如果用旧版本(0.11.x)只需要一个依赖就行。如果你在网上看到只有一个依赖的教程,那大概率是老版本的写法,别慌,都能用!

然后是配置文件 `application.yml` ,我们先来个基础的:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

# JWT 相关配置,集中管理好习惯
jwt:
  # 密钥,生产环境务必用一个又长又复杂的字符串!
  secret: b3d8a9f1c2e4d6a8b0c2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6d8e0f2a4b6
  # 过期时间,单位毫秒,这里设了 24 小时
  expiration: 86400000

这里要重点提醒一下:`jwt.secret` 这个密钥千万别用短的、也别提交到公共仓库!HS256 算法要求密钥至少 256 位(32 字节),太短的话 jjwt 会直接给你抛异常。上面那个是我随便搓的,你们自己换一个。

第二步:整一个 JWT 工具类

        这个工具类是整个 JWT 操作的核心,负责生成 token、解析 token、验证 token 三大操作。咱把它封装好,后面用起来就方便了。

package com.example.demo.utils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

@Component
public class JwtUtils {

    // 从配置文件读取密钥和过期时间
    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    /**
     * 生成 JWT token
     * @param claims  要存进 token 的数据(用户ID、用户名啥的)
     * @return token 字符串
     */
    public String generateToken(Map<String, Object> claims) {
        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));

        return Jwts.builder()
                .claims(claims)                    // 设置载荷数据
                .issuedAt(new Date())              // 签发时间
                .expiration(new Date(System.currentTimeMillis() + expiration))  // 过期时间
                .signWith(key)                     // 签名
                .compact();                        // 打包成最终字符串
    }

    /**
     * 从 token 中解析出 claims 数据
     * @param token JWT token
     * @return claims 数据
     */
    public Claims parseToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));

        return Jwts.parser()
                .verifyWith(key)    // 用同一个密钥验证
                .build()
                .parseSignedClaims(token)  // 解析
                .getPayload();       // 拿到载荷
    }

    /**
     * 验证 token 是否合法(有没有被篡改、有没有过期)
     * @param token JWT token
     * @return true 表示合法,false 表示不合法
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            // JwtException:签名不对、过期等
            // IllegalArgumentException:token 为空等
            return false;
        }
    }
}

简单解释一下这几个方法在干啥:

generateToken:咱们往里面传一个 Map,存上用户 ID、用户名这些信息,它给你生成一个 token 串,顺便设好过期时间

parseToken:把 token 解开来,拿出里面的数据。如果 token 被篡改过或者过期了,这里会直接抛异常

validateToken:就是判断 token 合不合法,本质上就是调 parseToken,能解析成功就合法,抛异常就不合法

踩坑预警!!:jjwt 0.12.x 版本的 API 跟 0.11.x 变化挺大的!老版本用 `Jwts.parser().setSigningKey(key).parseClaimsJws(token)`,新版本变成了链式调用 `Jwts.parser().verifyWith(key).build().parseSignedClaims(token)`。如果你照着老教程写发现方法找不到,多半是版本问题 (╯‵□′)╯︵┻━┻

第三步:搞一个 JWT 过滤器

        有了生成和解析 token 的能力之后,咱们需要搞一个**过滤器**,在每个请求到达 Controller 之前先拦截下来,看看请求里有没有带合法的 token。

这就好比每个进小区的人,保安大叔都要看一下你的门禁卡,卡有效才放行。

package com.example.demo.filter;

import com.example.demo.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.ArrayList;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        // 从请求头里拿 token,一般是 "Authorization: Bearer xxx"
        String authHeader = request.getHeader("Authorization");

        // 没带 token,直接放行(后续 Security 配置会决定哪些接口必须认证)
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        // 把 "Bearer " 前缀去掉,拿到真正的 token
        String token = authHeader.substring(7);

        // 验证 token 合法性
        if (!jwtUtils.validateToken(token)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 解析出用户信息
        Claims claims = jwtUtils.parseToken(token);
        String userId = claims.get("userId", String.class);
        String username = claims.get("username", String.class);

        // 把用户信息封装成 Spring Security 能识别的认证对象
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userId, null, new ArrayList<>());

        // 塞到 Security 上下文里,这样后续的 Controller 就能拿到当前用户信息了
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 继续走下一个过滤器
        filterChain.doFilter(request, response);
    }
}

这个过滤器做这几件事:

1. 从请求头的 `Authorization` 字段里取出 token(格式是 `Bearer xxx`)

2. 验证 token 合不合法

3. 合法的话解析出用户信息,塞进 Spring Security 的上下文

4. 不管合不合法,都继续往下走(具体哪些接口需要认证,我们在 Security 配置里管)

这里还有个容易踩的坑:`OncePerRequestFilter` 确保一个请求只会经过这个过滤器一次,就算请求内部发生了转发也不会重复执行,比直接实现 `Filter` 接口更稳妥。

第四步:配置 Spring Security

        Spring Security 这玩意儿配置起来确实有点绕,不过咱只做 JWT 登录认证的话,配置其实不复杂。核心思路就是:关掉默认的表单登录和 Session,注册咱们自己写的 JWT 过滤器

package com.example.demo.config;

import com.example.demo.filter.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 关闭 CSRF,前后端分离 + JWT 用不着
            .csrf(csrf -> csrf.disable())

            // 不创建 Session,JWT 本身就是无状态的
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

            // 配置接口权限
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/user/login", "/api/user/register").permitAll()  // 登录注册不需要认证
                .anyRequest().authenticated()  // 其他接口全部需要认证
            )

            // 把咱的 JWT 过滤器加到 UsernamePasswordAuthenticationFilter 前面
            .addFilterBefore(jwtAuthenticationFilter,
                             UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 密码加密器,BCrypt 是目前业界主流选择
        return new BCryptPasswordEncoder();
    }
}

这里有几个点说一下哈:

关 CSRF:CSRF 攻击主要针对的是 Cookie + Session 模式,咱们用 JWT、token 放在请求头里,不存在 CSRF 的问题,关掉就完事了

STATELESS:无状态策略,Spring Security 不会创建 HttpSession,也不会用 Session 来存 SecurityContext。每次请求都是独立的,全靠 JWT 来识别身份

addFilterBefore:把咱们的 JWT 过滤器放在 `UsernamePasswordAuthenticationFilter` 之前执行,这样 JWT 认证完了之后,Spring Security 就知道当前用户是谁了

密码加密:千万千万不要明文存密码啊朋友们! BCrypt 是目前最主流的选择,它自带盐值,同一个密码两次加密出来的结果都不一样,安全性杠杠的

第五步:写登录接口

        终于到了大家最关心的部分——登录接口咋写!其实有了上面的铺垫,登录接口就特别简单了:

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.example.demo.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtUtils jwtUtils;

    /**
     * 登录接口
     */
    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody Map<String, String> loginData) {
        String username = loginData.get("username");
        String password = loginData.get("password");

        Map<String, Object> result = new HashMap<>();

        // 查数据库,验证用户名密码
        User user = userService.login(username, password);

        if (user == null) {
            result.put("code", 401);
            result.put("message", "用户名或密码错误捏~");
            return result;
        }

        // 生成 JWT,把用户信息存进去
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getId());
        claims.put("username", user.getUsername());
        String token = jwtUtils.generateToken(claims);

        result.put("code", 200);
        result.put("message", "登录成功!");
        result.put("token", token);

        return result;
    }
}

然后到了Service 层的登录逻辑也很直白

package com.example.demo.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public User login(String username, String password) {
        // 根据用户名查用户
        User user = userMapper.selectOne(
            new LambdaQueryWrapper<User>().eq(User::getUsername, username)
        );

        if (user == null) {
            return null;
        }

        // 用 BCrypt 比对密码(user.getPassword() 是数据库中加密后的密码)
        if (!passwordEncoder.matches(password, user.getPassword())) {
            return null;
        }

        return user;
    }
}

登录接口就是:接收用户名密码 → 查数据库验证 → 验证通过就生成 token 返回。逻辑清晰得很,没啥复杂的。

提醒一下:返回给前端的 token,前端记得存在 `localStorage` 里,然后每次请求在请求头里带上 `Authorization: Bearer <token>`。如果是用 axios,可以直接配一个请求拦截器统一处理,就不用每个请求都手动加了。

第六步:把注册接口也整一个

        光有登录不行啊,没有注册哪来的用户对吧?

@PostMapping("/register")
public Map<String, Object> register(@RequestBody Map<String, String> registerData) {
    String username = registerData.get("username");
    String password = registerData.get("password");

    Map<String, Object> result = new HashMap<>();

    // 检查用户名是否已存在
    User existUser = userService.findByUsername(username);
    if (existUser != null) {
        result.put("code", 400);
        result.put("message", "这个用户名已经被占用了,换一个吧~");
        return result;
    }

    // 创建用户,密码加密存储
    User user = new User();
    user.setUsername(username);
    user.setPassword(passwordEncoder.encode(password));  // 加密!加密!加密!
    userService.save(user);

    result.put("code", 200);
    result.put("message", "注册成功,快去登录吧 (๑•̀ㅂ•́)و✧");
    return result;
}

        再次强调:`passwordEncoder.encode(password)` 这步不能省!你要是直接 `user.setPassword(password)` 明文存进去,那数据库一泄露,全部用户的密码就都裸奔了,就真的会谢了

第七步:搞一个获取当前用户信息的接口

        用户登录之后,前端肯定需要展示一下"你好,xxx"之类的,咱得提供一个接口来获取当前登录用户的信息:

@GetMapping("/info")
public Map<String, Object> getUserInfo() {
    Map<String, Object> result = new HashMap<>();

    // 从 Security 上下文中取出认证信息(就是咱们在过滤器里塞进去的那个)
    Object principal = SecurityContextHolder.getContext()
                            .getAuthentication().getPrincipal();

    // principal 就是咱们在过滤器里存进去的 userId
    String userId = (String) principal;

    User user = userService.getById(userId);

    result.put("code", 200);
    result.put("data", user);
    return result;
}

这样前端一调 `/api/user/info` 就能拿到当前用户的信息了。这个接口经过了 JWT 过滤器,Security 上下文里已经有用户 ID 了,直接取就行。

补充一些你可能关心的问题

Token 过期了咋整?

咱们在配置文件里设了 `expiration: 86400000`(24 小时),过期之后 token 就失效了。一般来说有两种处理方式:

1. 简单粗暴型:过期了就弹回登录页,让用户重新登录

2. 体验优化型:搞个 refresh token 机制,access token 过期了用 refresh token 去换一个新的。这个方案更复杂一些,新手可以先不管,等后面有需要了再整

对于大多数小项目来说,24 小时过期够用了,大不了设长一点嘛。不过也别太久,token 一旦签发出去在过期之前是无法主动失效的(除非你引入黑名单机制),安全性和便利性总要有个权衡的。

用户登出怎么处理?

JWT 的一个"缺点"就是没法在服务端主动让一个 token 失效,因为服务器没存任何状态嘛。解决方案有几种:

前端直接删掉 token:最简单的做法,用户点退出就把 localStorage 里的 token 清了。但这样 token 本身还是有效的(在过期时间之前),如果有人拿到了这个 token 还是能用

-Redis 黑名单:在服务端搞个 Redis,把要废弃的 token 存进去,过滤器里多一步检查 token 在不在黑名单中。这个方案比较稳妥,但引入了额外的复杂度

对于大部分项目,方案一就够用了。如果对安全性要求比较高的话(比如金融类应用),考虑方案二。

每次请求都解析 JWT 会不会很慢?

        放心,JWT 解析只是做 Base64 解码和 HMAC 签名验证,速度是非常快的,基本不会成为性能瓶颈。除非你的 QPS 达到了非常恐怖的数量级(比如几十万),那时候你已经是架构师大佬了,不需要看我这篇文章了 (๑•́ ₃ •̀๑)

最后总结一下

        OK啊,到这里一个完整的 SpringBoot + JWT 登录认证就整完了。来捋一捋咱干了啥:

1. 引入 jjwt 依赖,准备好工具

2. 写了个 `JwtUtils` 工具类来生成和解析 token

3. 整了个 `JwtAuthenticationFilter` 过滤器拦截请求

4. 配置了 Spring Security,关掉 Session,注册 JWT 过滤器

5. 写了登录和注册接口

6. 搞了个获取当前用户信息的接口

整个流程其实不复杂,核心就是:登录时发 token,请求时带 token,过滤器验证 token,完事!

当然这只是最基础的实现,实际项目中可能还需要考虑角色权限控制、token 刷新、多端登录互踢等进阶需求,但万变不离其宗,把基础搞明白了,后面的都好说。

以上是个人的一些经验分享,如果有哪里有什么错误的地方也请大佬们指出,咱一起学习一起进步。

                                                                                                                        本文完结撒花!!!

更多推荐