基于SpringBoot+JWT 实现Token登录认证与登录人信息查询

在前后端分离项目中,传统的Session登录认证已无法满足需求------Session依赖服务器内存存储,分布式部署时会出现认证失效问题,且存在跨域、安全性不足等弊端。而JWT(JSON Web Token)作为一种无状态、轻量级的认证方式,无需在服务器存储会话信息,仅通过客户端携带Token即可完成认证,完美解决上述问题。

本文将基于SpringBoot框架,结合JJWT工具包,完整实现"Token生成→登录认证→Token解析→登录人信息查询"全流程,全程结合可直接复用的代码,拆解每一步核心逻辑,从依赖配置到接口实现,再到避坑指南,覆盖实战开发所有关键要点,新手也能快速上手落地。

核心技术栈:SpringBoot(后端框架)+ JJWT(JWT工具包,简化Token生成与解析)+ SpringMVC(接口开发),无需复杂配置,依赖引入后即可快速开发,代码可直接集成到管理系统、APP后端等各类前后端分离项目中。

一、核心原理:JWT为什么能实现无状态认证?

在讲解代码实现前,先简单理解JWT的核心逻辑,避免只懂代码、不懂原理:

  1. JWT结构:由三部分组成(Header+Payload+Signature),通过Base64编码和签名拼接而成,整体为一串字符串,可直接在HTTP请求头中携带。

    1. Header(头部):指定签名算法(如HS256)和Token类型,Base64编码后不可加密(仅编码,非加密)。

    2. Payload(载荷):存储核心业务信息(如用户ID、用户名),可设置过期时间,同样是Base64编码(注意:Payload不加密,不可存储敏感信息,如密码)。

    3. Signature(签名):用自定义密钥对Header和Payload进行签名,确保Token不被篡改------服务器接收Token后,会用相同的密钥验证签名,若签名不一致则Token无效。

  2. 认证流程

      1. 客户端提交用户名/密码,服务器验证通过后,生成JWT Token并返回给客户端。
      1. 客户端接收Token后,存储在本地(如localStorage、Cookie),后续所有请求都在请求头中携带Token。
      1. 服务器拦截所有需要认证的请求,解析请求头中的Token,验证Token有效性(签名是否正确、是否过期)。
      1. Token验证通过后,解析出Payload中的用户标识(如用户ID),根据标识查询登录人信息,返回给客户端。
  3. 核心优势:无状态(服务器无需存储会话信息,减轻服务器压力)、支持跨域、可分布式部署、轻量级(Token字符串体积小,传输效率高)。

二、前期准备:依赖配置与核心参数配置

实现JWT认证,需先引入JJWT依赖(负责Token的生成、解析和验证),再配置JWT核心参数(密钥、过期时间),这是实现后续功能的基础。

2.1 引入JJWT依赖(pom.xml)

本文使用JJWT 0.11.5版本(稳定版本,兼容SpringBoot 2.x/3.x),需引入3个依赖(api、impl、jackson,分别负责核心API、运行时实现、JSON序列化):

XML 复制代码
<!-- JWT核心依赖:提供Token生成、解析的API -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<!-- JWT运行时依赖:实现核心API的底层逻辑 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope> <!-- 仅运行时生效,编译时不依赖 -->
</dependency>
<!-- JWT JSON序列化依赖:用于Payload中JSON数据的序列化/反序列化 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

2.2 配置JWT核心参数(application.yml)

将JWT的密钥(secret)和Token过期时间(expiration)配置在配置文件yaml中,避免硬编码,便于后续修改和部署,参数说明如下:

  • secret:密钥,必须至少32位(JJWT的HS256算法要求密钥长度不低于256位,即32个字符),可自定义(如字母+数字组合),核心作用是签名Token,必须保密,不可泄露。

  • expiration:Token过期时间,单位为秒(本文配置7200秒,即2小时,可根据业务需求调整,如后台管理系统可设置1小时,APP可设置7天)。

