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

Spring 后端安全双剑(上篇):JWT 无状态认证 + 密码加盐加密实战

目录

  • 引言
  • 一、令牌技术(Token):无状态认证的核心方案
    • 传统思路下存在的问题
    • 什么是令牌技术
    • JWT(JSON Web Token)
      • JWT组成
      • JWT令牌⽣成和校验
      • 运用实战

引言

在 Java Spring 后端开发中,“安全”永远是绕不开的话题,用户登录如何免 Session 认证?用户密码如何防止泄露?我们通过上篇令牌技术下篇加盐/加密,这两个核心技术,来聊聊关于如何让后端的认证体系更安全、更可靠

一、令牌技术(Token):无状态认证的核心方案

传统思路下存在的问题

在传统的思路下,实现登录接口无非就这几个步骤

  • 登陆⻚⾯把⽤⼾名密码提交给服务器
  • 服务器端验证⽤⼾名密码是否正确,并返回校验结果给后端
  • 如果密码正确,则在服务器端创建Session.通过Cookie把sessionId返回给浏览器

存在问题

但是你可以仔细一想,在开发的项⽬中,在企业中很少会部署在⼀台机器上,容易发⽣单点故障.(单点故障:⼀旦这台服务器挂了,整个应⽤都没法访问了).所以通常情况下,⼀个Web应⽤会部署在多个服务器上,通过Nginx等进⾏负载均衡.此时,来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上

假如我们使⽤Session进⾏会话跟踪,我们来思考如下场景:

  1. 首次请求
    客户端发起请求,经过负载均衡分配到其中一台服务器;这台服务器创建Session并存储,同时生成SessionId,通过响应把SessionId 存到客户端 Cookie 里

  2. 后续请求
    客户端再次请求时,会携带Cookie(包含 SessionId);但负载均衡可能把请求分配到另一台服务器,这台服务器本地没有之前存储的Session,所以通过SessionId 查不到对应的会话信息,导致 “会话失效”。

也就是说Session 存在单台服务器本地,多服务器之间没有共享 Session 数据,赵成跨服务器请求会丢失会话状态。

什么是负载均衡
其实负载均衡可以理解为分布式系统里的 “流量调度员”
它处于客户端和多台服务器之间,核心作用是把客户端发来的请求合理分配到不同的服务器上:既避免单台服务器被大量请求 “压垮”,也能在某台服务器故障时,把请求转移到其他正常机器,以此提升系统的并发处理能力和稳定性。

什么是令牌技术

令牌(Token)的本质:是服务端颁发给客户端的 “临时身份凭证”,本质是一段经过加密的字符串,包含用户身份、权限、过期时间等核心信息,服务器具备⽣成令牌和验证令牌的能⼒
你可以把它理解为:用户登录成功后,服务端给的一张 “电子通行证”—— 后续客户端访问需要权限的接口时,只需出示这张 “通行证”,服务端验证通过就允许访问,无需再重复验证用户名密码

我们使⽤令牌技术,继续思考上述场景:

  1. ⽤⼾登录 ⽤⼾登录请求,经过负载均衡,把请求转给了第⼀台服务器,第⼀台服务器进⾏账号密码验证,验证成功后,⽣成⼀个令牌,并返回给客⼾端
  2. 客⼾端收到令牌之后,把令牌存储起来.可以存储在Cookie中,也可以存储在其他的存储空间(⽐如localStorage)
  3. 查询操作 ⽤⼾登录成功之后,携带令牌继续执⾏查询操作,⽐如查询博客列表.此时请求转发到了第⼆台机器,第⼆台机器会先进⾏权限验证操作.服务器验证令牌是否有效,如果有效,就说明⽤⼾已经执⾏了登录操作,如果令牌是⽆效的,就说明⽤⼾之前未执⾏登录操作.

优点显而易见

  1. 无状态:服务端无需存储,支持分布式部署
  2. 轻量级:⽆需在服务器端存储,传输成本低,适配各种客户端

缺点的话可能是需要自己实现(包括令牌的⽣成,令牌的传递,令牌的校验)

JWT(JSON Web Token)

JWT是最常用的轻量级令牌​是目前最流行的令牌标准,核心是 “自包含信息 + 数字签名”,无需服务端存储状态,本质就是⼀个字符串,这篇文章主要使用JWT令牌来实现

