当前位置: 首页 > backend >正文

智慧社区项目开发(二)——基于 JWT 的登录验证功能实现详解

在 Web 应用中,登录验证是保障系统安全的核心环节。本文将结合具体接口文档,详细讲解如何基于 JWT(JSON Web Token)实现登录验证功能,包括 JWT 配置、工具类封装、登录流程处理等关键步骤,帮助开发者快速理解并落地类似功能。

一、需求分析:接口文档解读

本次实现的登录验证功能需满足以下接口文档要求,核心接口包括:

接口名称请求方式接口地址核心功能描述
生成验证码GET/captcha生成验证码并存储(用于登录校验)
用户登录POST/login校验用户信息,生成 JWT 令牌返回
Token 校验GET/checkToken验证令牌有效性
退出登录POST/logout清除令牌,退出登录

其中,登录接口(/login) 是核心,需接收前端传递的username(用户名)、password(密码)、captcha(验证码)、uuid(验证码唯一标识),验证通过后返回token(令牌)和expire(令牌过期时间)。

二、技术选型:JWT 为何适合登录验证?

JWT 是一种基于 JSON 的轻量级令牌,用于在客户端和服务器之间安全传递信息。其优势在于:

  • 无状态:服务器无需存储会话信息,令牌本身包含用户身份等关键信息,适合分布式系统。
  • 安全性:通过签名机制确保令牌不被篡改。
  • 自包含:可在令牌中嵌入用户权限等信息,减少数据库查询。

本次使用jjwt库实现 JWT 功能,配合 Redis 存储验证码和令牌,兼顾安全性与效率。

三、实现步骤:从配置到接口落地

1. JWT 配置:基础参数定义

首先在配置文件中定义 JWT 的核心参数,用于生成和验证令牌:

jwt:secret: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4K67DMlSPXbgG0MPp0gH  # 签名密钥(需保密)expire: 86400000  # 令牌过期时间(毫秒),此处为24小时subject: door  # 令牌主题(可选,用于标识令牌用途)
  • secret:签名密钥,生成令牌时用于加密,验证时用于解密,需确保安全性(建议生产环境使用更长更复杂的密钥)。
  • expire:对应登录接口返回的expire字段,控制令牌有效期。

2. 依赖导入:引入 JWT 工具库

pom.xml中引入jjwt依赖,用于处理 JWT 的生成与解析:

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

该版本稳定且功能完善,支持 HS256 等签名算法,满足本次需求。

3. JWT 工具类:封装令牌核心操作

封装JwtUtil工具类,实现令牌的生成、校验等核心功能,代码如下

package com.qcby.community.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.util.Date;
import java.util.UUID;@ConfigurationProperties(prefix = "jwt") // 绑定配置文件中jwt前缀的参数
@Component
public class JwtUtil {private long expire; // 过期时间(从配置文件注入)private String secret; // 签名密钥(从配置文件注入)private String subject; // 令牌主题(从配置文件注入)/*** 生成令牌* @param userId 用户ID(作为令牌中的核心标识)* @return 生成的JWT令牌字符串*/public String createToken(String userId) {return Jwts.builder().claim("userId", userId) // 自定义载荷:存储用户ID.setSubject(subject) // 令牌主题.setExpiration(new Date(System.currentTimeMillis() + expire)) // 过期时间.setId(UUID.randomUUID().toString()) // 唯一标识(可选).signWith(SignatureAlgorithm.HS256, secret) // 使用HS256算法签名.compact(); // 组装令牌}/*** 校验令牌有效性* @param token 待校验的令牌* @return 校验结果(true:有效;false:无效)*/public boolean checkToken(String token){if(StringUtils.isEmpty(token)){return false; // 令牌为空,直接无效}try {// 解析令牌(自动验证签名和过期时间)Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secret).parseClaimsJws(token);return true; // 解析成功,令牌有效} catch (Exception e) {// 解析失败(签名错误、过期等),令牌无效return false;}}// getter/setter(用于注入配置参数)public long getExpire() { return expire; }public void setExpire(long expire) { this.expire = expire; }public String getSecret() { return secret; }public void setSecret(String secret) { this.secret = secret; }public String getSubject() { return subject; }public void setSubject(String subject) { this.subject = subject; }
}

