智慧社区项目开发(二)——基于 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.pswp.cn/news/916602.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/916602.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Jmeter的元件使用介绍:(七)后置处理器详解

Jmeter的后置处理器主要用于取样器执行后的提取数据操作。 Jmeter常用的后置处理器有:Json提取器、正则表达式提取器、边界提取器、Beanshell后置处理器。此外还有Xpath提取器、CSS选择器提取器等&#xff0c;由于这两项多用前端页面提取元素&#xff0c;目前的项目基本都是采…

Allure的安装,在Pytest中的简单使用以及生成测试报告

目录 1.Allure的安装 1--下载网址 2--选择对应系统版本下载 3--配置Allure环境变量 4--验证安装是否成功 5--配置JAVAJDK的环境变量&#xff08;如果已经配置&#xff0c;可以忽视这一步&#xff09; 2.python中pytestAllure 1--python安装Allure包 2--生成测试报告 1--使用pyt…

Oracle 数据库报 ora-00257 错误并且执行alter system switch logfile 命令卡死的解决过程

Oracle 数据库报 ora-00257 错误并且执行alter system switch logfile 命令卡死的解决过程 7月26日下午&#xff0c;某医院用户的 HIS 系统无法连接&#xff0c;报如下错误&#xff1a;初步判断是归档日志问题。 用户的 HIS 系统数据库是双节点 Oracle 11g Rac 集群。登录服务器…

ArKTS:List 数组

一种&#xff1a;/**# encoding: utf-8# 版权所有 2025 ©涂聚文有限公司™ # 许可信息查看&#xff1a;言語成了邀功盡責的功臣&#xff0c;還需要行爲每日來值班嗎# 描述&#xff1a; 数组# Author : geovindu,Geovin Du 涂聚文.# IDE : DevEco Studio 5.1.1 …

Spring Boot 3整合Spring AI实战:9轮面试对话解析AI应用开发

Spring Boot 3整合Spring AI实战&#xff1a;9轮面试对话解析AI应用开发 第1轮&#xff1a;基础配置与模型调用 周先生&#xff1a;cc&#xff0c;先聊聊Spring AI的基础配置吧。如何在Spring Boot 3项目中集成Ollama&#xff1f; cc&#xff1a;我们可以通过OllamaConfig.java…

标准SQL语句示例

一、基础操作1. 数据库操作-- 1. 创建数据库 CREATE DATABASE 数据库名称 CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;-- 2. 删除数据库 DROP DATABASE IF EXISTS 数据库名称;-- 3. 选择数据库 USE 数据库名称;-- 4. 显示所有数据库 SHOW DATABASES;-- 5. 查看数据库创…

STM32-基本定时器

一.基本定时器简介 STM32F1 系列共有 8 个定时器&#xff0c;分别为&#xff1a;基本定时器、通用定时器、高级定时器。基本定时器 TIM6 和 TIM7 是一个 16 位的只能向上计数的定时器&#xff0c;只能定时&#xff0c;没有外部IO。 二.基本定时器功能 上图为基本定时器的功能框…

ofd文件转pdf

主要后端使用Java实现&#xff0c;前端可随意搭配http请求添加依赖&#xff1a;<!-- OFD解析与转换库 --><dependency><groupId>org.ofdrw</groupId><artifactId>ofdrw-converter</artifactId><version>1.17.9</version></…

4.应用层自定义协议与序列化

1.应用层程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层1.1再谈“协议”协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢…

【QT搭建opencv环境】

本文参考以下文章&#xff1a; https://blog.csdn.net/weixin_43763292/article/details/112975207 https://blog.csdn.net/qq_44743171/article/details/124335100 使用软件 QT 5.14.2下载地址&#xff1a;download.qt.io 选择版本&#xff1a;Qt 5.14.2 Qt 5.14.2百度网盘链接…

golang--函数栈

一、函数栈的组成结构&#xff08;栈帧&#xff09; 每个函数调用对应一个栈帧&#xff0c;包含以下核心部分&#xff1a; 1. 参数区 (Arguments) 位置&#xff1a;栈帧顶部&#xff08;高地址端&#xff09;内容&#xff1a; 函数调用时传入的参数按从右向左顺序压栈&#xff…

【FAQ】创建Dynamics 365 Sales环境

参考文章&#xff1a;5 分钟内安装 Dynamics 365 Sales 步骤 1&#xff1a;访问 Power Platform 管理中心 导航到make.powerapps.com&#xff0c;然后点击右上角的齿轮图标。选择管理中心&#xff0c;或者访问aka.ms/ppac访问 Power Platform 管理中心。 第 2 步&#xff1a…

【数据库】使用Sql Server将分组后指定字段的行数据转为一个字段显示,并且以逗号隔开每个值,收藏不迷路

大家好&#xff0c;我是全栈小5&#xff0c;欢迎来到《小5讲堂》。 这是《Sql Server》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录前言示例数据集数…

7.项目起步(1)

1&#xff0c;项目起步-初始化项目并使用git管理创建项目并精细化配置src目录调整git 管理项目2项目起步-配置别名路径联想提示什么是别名路径联想提示如何进行配置 &#xff08;自动配置了&#xff09;{"compilerOptions" : {"baseUrl" : "./",…

【C++详解】深入解析继承 类模板继承、赋值兼容转换、派生类默认成员函数、多继承与菱形继承

文章目录一、继承概念二、继承定义定义格式继承后基类成员访问方式的变化类模板的继承三、基类和派⽣类间的转换(赋值兼容转换)四、继承中的作用域隐藏规则两道笔试常考题五、派生类的默认成员函数四个常见默认成员函数实现⼀个不能被继承的类六、继承与友元七、继承与静态成员…

加法器 以及ALU(逻辑算术单元)

加法器框架&#xff0c;首先介绍原理&#xff0c;然后引入一位加法器最后再引入多位加法器最后引入带符号的加法器这一节涉及到的硬件电路的知识理解就好&#xff0c;实在看不懂就跳过&#xff0c;但是封装以后的功能必须看懂。这是一个一般的加法过程涉及到的必要元素图中已经…

设计模式实战:自定义SpringIOC(亲手实践)

上一篇&#xff1a;设计模式实战&#xff1a;自定义SpringIOC&#xff08;理论分析&#xff09; 自定义SpringIOC&#xff08;亲手实践&#xff09; 上一篇文章&#xff0c;我们介绍了SpringIOC容器的核心组件及其作用&#xff0c;下面我们来动手仿写一个SpringIOC容器&#…

力扣面试150(42/150)

7.28 20. 有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一…

基于黑马教程——微服务架构解析(二):雪崩防护+分布式事务

之前的两篇文章我们介绍了微服务的基础概念及其服务间通信机制。本篇将深入探讨微服务的核心保障&#xff1a;服务保护与分布式事务。一、微服务保护问题描述&#xff1a; 在一个购物车的微服务中&#xff0c;倘若某一项服务&#xff08;服务A&#xff09;同一时刻访问的数据十…

LeetCode: 429 N叉树的层序遍历

题目描述给定一个 N 叉树&#xff0c;返回其节点值的层序遍历&#xff08;即从左到右&#xff0c;逐层访问每一层的所有节点&#xff09;。示例输入格式&#xff08;层序序列化&#xff09;&#xff1a;输入示意&#xff1a;1/ | \3 2 4/ \5 6输出&#xff1a;[[1], [3,2,4…