XML 复制代码
jwt:
  secret: "zxcvbnmlkjhgfdsaqwertyuiop123456" # 自定义密钥,长度≥32位,建议生产环境使用更复杂的密钥
  expiration: 7200 # Token过期时间(秒),7200秒=2小时

三、核心功能实现(结合代码,逐模块拆解)

本文实现3个核心功能,串联起Token登录与信息查询的全流程:1. JWT工具类(Token生成、解析、验证);2. 登录接口(验证用户身份,生成Token并返回);3. 登录人信息查询接口(解析Token,获取用户标识,查询并返回用户信息)。

模块1:JWT工具类(JwtUtil)------ 核心工具

封装JWT的核心操作:生成Token、解析Token获取用户标识、验证Token有效性,该类交给Spring管理(@Component),通过@Value注入配置文件中的参数,可在其他类中直接注入使用。

java 复制代码
package com.qcby.aischool.util;

import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component // 交给Spring管理,支持依赖注入
public class JwtUtil {
    // 从配置文件注入Token过期时间(秒)
    @Value("${jwt.expiration}")
    private long expiration;
    // 从配置文件注入JWT密钥
    @Value("${jwt.secret}")
    private String secret;

    /**
     * 生成Token(核心方法)
     * @param id 存储在Token中的用户标识(如用户ID、用户名,本文用用户ID)
     * @return 生成的JWT Token字符串
     */
    public String generateToken(String id) {
        // 1. 根据密钥生成加密Key(HS256算法要求Key为HmacSHA256类型)
        Key key = Keys.hmacShaKeyFor(secret.getBytes());
        // 2. 构建Token并返回
        return Jwts.builder()
                .setSubject(id) // 设置Payload:存储用户标识(核心信息)
                .setIssuedAt(new Date()) // 设置签发时间(当前时间)
                // 设置过期时间:当前时间 + 过期时间(秒转毫秒)
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .signWith(key, SignatureAlgorithm.HS256) // 设置签名算法和密钥
                .compact(); // 生成最终的Token字符串
    }

    /**
     * 解析Token,获取存储在Payload中的用户标识(如用户ID)
     * @param token 客户端传递的Token字符串
     * @return 用户标识(本文返回用户ID)
     */
    public String getUsernameFromToken(String token) {
        // 1. 生成加密Key(与生成Token时的密钥一致)
        Key key = Keys.hmacShaKeyFor(secret.getBytes());
        // 2. 解析Token,获取Payload中的subject(即用户标识)
        return Jwts.parserBuilder()
                .setSigningKey(key) // 设置验证签名的密钥
                .build() // 构建解析器
                .parseClaimsJws(token) // 解析Token(若Token无效,会抛出JwtException)
                .getBody() // 获取Payload部分
                .getSubject(); // 获取subject(用户标识)
    }

    /**
     * 验证Token是否有效(核心方法)
     * @param token 客户端传递的Token字符串
     * @return true:Token有效;false:Token无效(签名错误、已过期、格式错误等)
     */
    public boolean validateToken(String token) {
        try {
            // 1. 生成加密Key
            Key key = Keys.hmacShaKeyFor(secret.getBytes());
            // 2. 解析Token,若解析成功且未过期,则Token有效
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            // 捕获异常:JwtException(Token签名错误、已过期等)、IllegalArgumentException(Token格式错误)
            return false;
        }
    }
}

关键细节与避坑点

  • 密钥生成:必须使用Keys.hmacShaKeyFor(secret.getBytes())生成Key,不可手动构建,否则会导致签名验证失败。

  • Payload存储:仅存储非敏感信息(如用户ID、用户名),绝对不能存储密码、手机号等敏感数据(因为Payload是Base64编码,可直接解码查看)。

  • 异常捕获:validateToken方法中捕获JwtException(Token相关异常)和IllegalArgumentException(参数异常),避免因Token无效导致程序崩溃。

  • 方法命名:getUsernameFromToken方法名可根据实际存储的信息修改(如getUserIdFromToken),本文因代码中存储的是用户ID,实际功能是获取用户ID。