核心说明

  • createToken方法:根据用户 ID 生成令牌,包含用户标识、过期时间等信息,通过secret签名确保不可篡改,对应登录接口成功后返回的token
  • checkToken方法:用于验证令牌有效性(包括签名正确性和是否过期),对应/checkToken接口的核心逻辑。

4. 登录接口实现:完整流程处理

登录接口(/login)是验证流程的核心,需完成验证码校验、用户信息验证、令牌生成等步骤,代码如下:

@RestController
public class LoginController {@Autowiredprivate JwtUtil jwtUtil; // 注入JWT工具类@Autowiredprivate UserService userService; // 用户服务@Autowiredprivate RedisTemplate redisTemplate; // Redis模板(用于存储验证码和令牌)/*** 处理登录请求* @param loginForm 前端传递的登录参数(包含username、password、captcha、uuid)* @return 登录结果(成功返回token和expire;失败返回错误信息)*/@PostMapping("/login")public Result login(@RequestBody LoginForm loginForm){// 1. 验证码校验(基于Redis)// 从Redis获取验证码(键为uuid,对应生成验证码接口返回的uuid)String code = (String) redisTemplate.opsForValue().get(loginForm.getUuid());if(code == null){return Result.ok().put("status", "fail").put("data", "验证码已过期");}if(!code.equals(loginForm.getCaptcha())){return Result.ok().put("status", "fail").put("data", "验证码错误");}// 2. 验证用户名QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username", loginForm.getUsername());User user = userService.getOne(queryWrapper);if(user == null){return Result.error("用户名错误");}// 3. 验证密码(SHA256加密比对)String password = SecureUtil.sha256(loginForm.getPassword()); // 前端密码加密if(!password.equals(user.getPassword())){ // 与数据库中加密后的密码比对return Result.error("密码错误");}// 4. 验证用户状态(是否被锁定)if(user.getStatus() == 0) {return Result.error("账号已被锁定,请联系管理员");}// 5. 登录成功:生成令牌并返回String token = jwtUtil.createToken(String.valueOf(user.getUserId())); // 生成token// 将token存入Redis(键为"communityuser-用户ID",过期时间与token一致)redisTemplate.opsForValue().set("communityuser-"+user.getUserId(), token, jwtUtil.getExpire(), TimeUnit.MILLISECONDS);// 组装返回结果(符合接口文档:data包含token和expire)Map<String,Object> map = new HashMap<>();map.put("token", token);map.put("expire", jwtUtil.getExpire());return Result.ok().put("data", map);}
}

流程对应接口文档说明

  • 参数接收LoginForm包含uuid(验证码标识)、captcha(验证码)、username(用户名)、password(密码),完全匹配登录接口的请求参数。
  • 验证码校验:通过uuid从 Redis 获取验证码(生成验证码接口会将uuidcode存入 Redis),验证过期和正确性,对应生成验证码接口的交互逻辑。
  • 返回结果:登录成功时,返回data对象包含tokenexpire,与接口文档中登录成功的返回结构一致;失败时返回对应错误信息。

5. Token 校验接口实现

基于JwtUtilcheckToken方法,实现/checkToken接口:

@GetMapping("/checkToken")
public Result checkToken(HttpServletRequest request){// 从请求头获取token(假设前端将token放在Authorization头中)String token = request.getHeader("Authorization");boolean valid = jwtUtil.checkToken(token);if(valid){return Result.ok().put("status", "ok");}else{return Result.ok().put("status", "error");}
}

该接口直接调用JwtUtil的校验方法,返回statusokerror,完全符合接口文档要求。

可以先在前端的 permission.js里代码进行修改,