JWT组成

JWT由三部分组成,每部分中间使⽤点(.)分隔,⽐如:aaaaa.bbbbb.cccc

部分名称作用格式示例(Base64 解码后)
第一部分Header(头部)声明令牌类型和签名算法{“alg”:“HS256”,“typ”:“JWT”}
第二部分Payload(负载)存储核心业务信息(用户 ID、角色等){“userId”:1,“role”:“ADMIN”,“exp”:1616270640}
第三部分Signature(签名)验证令牌完整性和真实性(HMAC256 算法加密结果,无法直接解码)

Payload(负载)部分不建议存放敏感信息,因为此部分可以解码还原原始内容

JWT官网描述

JWT令牌⽣成和校验

需要引⼊JWT令牌的依赖

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><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><!-- or jjwt-gson if Gson is preferred --><version>0.11.5</version><scope>runtime</scope></dependency>

使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验

先通过@Test测试方法生成密钥

@TestvoidgenKey(){//生成密钥Keykey=Keys.secretKeyFor(SignatureAlgorithm.HS256);StringsecretString=Encoders.BASE64.encode(key.getEncoded());System.out.println(secretString);//uKdkjBTVIskTsz91VImoh6+CL+HZKOcWDce1kEuCNaE=}

⽣成令牌

privatestaticfinalStringsecretString="uKdkjBTVIskTsz91VImoh6+CL+HZKOcWDce1kEuCNaE=";//⽣成安全密钥privatestaticfinalSecretKeyKEY=Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));//⾃定义信息Map<String,Object>claims=newHashMap<>();claims.put("id",12);claims.put("username","admin");//生成 tokenStringcompact=Jwts.builder().setClaims(claims)//⾃定义内容(负载).signWith(key)//签名算法.compact();System.out.println(compact);//eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTIsInVzZXJuYW1lIjoiYWRtaW4ifQ.TlsChs_dzYex6pYnym8x2M7zAtcxjl2EBolAbYNvMdM//解析JwtParserbuild=Jwts.parserBuilder().setSigningKey(key).build();System.out.println(build.parse(compact).getBody());//{id=12, username=admin}

运行结果:

上述第一行输出的内容,就是JWT令牌
可以直接进行解析如上图第二行所示,也可以通过点(.)对三个部分进⾏分割,我们把⽣成的令牌通过官⽹进⾏解析

注意:
对于密钥有⻓度和内容有要求
如何长度不符合要求就会发生一下错误

  • 你使用的密钥字节数组仅为32 位,不符合 JWT HMAC-SHA 算法的安全要求;
  • 根据 JWA 规范(RFC 7518),HMAC-SHA 算法的密钥长度必须≥256 位(密钥长度需不小于哈希输出长度)。

运用实战

  1. 登陆⻚⾯把⽤⼾名密码提交给服务器.
  2. 服务器端验证⽤⼾名密码是否正确,如果正确,服务器⽣成令牌,下发给客⼾端.
  3. 客⼾端把令牌存储起来(⽐如Cookie,localstorage等),后续请求时,把token发给服务器
  4. 服务器对令牌进⾏校验, 如果令牌正确,进⾏下⼀步操作


生成请求和响应实体类

@Data@AllArgsConstructorpublicclassUserLoginResponse{privateIntegeruserId;privateStringtoken;}
@DatapublicclassUserLoginRequest{@NotNull(message="用户名不能为空")privateStringuserName;@NotNull(message="密码不能为空")privateStringpassword;}

生成验证和生成JWT工具类

@Slf4jpublicclassJwtUtils{//使用固定密钥privatestaticStringSECRET_StRING="rPsPc6787n3N5+LjmAgaF85ga6I1qrHdQaasH8tKwTs=";publicstaticKeykey=Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_StRING));publicstaticStringgenToken(Map<String,Object>claims){//生成 tokenStringcompact=Jwts.builder().setClaims(claims).signWith(key).compact();returncompact;}publicstaticClaimsparseToken(Stringtoken){if(!StringUtils.hasLength(token)){returnnull;}//解析JwtParserbuild=Jwts.parserBuilder().setSigningKey(key).build();Claimsclaims=null;try{claims=build.parseClaimsJws(token).getBody();}catch(Exceptione){log.error("解析token异常",token);}returnclaims;}

验证账号密码和生成token:

publicUserLoginResponsecheckPassword(UserLoginRequestuserLoginRequest){QueryWrapper<UserInfo>queryWrapper=newQueryWrapper<>();queryWrapper.lambda().eq(UserInfo::getUserName,userLoginRequest.getUserName()).eq(UserInfo::getDeleteFlag,0);UserInfouserInfo=userInfoMapper.selectOne(queryWrapper);if(userInfo==null){//用户不存在thrownewBlogException("用户不存在");}if(!SecurityUtil.verify(userLoginRequest.getPassword(),userInfo.getPassword())){thrownewBlogException("用户密码错误");}//密码正确Map<String,Object>map=newHashMap<>();map.put("id",userInfo.getId());map.put("name",userInfo.getUserName());Stringtoken=JwtUtils.genToken(map);returnnewUserLoginResponse(userInfo.getId(),token);}

在统一拦截中验证token:

@Slf4j@ComponentpublicclassLoginInterceptorimplementsHandlerInterceptor{//约定前端把token放在header里@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{StringuserToken=request.getHeader(Constants.USER_TOKEN_HEADER_KEY);log.info("从header中获得用户token:{}",userToken);if(userToken==null){//拦截response.setStatus(401);returnfalse;}//校验token是否合法Claimsclaims=JwtUtils.parseToken(userToken);if(claims==null){//拦截response.setStatus(401);returnfalse;}returntrue;}}

前端代码不赘述,主要是通过localStorage.setItem,将token存在浏览器中

当再次进入页面,后端就会将我们的携带登录中生成的唯一的token进行验证
如果当我们在客户端中错误的token或者无token,就会导致验证不通过,无法进入此页面,从而实现强制要求登陆

http://www.cnnetsun.cn/news/6183.html

相关文章:

  • 5分钟掌握Ant Design Vue Pro Components:打造企业级Vue3应用的终极方案
  • Nilesoft Shell终极配置手册:从入门到精通
  • Qwen3-VL-4B-Instruct-FP8终极指南:重新定义边缘多模态AI
  • NumCpp终极指南:C++科学计算的完整解决方案
  • 从零开始搭建量子模拟环境,全面解析VSCode+Jupyter协同工作流
  • 用 Python 打造一个图形化局域网扫描器:实战网络设备发现工具
  • Android摄像头调试终极指南:V4L2 Camera APK快速上手
  • 36、Red Hat KVM 虚拟化实战指南
  • 861-LangChain框架Use-Cases - Gemini多模态RAG案例分析报告
  • vnpy可视化技术:5步打造专业级K线图表与交易界面
  • 告别机械感,亲测5款AI小说写作工具!让创作更对味
  • 1.3万亿令牌教育数据集登场:FineWeb-Edu如何重塑AI学习能力?
  • 河道水位如何实时掌握?1套监测站的故事,防汛抗旱有了“千里眼”。
  • 从微信群到智能社区:KoalaQA如何重塑企业售后服务新生态
  • 免费获取自动控制原理第3版PDF教材,开启自动化学习之旅
  • 专科女生学云计算前景好吗?怎么样?好就业吗?有前途吗?
  • Responder网络工具配置优化与故障排除实战指南
  • UnityLive2DExtractor终极指南:快速提取Live2D Cubism资源
  • AgentBench完整使用指南:快速上手LLM智能体评测框架
  • Wan2.2-T2V-5B生成视频可用于智能家居场景模拟
  • C++ 虚构造机制深度解析
  • 保护进程的驱动,真正的驱动保护,小弟弟手写并测试通过(直接可以编译)
  • 生成引擎优化(GEO)在优化网站内容与提升访客体验中的实践价值分析
  • LSTM-VAE用于特征提取和数据降维
  • 数据结构——二叉树
  • Qwen3-Next-80B-A3B-Thinking:仅激活3B参数实现800亿模型性能,大模型效率革命深度解析
  • 揭秘FSNotes:现代笔记管理的智能解决方案实战指南
  • Wan2.2-T2V-A14B在游戏开发中的应用:快速制作剧情动画
  • Redmine项目管理平台终极使用指南:新手必读FAQ
  • 3大核心技能带你玩转大规模并行处理器编程