模块2:登录接口(login)------ Token生成入口

登录接口是Token生成的入口,核心逻辑:接收客户端提交的用户名/密码,验证用户身份(查询数据库,判断用户名密码是否正确),验证通过后,调用JwtUtil生成Token,将Token返回给客户端,客户端后续请求携带该Token即可完成认证。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

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

@RestController // 标记为控制器,处理HTTP请求
public class LoginController {

    // 注入管理员服务(用于查询数据库,验证用户身份,替换为自己项目中的Service)
    @Autowired
    private AdministratorService administratorService;

    // 注入JWT工具类(用于生成Token)
    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 登录接口(客户端提交用户名/密码,获取Token)
     * @param user 客户端传递的登录信息(包含用户名、密码,封装为Administrator实体类)
     * @return 响应结果(成功返回Token,失败返回错误信息)
     */
    @PostMapping("/login")
    @ResponseBody
    public Result login(@RequestBody Administrator user){
        // 1. 验证用户身份:查询数据库,判断用户名/密码是否正确(实际项目中需加密密码后比对)
        List<Administrator> users = administratorService.login(user);
        
        // 2. 验证通过(查询到唯一用户),生成Token
        if (users.size() == 1){
            // 获取登录用户的ID,作为Token的Payload(用户标识)
            String userId = users.get(0).getId().toString();
            // 调用JwtUtil生成Token
            String token = jwtUtil.generateToken(userId);
            // 构建响应数据,将Token返回给客户端
            Map&lt;String,String&gt; data = new HashMap<>();
            data.put("token", token);
            // 返回成功响应(Result为自定义响应实体,包含code、msg、data)
            return new Result().success(data);
        }else {
            // 验证失败(未查询到用户或查询到多个用户),返回错误信息
            return new Result().error("用户名或密码错误");
        }
    }
}

核心注意点与优化建议

  • 密码验证:实际项目中,数据库存储的密码必须加密(如使用BCrypt加密),客户端传递的密码需先加密,再与数据库中的加密密码比对,避免明文密码传输和存储(本文代码为简化演示,未做加密处理,实际需补充)。

  • 用户校验:users.size() == 1 是简化校验,实际项目中可优化为"查询到唯一用户且密码匹配",避免因数据库数据异常(如重复用户)导致的问题。

  • 响应格式:使用自定义Result实体类统一响应格式(包含状态码、提示信息、数据),便于前端统一解析(如成功返回code=200,失败返回code=500)。

模块3:登录人信息查询接口(info)------ Token解析与信息查询

客户端登录成功并获取Token后,后续请求(如查询个人信息)需携带Token,服务器通过解析Token获取用户标识,再根据标识查询用户信息并返回。本文中,Token解析逻辑由拦截器完成(代码中未展示拦截器,后续补充),拦截器解析Token后,将用户ID存入request属性中,接口直接从request中获取用户ID即可。

java 复制代码
    /**
     * 登录人信息查询接口(需携带Token访问)
     * 前端请求路径:/api/user/info(可根据实际需求修改)
     * @param request 请求对象,用于获取拦截器解析后的用户ID
     * @return 登录人的完整信息(如管理员信息)
     */
    @GetMapping("/info")
    @ResponseBody
    public Result getLoginAdministratorInfo(HttpServletRequest request) {
        System.out.println("进入查询登录人信息的接口");
        // 1. 从request中获取拦截器解析的用户ID(拦截器已验证Token有效性)
        String id = (String) request.getAttribute("id");
        // 2. 根据用户ID查询数据库,获取登录人完整信息
        Administrator administrator = administratorService.findAdministratorById(Integer.valueOf(id));
        // 3. 返回登录人信息
        return Result.success(administrator);
    }