import router from "./router";
import store from "./store";
import { Message } from "element-ui";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { getToken } from "@/utils/auth";
import getPageTitle from "@/utils/get-page-title";
import { checkToken } from "@/api/sys/login";NProgress.configure({ showSpinner: false });const whiteList = ["/login", "/auth-redirect"]; // 没有重定向白名单router.beforeEach(async (to, from, next) => {NProgress.start();// 设置页面标题document.title = getPageTitle(to.meta.title);let token = getToken();if (token) {//校验TokencheckToken(token).then(res => {if (res.code === 200 && res.status === "error") {next({ path: "/error" });}});if (to.path === "/login") {// 如果已登录,请重定向到主页next({ path: "/" });NProgress.done();} else {// 确定用户是否通过getInfo获取了权限角色const hasRoles = store.getters.roles && store.getters.roles.length > 0;if (hasRoles) {next();} else {next();// try {//   // 获取用户信息//   const { routers } = await store.dispatch("user/getInfo");//   // 基于角色生成可访问的路由映射//   const accessRoutes = await store.dispatch(//     "permission/generateRoutes",//     { routers }//   );//   // 动态添加可访问的路由//   router.addRoutes(accessRoutes);//   // hack方法 确保addRoutes已完成//   // 设置replace:true,这样导航就不会留下历史记录//   next({ ...to, replace: true });// } catch (error) {//   // 删除令牌并转到登录页重新登录//   await store.dispatch("user/resetToken");//   Message.error(error || "Has Error");//   // next(`/login?redirect=${to.path}`)//   next("/login");//   NProgress.done();// }}}} else {/* 没有token */if (whiteList.indexOf(to.path) !== -1) {// 在免登录白名单,直接进入next();} else {// 否则全部重定向到登录页// next(`/login?redirect=${to.path}`)next("/login");NProgress.done();}}
});router.afterEach(() => {// 结束进度条NProgress.done();
});

修改前的逻辑(注释部分)

原本的代码实现了完整的权限控制流程:

  1. 用户登录后,获取用户角色和权限信息(通过 store.dispatch("user/getInfo"))。
  2. 根据用户角色动态生成可访问的路由(通过 store.dispatch("permission/generateRoutes"))。
  3. 使用 router.addRoutes() 动态添加路由,确保用户只能访问其权限范围内的页面。

修改后的逻辑(直接 next()

当你将这部分代码注释掉并直接调用 next() 时,会发生以下变化:

  1. 权限控制失效
    所有用户(无论是否登录、拥有何种角色)都可以访问任意路由,包括需要特定权限的页面。例如,普通用户可能可以访问管理员页面。

  2. 动态路由未生成
    router.addRoutes() 未执行,意味着基于用户角色的动态路由配置不会生效。应用可能只能访问静态定义的基础路由。

  3. 用户信息未获取
    store.dispatch("user/getInfo") 未执行,Vuex 中不会存储用户角色、权限等信息,导致页面上可能无法正确显示与用户相关的内容(如用户名、头像、导航菜单)。

http://www.xdnf.cn/news/16491.html

相关文章:

  • 分布式高可用架构核心:复制、冗余与生死陷阱——从主从灾难到无主冲突的避坑指南
  • 【Linux篇】进程间通信:进程IPC
  • 负载均衡算法中的加权随机算法
  • kafka开启Kerberos使用方式
  • uniapp_微信小程序_根据胶囊按钮计算出的导航栏高度为什么不是44px?
  • 【Linux】Ubuntu上安装.NET 9运行时与ASP.NET Core项目部署入门
  • 复杂人流场景口罩识别漏检率↓76%:陌讯动态特征融合算法实战解析
  • 开源智能体-JoyAgent集成ollama私有化模型
  • ATF 运行时服务
  • 标准解读——2024 数据资产价值评估指南(正式版)【附全文阅读】
  • ICDC自动化部署方案概述
  • 7.28 错题(zz)史纲 第五章新道路
  • Qt_Gif_Creator 基于Qt的屏幕gif录制工具
  • 灵动画布:快手可灵 AI 推出的多人协作 AI 创意工作台
  • PostgreSQL日志配置全解析:从基础设置到进阶策略
  • 墨者:SQL手工注入漏洞测试(MySQL数据库-字符型)
  • LangGraph智能体(天气和新闻助手)开发与部署
  • MySQL的常用数据类型详解
  • ROS2编写一个简单的插件
  • 2025年7月一区SCI-基尔霍夫定律优化算法Kirchhoff’s law algorithm-附Matlab免费代码
  • HDFS Block与Spark的partition对比
  • 基于AFLFast的fuzz自动化漏洞挖掘(2)
  • 中型企业如何用 RUM 技术破解地理分布式用户体验难题?从指标监测到优化实操
  • 嵌入式开发学习———Linux环境下数据结构学习(四)
  • Cacti RCE漏洞复现
  • 【AlphaFold3】网络架构篇(2)|Input Embedding 对输入进行特征嵌入
  • halcon-blob
  • docker 入门,运行上传自己的首个镜像
  • 学习人工智能所需知识体系及路径详解
  • CTF-Web学习笔记:文件包含篇