关键补充:Token拦截器(必加,代码补充)

上述接口能直接从request中获取用户ID,核心是Token拦截器------拦截所有需要认证的请求,验证Token有效性,解析Token中的用户ID,存入request属性中。补充拦截器代码(可直接复用):

java 复制代码
@Component
public class JwtInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从Header中获取Token(前端需将Token放在Authorization中)
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7); // 去掉"Bearer "前缀
        }
        // 2. 验证Token
        if (token == null || !jwtUtil.validateToken(token)) {
            response.setStatus(401); // 未授权
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\"}");
            return false;
        }
        // 3. Token有效,将用户ID存入request属性中
        String userId = jwtUtil.getUsernameFromToken(token);
        request.setAttribute("id", userId);
        // 4. 继续处理请求
        return true;
    }
}

拦截器配置(需注册到Spring,让拦截器生效):

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private JwtInterceptor jwtInterceptor;

    // 加自定义拦截器JwtInterceptor,设置拦截规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/administrator/login");  
    }
}

拦截器核心逻辑说明

  • Token获取:前端需将Token放在请求头的Authorization字段中,格式为"Bearer 生成的Token"(如Bearer eyJhbGciOiJIUzI1NiJ9...),拦截器截取前缀后获取真实Token。

  • 拦截范围:拦截所有请求(/**),排除登录接口(/login)------登录接口是获取Token的入口,无需认证即可访问。

  • 权限控制:Token无效或未携带时,返回401未授权,阻止访问接口;Token有效则放行,并将用户ID存入request,供接口查询使用。

四、完整流程串联与测试步骤

4.1 全流程串联(清晰理解Token的作用)

  1. 客户端提交用户名/密码,发送POST请求到/login接口。

  2. 服务器验证用户名/密码正确,调用JwtUtil.generateToken()生成Token(Payload中存储用户ID),返回Token给客户端。

  3. 客户端存储Token(如localStorage),后续访问/info接口时,在请求头中携带Token(Authorization: Bearer Token)。

  4. 服务器拦截器拦截/info请求,获取请求头中的Token,调用JwtUtil.validateToken()验证Token有效性。

  5. Token有效,调用JwtUtil.getUsernameFromToken()解析出用户ID,存入request属性中。

  6. /info接口从request中获取用户ID,查询数据库得到登录人信息,返回给客户端。

4.2 测试步骤(快速验证功能,可直接操作)

  1. 环境准备:启动SpringBoot项目,确保数据库连接正常(AdministratorService能正常查询用户),拦截器已注册生效。

  2. 登录测试:使用Postman发送POST请求(http://localhost:8080/login),请求体为JSON格式({"username":"admin","password":"123456"}),成功返回Token。

  3. 信息查询测试:发送GET请求(http://localhost:8080/info),在请求头中添加Authorization字段,值为"Bearer 上一步返回的Token",成功返回登录人的管理员信息。

  4. 无效Token测试:修改Token中的任意字符,再次发送/info请求,服务器返回401未授权,验证拦截器生效。

五、落地优化与避坑指南(实战必备)

上述代码可直接复用,但在实际生产环境中,需注意以下优化点和坑点,避免线上问题,提升系统安全性和稳定性。

5.1 核心优化点

  • 密码加密:数据库存储的密码必须使用BCrypt等不可逆加密算法加密,客户端传递的密码需先加密再与数据库比对,避免明文传输和存储。

  • 密钥安全:生产环境中,JWT密钥(secret)不能硬编码在配置文件中,建议通过环境变量、配置中心(如Nacos)注入,避免密钥泄露。

  • Token过期处理:前端需监听Token过期时间,在Token过期前自动刷新Token(如调用刷新Token接口),避免用户频繁登录;服务器可设置Token刷新机制(如 refreshToken)。

  • 日志优化:在拦截器、JwtUtil、接口中添加日志(如SLF4J+Logback),记录Token生成、验证、解析的关键信息,便于问题排查(如Token无效原因、登录失败原因)。

  • 接口权限细化:拦截器可根据用户角色(如管理员、普通用户)进行权限控制,不同角色可访问的接口不同,而非仅验证Token有效性。

5.2 常见坑点与解决方案

  • 坑点1:Token验证失败,提示签名错误------解决方案:检查生成Token和验证Token时使用的密钥是否一致,确保secret长度≥32位,避免密钥拼写错误。

  • 坑点2:Token解析时抛出JwtException(过期)------解决方案:检查Token过期时间配置,前端及时刷新Token,服务器返回401后,前端引导用户重新登录。

  • 坑点3:拦截器未生效,无需Token即可访问接口------解决方案:检查拦截器是否添加@Component注解(交给Spring管理),是否在WebMvcConfig中注册,拦截路径和排除路径是否配置正确。

  • 坑点4:前端携带Token后,服务器仍返回401------解决方案:检查请求头中Token的格式是否正确(是否添加Bearer前缀),Token是否被篡改,拦截器中Token截取逻辑是否正确。

  • 坑点5:Payload中存储敏感信息导致泄露------解决方案:仅存储非敏感信息(用户ID、用户名),密码、手机号等敏感信息绝对不能存入Payload。

六、总结与扩展

本文基于SpringBoot+JJWT,完整实现了Token登录认证与登录人信息查询功能,核心优势在于:

  1. 无状态认证:服务器无需存储会话信息,减轻服务器压力,支持分布式部署,解决传统Session认证的弊端。

  2. 代码简洁可复用:JWT工具类、拦截器、接口代码均可直接复用,只需替换实体类和Service,即可快速集成到各类前后端分离项目中。

  3. 安全性高:通过密钥签名Token,防止Token被篡改;Payload不存储敏感信息,降低信息泄露风险;拦截器统一拦截,确保接口安全。

扩展方向:

  • Token刷新机制:实现refreshToken接口,用户登录后返回accessToken(短期有效)和refreshToken(长期有效),accessToken过期后,用refreshToken刷新获取新的accessToken。

  • 多端适配:支持PC端、APP端、小程序端统一认证,通过Token中的设备信息,实现多端登录管理(如同一账号最多允许3台设备登录)。

  • 权限细化:结合Spring Security,实现基于角色的权限控制(RBAC),不同角色访问不同接口,提升系统安全性。

  • Token黑名单:实现Token注销功能(如用户退出登录),将注销的Token加入黑名单,即使Token未过期,也不允许访问接口。

如果在实现过程中遇到问题,可结合JJWT官方文档排查,也可留言交流探讨,助力快速落地JWT认证功能。

相关推荐
十月南城3 天前
Flink实时计算心智模型——流、窗口、水位线、状态与Checkpoint的协作
大数据·flink·wpf
听麟6 天前
HarmonyOS 6.0+ 跨端会议助手APP开发实战:多设备接续与智能纪要全流程落地
分布式·深度学习·华为·区块链·wpf·harmonyos
@hdd6 天前
Kubernetes 可观测性:Prometheus 监控、日志采集与告警
云原生·kubernetes·wpf·prometheus
zls3653656 天前
C# WPF canvas中绘制缺陷分布map
开发语言·c#·wpf
专注VB编程开发20年6 天前
c#Redis扣款锁的设计,多用户,多台电脑操作
wpf
闲人编程7 天前
定时任务与周期性调度
分布式·python·wpf·调度·cron·定时人物·周期性
zls3653657 天前
C# WPF canvas中绘制缺陷分布map并实现缩放
开发语言·c#·wpf
数据知道8 天前
PostgreSQL:Citus 分布式拓展,水平分片,支持海量数据与高并发
分布式·postgresql·wpf
闲人编程9 天前
Redis分布式锁实现
redis·分布式·wpf·进程··死锁·readlock